diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72700a3..a136641 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,6 +16,12 @@ jobs: fail-fast: false matrix: include: + - ruby: "3.4" + graphql-ruby: "2.5" + - ruby: "3.4" + graphql-ruby: "2.4" + - ruby: "3.3" + graphql-ruby: "2.3" - ruby: "3.3" graphql-ruby: "2.2" - ruby: "3.2" diff --git a/Appraisals b/Appraisals index a52fb26..61d5b77 100644 --- a/Appraisals +++ b/Appraisals @@ -25,3 +25,15 @@ end appraise "graphql-2.2" do gem "graphql", "~> 2.2.0" end + +appraise "graphql-2.3" do + gem "graphql", "~> 2.3.0" +end + +appraise "graphql-2.4" do + gem "graphql", "~> 2.4.0" +end + +appraise "graphql-2.5" do + gem "graphql", "~> 2.5.0" +end diff --git a/gemfiles/graphql_2.3.gemfile b/gemfiles/graphql_2.3.gemfile new file mode 100644 index 0000000..c1ecbaf --- /dev/null +++ b/gemfiles/graphql_2.3.gemfile @@ -0,0 +1,14 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "graphql", "~> 2.3.0" + +group :development, :test do + gem "pry" + gem "pry-inline" + gem "pry-byebug", platform: :mri + gem "graphql-batch" +end + +gemspec path: "../" diff --git a/gemfiles/graphql_2.3.gemfile.lock b/gemfiles/graphql_2.3.gemfile.lock new file mode 100644 index 0000000..247dfc3 --- /dev/null +++ b/gemfiles/graphql_2.3.gemfile.lock @@ -0,0 +1,80 @@ +PATH + remote: .. + specs: + yabeda-graphql (0.2.3) + graphql (>= 1.9, < 3) + yabeda (~> 0.2) + +GEM + remote: https://rubygems.org/ + specs: + anyway_config (2.7.2) + ruby-next-core (~> 1.0) + appraisal (2.5.0) + bundler + rake + thor (>= 0.14.0) + base64 (0.3.0) + byebug (12.0.0) + coderay (1.1.3) + concurrent-ruby (1.3.5) + diff-lcs (1.6.2) + dry-initializer (3.2.0) + fiber-storage (1.0.1) + graphql (2.3.22) + base64 + fiber-storage + graphql-batch (0.6.0) + graphql (>= 1.12.18, < 3) + promise.rb (~> 0.7.2) + method_source (1.1.0) + promise.rb (0.7.4) + pry (0.15.2) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.11.0) + byebug (~> 12.0) + pry (>= 0.13, < 0.16) + pry-inline (1.0.7) + pry (> 0.10.0) + unicode (~> 0.4.4) + rake (13.3.0) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.5) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.4) + ruby-next-core (1.1.2) + thor (1.3.2) + unicode (0.4.4.5) + yabeda (0.13.1) + anyway_config (>= 1.0, < 3) + concurrent-ruby + dry-initializer + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + appraisal + bundler + graphql (~> 2.3.0) + graphql-batch + pry + pry-byebug + pry-inline + rake (~> 13.0) + rspec (~> 3.0) + yabeda-graphql! + +BUNDLED WITH + 2.5.6 diff --git a/gemfiles/graphql_2.4.gemfile b/gemfiles/graphql_2.4.gemfile new file mode 100644 index 0000000..fafa33b --- /dev/null +++ b/gemfiles/graphql_2.4.gemfile @@ -0,0 +1,14 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "graphql", "~> 2.4.0" + +group :development, :test do + gem "pry" + gem "pry-inline" + gem "pry-byebug", platform: :mri + gem "graphql-batch" +end + +gemspec path: "../" diff --git a/gemfiles/graphql_2.4.gemfile.lock b/gemfiles/graphql_2.4.gemfile.lock new file mode 100644 index 0000000..c86c31e --- /dev/null +++ b/gemfiles/graphql_2.4.gemfile.lock @@ -0,0 +1,82 @@ +PATH + remote: .. + specs: + yabeda-graphql (0.2.3) + graphql (>= 1.9, < 3) + yabeda (~> 0.2) + +GEM + remote: https://rubygems.org/ + specs: + anyway_config (2.7.2) + ruby-next-core (~> 1.0) + appraisal (2.5.0) + bundler + rake + thor (>= 0.14.0) + base64 (0.3.0) + byebug (12.0.0) + coderay (1.1.3) + concurrent-ruby (1.3.5) + diff-lcs (1.6.2) + dry-initializer (3.2.0) + fiber-storage (1.0.1) + graphql (2.4.17) + base64 + fiber-storage + logger + graphql-batch (0.6.0) + graphql (>= 1.12.18, < 3) + promise.rb (~> 0.7.2) + logger (1.7.0) + method_source (1.1.0) + promise.rb (0.7.4) + pry (0.15.2) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.11.0) + byebug (~> 12.0) + pry (>= 0.13, < 0.16) + pry-inline (1.0.7) + pry (> 0.10.0) + unicode (~> 0.4.4) + rake (13.3.0) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.5) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.4) + ruby-next-core (1.1.2) + thor (1.3.2) + unicode (0.4.4.5) + yabeda (0.13.1) + anyway_config (>= 1.0, < 3) + concurrent-ruby + dry-initializer + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + appraisal + bundler + graphql (~> 2.4.0) + graphql-batch + pry + pry-byebug + pry-inline + rake (~> 13.0) + rspec (~> 3.0) + yabeda-graphql! + +BUNDLED WITH + 2.5.6 diff --git a/gemfiles/graphql_2.5.gemfile b/gemfiles/graphql_2.5.gemfile new file mode 100644 index 0000000..ac3512e --- /dev/null +++ b/gemfiles/graphql_2.5.gemfile @@ -0,0 +1,14 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "graphql", "~> 2.5.0" + +group :development, :test do + gem "pry" + gem "pry-inline" + gem "pry-byebug", platform: :mri + gem "graphql-batch" +end + +gemspec path: "../" diff --git a/gemfiles/graphql_2.5.gemfile.lock b/gemfiles/graphql_2.5.gemfile.lock new file mode 100644 index 0000000..d525bab --- /dev/null +++ b/gemfiles/graphql_2.5.gemfile.lock @@ -0,0 +1,82 @@ +PATH + remote: .. + specs: + yabeda-graphql (0.2.3) + graphql (>= 1.9, < 3) + yabeda (~> 0.2) + +GEM + remote: https://rubygems.org/ + specs: + anyway_config (2.7.2) + ruby-next-core (~> 1.0) + appraisal (2.5.0) + bundler + rake + thor (>= 0.14.0) + base64 (0.3.0) + byebug (12.0.0) + coderay (1.1.3) + concurrent-ruby (1.3.5) + diff-lcs (1.6.2) + dry-initializer (3.2.0) + fiber-storage (1.0.1) + graphql (2.5.9) + base64 + fiber-storage + logger + graphql-batch (0.6.0) + graphql (>= 1.12.18, < 3) + promise.rb (~> 0.7.2) + logger (1.7.0) + method_source (1.1.0) + promise.rb (0.7.4) + pry (0.15.2) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.11.0) + byebug (~> 12.0) + pry (>= 0.13, < 0.16) + pry-inline (1.0.7) + pry (> 0.10.0) + unicode (~> 0.4.4) + rake (13.3.0) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.5) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.4) + ruby-next-core (1.1.2) + thor (1.3.2) + unicode (0.4.4.5) + yabeda (0.13.1) + anyway_config (>= 1.0, < 3) + concurrent-ruby + dry-initializer + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + appraisal + bundler + graphql (~> 2.5.0) + graphql-batch + pry + pry-byebug + pry-inline + rake (~> 13.0) + rspec (~> 3.0) + yabeda-graphql! + +BUNDLED WITH + 2.5.23 diff --git a/lib/yabeda/graphql.rb b/lib/yabeda/graphql.rb index 82b1fa4..c8b4286 100644 --- a/lib/yabeda/graphql.rb +++ b/lib/yabeda/graphql.rb @@ -1,8 +1,15 @@ +require "graphql/version" + require "yabeda" require "yabeda/graphql/version" require "yabeda/graphql/yabeda_tracing" require "yabeda/graphql/instrumentation" +if Gem::Version.new(::GraphQL::VERSION) < Gem::Version.new("2.2.0") + require "yabeda/graphql/legacy/yabeda_tracing" + require "yabeda/graphql/legacy/instrumentation" +end + module Yabeda module GraphQL class Error < StandardError; end @@ -30,8 +37,13 @@ class Error < StandardError; end end def self.use(schema) - schema.instrument(:query, Instrumentation.new) - schema.use YabedaTracing, trace_scalars: true + if Gem::Version.new(::GraphQL::VERSION) >= Gem::Version.new( "2.2.0") + schema.trace_with Yabeda::GraphQL::Instrumentation + schema.trace_with Yabeda::GraphQL::YabedaTracing + else + schema.instrument :query, Legacy::Instrumentation.new + schema.use Legacy::YabedaTracing, trace_scalars: true + end end end end diff --git a/lib/yabeda/graphql/instrumentation.rb b/lib/yabeda/graphql/instrumentation.rb index af0ab7d..f032fa1 100644 --- a/lib/yabeda/graphql/instrumentation.rb +++ b/lib/yabeda/graphql/instrumentation.rb @@ -1,15 +1,17 @@ module Yabeda module GraphQL - class Instrumentation - def before_query(query) - reset_cache!(query) - end - - def after_query(query) - cache(query).each do |_path, options| - Yabeda.graphql.field_resolve_runtime.measure(options[:tags], options[:duration]) - Yabeda.graphql.fields_request_count.increment(options[:tags]) + module Instrumentation + def execute_multiplex(multiplex:) + queries = multiplex.queries + queries.each { |query| reset_cache!(query) } + result = super + queries.each do |query| + cache(query).each do |_path, options| + Yabeda.graphql.field_resolve_runtime.measure(options[:tags], options[:duration]) + Yabeda.graphql.fields_request_count.increment(options[:tags]) + end end + result end private diff --git a/lib/yabeda/graphql/legacy/instrumentation.rb b/lib/yabeda/graphql/legacy/instrumentation.rb new file mode 100644 index 0000000..452705a --- /dev/null +++ b/lib/yabeda/graphql/legacy/instrumentation.rb @@ -0,0 +1,29 @@ +module Yabeda + module GraphQL + module Legacy + class Instrumentation + def before_query(query) + reset_cache!(query) + end + + def after_query(query) + cache(query).each do |_path, options| + Yabeda.graphql.field_resolve_runtime.measure(options[:tags], options[:duration]) + Yabeda.graphql.fields_request_count.increment(options[:tags]) + end + end + + private + + def cache(query) + query.context.namespace(Yabeda::GraphQL)[:field_call_cache] + end + + def reset_cache!(query) + query.context.namespace(Yabeda::GraphQL)[:field_call_cache] = + Hash.new { |h,k| h[k] = { tags: {}, duration: 0.0 } } + end + end + end + end +end diff --git a/lib/yabeda/graphql/legacy/yabeda_tracing.rb b/lib/yabeda/graphql/legacy/yabeda_tracing.rb new file mode 100644 index 0000000..3e82bb1 --- /dev/null +++ b/lib/yabeda/graphql/legacy/yabeda_tracing.rb @@ -0,0 +1,102 @@ +require "graphql/tracing/platform_tracing" + +module Yabeda + module GraphQL + module Legacy + class YabedaTracing < ::GraphQL::Tracing::PlatformTracing + + self.platform_keys = { + 'lex' => "graphql.lex", + 'parse' => "graphql.parse", + 'validate' => "graphql.validate", + 'analyze_query' => "graphql.analyze", + 'analyze_multiplex' => "graphql.analyze", + 'execute_multiplex' => "graphql.execute", + 'execute_query' => "graphql.execute", + 'execute_query_lazy' => "graphql.execute", + 'execute_field' => "graphql.execute", + 'execute_field_lazy' => "graphql.execute" + } + + def platform_trace(platform_key, key, data, &block) + start = ::Process.clock_gettime ::Process::CLOCK_MONOTONIC + result = block.call + duration = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start + + case key + when "execute_field", "execute_field_lazy" + field, path, query = extract_field_trace_data(data) + + tags = extract_field_tags(field) + if path.length == 1 + return result if key == "execute_field" && query.schema.lazy?(result) + + if query.query? + instrument_query_execution(tags) + elsif query.mutation? + instrument_mutation_execution(tags) + elsif query.subscription? + # Not implemented yet + end + else + instrument_field_execution(query, path, tags, duration) + end + end + + result + end + + # See https://graphql-ruby.org/api-doc/1.10.5/GraphQL/Tracing + def extract_field_trace_data(data) + if data[:context] # Legacy non-interpreter mode + [data[:context].field, data[:context].path, data[:context].query] + else # Interpreter mode + data.values_at(:field, :path, :query) + end + end + + def extract_field_tags(field) + owner = field.respond_to?(:owner) ? field.owner : field.metadata[:type_class].owner + { + type: owner.graphql_name, + field: field.graphql_name, + deprecated: !field.deprecation_reason.nil?, + } + end + + def instrument_field_execution(query, path, tags, duration) + cache(query)[path][:tags] = tags + cache(query)[path][:duration] += duration + end + + def instrument_mutation_execution(tags) + tags = { name: tags[:field], deprecated: tags[:deprecated] } + Yabeda.graphql.mutation_fields_count.increment(tags) + end + + def instrument_query_execution(tags) + tags = { name: tags[:field], deprecated: tags[:deprecated] } + Yabeda.graphql.query_fields_count.increment(tags) + end + + def cache(query) + query.context.namespace(Yabeda::GraphQL)[:field_call_cache] + end + + def platform_field_key(type, field) + "#{type.graphql_name}.#{field.graphql_name}" + end + + # We don't use these yet, but graphql-ruby require us to declare them + + def platform_authorized_key(type) + "#{type.graphql_name}.authorized" + end + + def platform_resolve_type_key(type) + "#{type.graphql_name}.resolve_type" + end + end + end + end +end diff --git a/lib/yabeda/graphql/yabeda_tracing.rb b/lib/yabeda/graphql/yabeda_tracing.rb index 9aef0b3..dd91eed 100644 --- a/lib/yabeda/graphql/yabeda_tracing.rb +++ b/lib/yabeda/graphql/yabeda_tracing.rb @@ -1,57 +1,33 @@ -require "graphql/tracing/platform_tracing" - module Yabeda module GraphQL - class YabedaTracing < ::GraphQL::Tracing::PlatformTracing - - self.platform_keys = { - 'lex' => "graphql.lex", - 'parse' => "graphql.parse", - 'validate' => "graphql.validate", - 'analyze_query' => "graphql.analyze", - 'analyze_multiplex' => "graphql.analyze", - 'execute_multiplex' => "graphql.execute", - 'execute_query' => "graphql.execute", - 'execute_query_lazy' => "graphql.execute", - 'execute_field' => "graphql.execute", - 'execute_field_lazy' => "graphql.execute" - } - - def platform_trace(platform_key, key, data, &block) + module YabedaTracing + def execute_field(field:, query:, ast_node:, arguments:, object:, &block) start = ::Process.clock_gettime ::Process::CLOCK_MONOTONIC result = block.call duration = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start - case key - when "execute_field", "execute_field_lazy" - field, path, query = extract_field_trace_data(data) + tags = extract_field_tags(field) + path = query.context.current_path - tags = extract_field_tags(field) - if path.length == 1 - return result if key == "execute_field" && query.schema.lazy?(result) + if path.length == 1 + return result if query.schema.lazy?(result) - if query.query? - instrument_query_execution(tags) - elsif query.mutation? - instrument_mutation_execution(tags) - elsif query.subscription? - # Not implemented yet - end - else - instrument_field_execution(query, path, tags, duration) + if query.query? + instrument_query_execution(tags) + elsif query.mutation? + instrument_mutation_execution(tags) + elsif query.subscription? + # Not implemented yet end + else + instrument_field_execution(query, path, tags, duration) end result end - # See https://graphql-ruby.org/api-doc/1.10.5/GraphQL/Tracing - def extract_field_trace_data(data) - if data[:context] # Legacy non-interpreter mode - [data[:context].field, data[:context].path, data[:context].query] - else # Interpreter mode - data.values_at(:field, :path, :query) - end + def execute_field_lazy(field:, query:, ast_node:, arguments:, object:, &block) + execute_field(field: field, query: query, ast_node: ast_node, arguments: arguments, object: object, &block) end def extract_field_tags(field) @@ -82,19 +58,6 @@ def cache(query) query.context.namespace(Yabeda::GraphQL)[:field_call_cache] end - def platform_field_key(type, field) - "#{type.graphql_name}.#{field.graphql_name}" - end - - # We don't use these yet, but graphql-ruby require us to declare them - - def platform_authorized_key(type) - "#{type.graphql_name}.authorized" - end - - def platform_resolve_type_key(type) - "#{type.graphql_name}.resolve_type" - end end end end diff --git a/spec/support/graphql_schema.rb b/spec/support/graphql_schema.rb index fbac1d4..96bf930 100644 --- a/spec/support/graphql_schema.rb +++ b/spec/support/graphql_schema.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "graphql" require "graphql/batch" +require "ostruct" class PriceLoader < GraphQL::Batch::Loader def perform(ids)