graphql-metrics 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
 - data/.github/workflows/ruby.yml +20 -0
 - data/.rubocop-http---shopify-github-io-ruby-style-guide-rubocop-yml +1027 -0
 - data/.rubocop.yml +5 -0
 - data/.ruby-version +1 -0
 - data/CHANGELOG.md +39 -0
 - data/Gemfile.lock +23 -20
 - data/README.md +179 -107
 - data/Rakefile +6 -0
 - data/bin/console +4 -6
 - data/graphql_metrics.gemspec +7 -7
 - data/lib/graphql/metrics.rb +80 -0
 - data/lib/graphql/metrics/analyzer.rb +129 -0
 - data/lib/graphql/metrics/instrumentation.rb +54 -0
 - data/lib/graphql/metrics/tracer.rb +109 -0
 - data/lib/graphql/metrics/version.rb +7 -0
 - metadata +55 -24
 - data/.travis.yml +0 -5
 - data/lib/graphql_metrics.rb +0 -6
 - data/lib/graphql_metrics/extractor.rb +0 -277
 - data/lib/graphql_metrics/instrumentation.rb +0 -107
 - data/lib/graphql_metrics/timed_batch_executor.rb +0 -80
 - data/lib/graphql_metrics/version.rb +0 -5
 
    
        data/Rakefile
    CHANGED
    
    | 
         @@ -5,6 +5,12 @@ Rake::TestTask.new(:test) do |t| 
     | 
|
| 
       5 
5 
     | 
    
         
             
              t.libs << "test"
         
     | 
| 
       6 
6 
     | 
    
         
             
              t.libs << "lib"
         
     | 
| 
       7 
7 
     | 
    
         
             
              t.test_files = FileList["test/**/*_test.rb"]
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
              # TODO: We should remove this line. See `puts` line below.
         
     | 
| 
      
 10 
     | 
    
         
            +
              t.warning = false
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
              puts "Reminder: Remove `t.warning = false` in Rakefile once graphql-ruby fixes all instances of"\
         
     | 
| 
      
 13 
     | 
    
         
            +
                "`warning: instance variable @<ivar> not initialized` and `mismatched indentations`"
         
     | 
| 
       8 
14 
     | 
    
         
             
            end
         
     | 
| 
       9 
15 
     | 
    
         | 
| 
       10 
16 
     | 
    
         
             
            task :default => :test
         
     | 
    
        data/bin/console
    CHANGED
    
    | 
         @@ -1,14 +1,12 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            #!/usr/bin/env ruby
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            require "bundler/setup"
         
     | 
| 
       4 
     | 
    
         
            -
            require  
     | 
| 
      
 4 
     | 
    
         
            +
            require 'graphql'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require "graphql/metrics"
         
     | 
| 
       5 
6 
     | 
    
         | 
| 
       6 
7 
     | 
    
         
             
            # You can add fixtures and/or initialization code here to make experimenting
         
     | 
| 
       7 
8 
     | 
    
         
             
            # with your gem easier. You can also use a different console, if you like.
         
     | 
| 
       8 
9 
     | 
    
         | 
| 
       9 
10 
     | 
    
         
             
            # (If you use this, don't forget to add pry to your Gemfile!)
         
     | 
| 
       10 
     | 
    
         
            -
             
     | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
       13 
     | 
    
         
            -
            require "irb"
         
     | 
| 
       14 
     | 
    
         
            -
            IRB.start(__FILE__)
         
     | 
| 
      
 11 
     | 
    
         
            +
            require "pry"
         
     | 
| 
      
 12 
     | 
    
         
            +
            Pry.start
         
     | 
    
        data/graphql_metrics.gemspec
    CHANGED
    
    | 
         @@ -1,11 +1,8 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
             
     | 
| 
       2 
     | 
    
         
            -
            lib = File.expand_path("../lib", __FILE__)
         
     | 
| 
       3 
     | 
    
         
            -
            $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
         
     | 
| 
       4 
     | 
    
         
            -
            require "graphql_metrics/version"
         
     | 
| 
      
 1 
     | 
    
         
            +
            require_relative "lib/graphql/metrics/version"
         
     | 
| 
       5 
2 
     | 
    
         | 
| 
       6 
3 
     | 
    
         
             
            Gem::Specification.new do |spec|
         
     | 
| 
       7 
4 
     | 
    
         
             
              spec.name          = "graphql-metrics"
         
     | 
| 
       8 
     | 
    
         
            -
              spec.version       =  
     | 
| 
      
 5 
     | 
    
         
            +
              spec.version       = GraphQL::Metrics::VERSION
         
     | 
| 
       9 
6 
     | 
    
         
             
              spec.authors       = ["Christopher Butcher"]
         
     | 
| 
       10 
7 
     | 
    
         
             
              spec.email         = ["gems@shopify.com"]
         
     | 
| 
       11 
8 
     | 
    
         | 
| 
         @@ -33,16 +30,19 @@ Gem::Specification.new do |spec| 
     | 
|
| 
       33 
30 
     | 
    
         
             
              spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
         
     | 
| 
       34 
31 
     | 
    
         
             
              spec.require_paths = ["lib"]
         
     | 
| 
       35 
32 
     | 
    
         | 
| 
       36 
     | 
    
         
            -
              spec. 
     | 
| 
      
 33 
     | 
    
         
            +
              spec.add_runtime_dependency "concurrent-ruby", "~> 1.1.0"
         
     | 
| 
      
 34 
     | 
    
         
            +
              spec.add_runtime_dependency "graphql", "~> 1.9.15"
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
              spec.add_development_dependency "bundler", "~> 2.0.2"
         
     | 
| 
       37 
37 
     | 
    
         
             
              spec.add_development_dependency "rake", "~> 10.0"
         
     | 
| 
       38 
38 
     | 
    
         
             
              spec.add_development_dependency "minitest", "~> 5.0"
         
     | 
| 
       39 
39 
     | 
    
         
             
              spec.add_development_dependency 'graphql-batch'
         
     | 
| 
       40 
     | 
    
         
            -
              spec.add_development_dependency "graphql", "~> 1.8.2"
         
     | 
| 
       41 
40 
     | 
    
         
             
              spec.add_development_dependency "activesupport", "~> 5.1.5"
         
     | 
| 
       42 
41 
     | 
    
         
             
              spec.add_development_dependency "pry"
         
     | 
| 
       43 
42 
     | 
    
         
             
              spec.add_development_dependency "pry-byebug"
         
     | 
| 
       44 
43 
     | 
    
         
             
              spec.add_development_dependency "mocha"
         
     | 
| 
       45 
44 
     | 
    
         
             
              spec.add_development_dependency "diffy"
         
     | 
| 
      
 45 
     | 
    
         
            +
              spec.add_development_dependency "hashdiff"
         
     | 
| 
       46 
46 
     | 
    
         
             
              spec.add_development_dependency "fakeredis"
         
     | 
| 
       47 
47 
     | 
    
         
             
              spec.add_development_dependency "minitest-focus"
         
     | 
| 
       48 
48 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,80 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "concurrent"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "graphql/metrics/version"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require "graphql/metrics/instrumentation"
         
     | 
| 
      
 6 
     | 
    
         
            +
            require "graphql/metrics/tracer"
         
     | 
| 
      
 7 
     | 
    
         
            +
            require "graphql/metrics/analyzer"
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            module GraphQL
         
     | 
| 
      
 10 
     | 
    
         
            +
              module Metrics
         
     | 
| 
      
 11 
     | 
    
         
            +
                # The context namespace for all values stored by this gem.
         
     | 
| 
      
 12 
     | 
    
         
            +
                CONTEXT_NAMESPACE = :graphql_metrics_analysis
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                # Skip metrics capture altogher, by setting `skip_graphql_metrics_analysis: true` in query context.
         
     | 
| 
      
 15 
     | 
    
         
            +
                SKIP_GRAPHQL_METRICS_ANALYSIS = :skip_graphql_metrics_analysis
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                # Skips just field and argument logging, when query metrics logging is still desirable
         
     | 
| 
      
 18 
     | 
    
         
            +
                SKIP_FIELD_AND_ARGUMENT_METRICS = :skip_field_and_argument_metrics
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                # Timings related constants.
         
     | 
| 
      
 21 
     | 
    
         
            +
                TIMINGS_CAPTURE_ENABLED = :timings_capture_enabled
         
     | 
| 
      
 22 
     | 
    
         
            +
                ANALYZER_INSTANCE_KEY = :analyzer_instance
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                # Context keys to store timings for query phases of execution, field resolver timings.
         
     | 
| 
      
 25 
     | 
    
         
            +
                QUERY_START_TIME = :query_start_time
         
     | 
| 
      
 26 
     | 
    
         
            +
                QUERY_START_TIME_MONOTONIC = :query_start_time_monotonic
         
     | 
| 
      
 27 
     | 
    
         
            +
                PARSING_START_TIME_OFFSET = :parsing_start_time_offset
         
     | 
| 
      
 28 
     | 
    
         
            +
                PARSING_DURATION = :parsing_duration
         
     | 
| 
      
 29 
     | 
    
         
            +
                VALIDATION_START_TIME_OFFSET = :validation_start_time_offset
         
     | 
| 
      
 30 
     | 
    
         
            +
                VALIDATION_DURATION = :validation_duration
         
     | 
| 
      
 31 
     | 
    
         
            +
                INLINE_FIELD_TIMINGS = :inline_field_timings
         
     | 
| 
      
 32 
     | 
    
         
            +
                LAZY_FIELD_TIMINGS = :lazy_field_timings
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                def self.timings_capture_enabled?(context)
         
     | 
| 
      
 35 
     | 
    
         
            +
                  return false unless context
         
     | 
| 
      
 36 
     | 
    
         
            +
                  !!context.namespace(CONTEXT_NAMESPACE)[TIMINGS_CAPTURE_ENABLED]
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                def self.current_time
         
     | 
| 
      
 40 
     | 
    
         
            +
                  Process.clock_gettime(Process::CLOCK_REALTIME)
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                def self.current_time_monotonic
         
     | 
| 
      
 44 
     | 
    
         
            +
                  Process.clock_gettime(Process::CLOCK_MONOTONIC)
         
     | 
| 
      
 45 
     | 
    
         
            +
                end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                def self.time(*args)
         
     | 
| 
      
 48 
     | 
    
         
            +
                  TimedResult.new(*args) { yield }
         
     | 
| 
      
 49 
     | 
    
         
            +
                end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                class TimedResult
         
     | 
| 
      
 52 
     | 
    
         
            +
                  # NOTE: `time_since_offset` is used to produce start times timed phases of execution (validation, field
         
     | 
| 
      
 53 
     | 
    
         
            +
                  # resolution). These start times are relative to the executed operation's start time, which is captured at the
         
     | 
| 
      
 54 
     | 
    
         
            +
                  # outset of document parsing.
         
     | 
| 
      
 55 
     | 
    
         
            +
                  #
         
     | 
| 
      
 56 
     | 
    
         
            +
                  # The times produced are intentionally similar to:
         
     | 
| 
      
 57 
     | 
    
         
            +
                  # https://github.com/apollographql/apollo-tracing#response-format
         
     | 
| 
      
 58 
     | 
    
         
            +
                  #
         
     | 
| 
      
 59 
     | 
    
         
            +
                  # Taking a field resolver start offset example:
         
     | 
| 
      
 60 
     | 
    
         
            +
                  #
         
     | 
| 
      
 61 
     | 
    
         
            +
                  # <   start offset   >
         
     | 
| 
      
 62 
     | 
    
         
            +
                  # |------------------|----------|--------->
         
     | 
| 
      
 63 
     | 
    
         
            +
                  # OS (t=0)           FS (t=1)   FE (t=2)
         
     | 
| 
      
 64 
     | 
    
         
            +
                  #
         
     | 
| 
      
 65 
     | 
    
         
            +
                  # OS = Operation start time
         
     | 
| 
      
 66 
     | 
    
         
            +
                  # FS = Field resolver start time
         
     | 
| 
      
 67 
     | 
    
         
            +
                  # FE = Field resolver end time
         
     | 
| 
      
 68 
     | 
    
         
            +
                  #
         
     | 
| 
      
 69 
     | 
    
         
            +
                  attr_reader :result, :start_time, :duration, :time_since_offset
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                  def initialize(offset_time = nil)
         
     | 
| 
      
 72 
     | 
    
         
            +
                    @offset_time = offset_time
         
     | 
| 
      
 73 
     | 
    
         
            +
                    @start_time = GraphQL::Metrics.current_time_monotonic
         
     | 
| 
      
 74 
     | 
    
         
            +
                    @result = yield
         
     | 
| 
      
 75 
     | 
    
         
            +
                    @duration = GraphQL::Metrics.current_time_monotonic - @start_time
         
     | 
| 
      
 76 
     | 
    
         
            +
                    @time_since_offset = @start_time - @offset_time if @offset_time
         
     | 
| 
      
 77 
     | 
    
         
            +
                  end
         
     | 
| 
      
 78 
     | 
    
         
            +
                end
         
     | 
| 
      
 79 
     | 
    
         
            +
              end
         
     | 
| 
      
 80 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,129 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module GraphQL
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Metrics
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Analyzer < GraphQL::Analysis::AST::Analyzer
         
     | 
| 
      
 6 
     | 
    
         
            +
                  attr_reader :query
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  def initialize(query_or_multiplex)
         
     | 
| 
      
 9 
     | 
    
         
            +
                    super
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                    @query = query_or_multiplex
         
     | 
| 
      
 12 
     | 
    
         
            +
                    ns = query.context.namespace(CONTEXT_NAMESPACE)
         
     | 
| 
      
 13 
     | 
    
         
            +
                    ns[ANALYZER_INSTANCE_KEY] = self
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                    @static_query_metrics = nil
         
     | 
| 
      
 16 
     | 
    
         
            +
                    @static_field_metrics = []
         
     | 
| 
      
 17 
     | 
    
         
            +
                  end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  def analyze?
         
     | 
| 
      
 20 
     | 
    
         
            +
                    query.valid? && !query.context[GraphQL::Metrics::SKIP_GRAPHQL_METRICS_ANALYSIS]
         
     | 
| 
      
 21 
     | 
    
         
            +
                  end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                  def extract_query(runtime_query_metrics: {})
         
     | 
| 
      
 24 
     | 
    
         
            +
                    query_extracted(@static_query_metrics.merge(runtime_query_metrics)) if @static_query_metrics
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                  def on_enter_operation_definition(_node, _parent, visitor)
         
     | 
| 
      
 28 
     | 
    
         
            +
                    @static_query_metrics = {
         
     | 
| 
      
 29 
     | 
    
         
            +
                      operation_type: visitor.query.selected_operation.operation_type,
         
     | 
| 
      
 30 
     | 
    
         
            +
                      operation_name: visitor.query.selected_operation.name,
         
     | 
| 
      
 31 
     | 
    
         
            +
                    }
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  def on_leave_field(node, _parent, visitor)
         
     | 
| 
      
 35 
     | 
    
         
            +
                    return if visitor.field_definition.introspection?
         
     | 
| 
      
 36 
     | 
    
         
            +
                    return if query.context[SKIP_FIELD_AND_ARGUMENT_METRICS]
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                    # NOTE: @rmosolgo "I think it could be reduced to `arguments = visitor.arguments_for(ast_node)`"
         
     | 
| 
      
 39 
     | 
    
         
            +
                    arguments = visitor.arguments_for(node, visitor.field_definition)
         
     | 
| 
      
 40 
     | 
    
         
            +
                    extract_arguments(arguments.argument_values.values, visitor.field_definition)
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                    static_metrics = {
         
     | 
| 
      
 43 
     | 
    
         
            +
                      field_name: node.name,
         
     | 
| 
      
 44 
     | 
    
         
            +
                      return_type_name: visitor.type_definition.name,
         
     | 
| 
      
 45 
     | 
    
         
            +
                      parent_type_name: visitor.parent_type_definition.name,
         
     | 
| 
      
 46 
     | 
    
         
            +
                      deprecated: visitor.field_definition.deprecation_reason.present?,
         
     | 
| 
      
 47 
     | 
    
         
            +
                      path: visitor.response_path,
         
     | 
| 
      
 48 
     | 
    
         
            +
                    }
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                    if GraphQL::Metrics.timings_capture_enabled?(query.context)
         
     | 
| 
      
 51 
     | 
    
         
            +
                      @static_field_metrics << static_metrics
         
     | 
| 
      
 52 
     | 
    
         
            +
                    else
         
     | 
| 
      
 53 
     | 
    
         
            +
                      field_extracted(static_metrics)
         
     | 
| 
      
 54 
     | 
    
         
            +
                    end
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                  def extract_fields_with_runtime_metrics
         
     | 
| 
      
 58 
     | 
    
         
            +
                    return if query.context[SKIP_FIELD_AND_ARGUMENT_METRICS]
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                    ns = query.context.namespace(CONTEXT_NAMESPACE)
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                    @static_field_metrics.each do |static_metrics|
         
     | 
| 
      
 63 
     | 
    
         
            +
                      resolver_timings = ns[GraphQL::Metrics::INLINE_FIELD_TIMINGS][static_metrics[:path]]
         
     | 
| 
      
 64 
     | 
    
         
            +
                      lazy_field_timings = ns[GraphQL::Metrics::LAZY_FIELD_TIMINGS][static_metrics[:path]]
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                      metrics = static_metrics.merge(
         
     | 
| 
      
 67 
     | 
    
         
            +
                        resolver_timings: resolver_timings || [],
         
     | 
| 
      
 68 
     | 
    
         
            +
                        lazy_resolver_timings: lazy_field_timings || [],
         
     | 
| 
      
 69 
     | 
    
         
            +
                      )
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                      field_extracted(metrics)
         
     | 
| 
      
 72 
     | 
    
         
            +
                    end
         
     | 
| 
      
 73 
     | 
    
         
            +
                  end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                  def result
         
     | 
| 
      
 76 
     | 
    
         
            +
                    return if GraphQL::Metrics.timings_capture_enabled?(query.context)
         
     | 
| 
      
 77 
     | 
    
         
            +
                    return if query.context[GraphQL::Metrics::SKIP_GRAPHQL_METRICS_ANALYSIS]
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                    # NOTE: If we're running as a static analyzer (i.e. not with instrumentation and tracing), we still need to
         
     | 
| 
      
 80 
     | 
    
         
            +
                    # flush static query metrics somewhere other than `after_query`.
         
     | 
| 
      
 81 
     | 
    
         
            +
                    ns = query.context.namespace(CONTEXT_NAMESPACE)
         
     | 
| 
      
 82 
     | 
    
         
            +
                    analyzer = ns[GraphQL::Metrics::ANALYZER_INSTANCE_KEY]
         
     | 
| 
      
 83 
     | 
    
         
            +
                    analyzer.extract_query
         
     | 
| 
      
 84 
     | 
    
         
            +
                  end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                  private
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                  def extract_arguments(argument, field_defn, parent_input_object = nil)
         
     | 
| 
      
 89 
     | 
    
         
            +
                    case argument
         
     | 
| 
      
 90 
     | 
    
         
            +
                    when Array
         
     | 
| 
      
 91 
     | 
    
         
            +
                      argument.each do |a|
         
     | 
| 
      
 92 
     | 
    
         
            +
                        extract_arguments(a, field_defn, parent_input_object)
         
     | 
| 
      
 93 
     | 
    
         
            +
                      end
         
     | 
| 
      
 94 
     | 
    
         
            +
                    when Hash
         
     | 
| 
      
 95 
     | 
    
         
            +
                      argument.each_value do |a|
         
     | 
| 
      
 96 
     | 
    
         
            +
                        extract_arguments(a, field_defn, parent_input_object)
         
     | 
| 
      
 97 
     | 
    
         
            +
                      end
         
     | 
| 
      
 98 
     | 
    
         
            +
                    when ::GraphQL::Query::Arguments
         
     | 
| 
      
 99 
     | 
    
         
            +
                      argument.each_value do |arg_val|
         
     | 
| 
      
 100 
     | 
    
         
            +
                        extract_arguments(arg_val, field_defn, parent_input_object)
         
     | 
| 
      
 101 
     | 
    
         
            +
                      end
         
     | 
| 
      
 102 
     | 
    
         
            +
                    when ::GraphQL::Query::Arguments::ArgumentValue
         
     | 
| 
      
 103 
     | 
    
         
            +
                      extract_argument(argument, field_defn, parent_input_object)
         
     | 
| 
      
 104 
     | 
    
         
            +
                      extract_arguments(argument.value, field_defn, parent_input_object)
         
     | 
| 
      
 105 
     | 
    
         
            +
                    when ::GraphQL::Schema::InputObject
         
     | 
| 
      
 106 
     | 
    
         
            +
                      input_object_argument_values = argument.arguments.argument_values.values
         
     | 
| 
      
 107 
     | 
    
         
            +
                      parent_input_object = input_object_argument_values.first&.definition&.metadata&.fetch(:type_class, nil)&.owner
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                      extract_arguments(input_object_argument_values, field_defn, parent_input_object)
         
     | 
| 
      
 110 
     | 
    
         
            +
                    end
         
     | 
| 
      
 111 
     | 
    
         
            +
                  end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                  def extract_argument(value, field_defn, parent_input_object = nil)
         
     | 
| 
      
 114 
     | 
    
         
            +
                    static_metrics = {
         
     | 
| 
      
 115 
     | 
    
         
            +
                      argument_name: value.definition.expose_as,
         
     | 
| 
      
 116 
     | 
    
         
            +
                      argument_type_name: value.definition.type.unwrap.to_s,
         
     | 
| 
      
 117 
     | 
    
         
            +
                      parent_field_name: field_defn.name,
         
     | 
| 
      
 118 
     | 
    
         
            +
                      parent_field_type_name: field_defn.metadata[:type_class].owner.graphql_name,
         
     | 
| 
      
 119 
     | 
    
         
            +
                      parent_input_object_type: parent_input_object&.graphql_name,
         
     | 
| 
      
 120 
     | 
    
         
            +
                      default_used: value.default_used?,
         
     | 
| 
      
 121 
     | 
    
         
            +
                      value_is_null: value.value.nil?,
         
     | 
| 
      
 122 
     | 
    
         
            +
                      value: value,
         
     | 
| 
      
 123 
     | 
    
         
            +
                    }
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
                    argument_extracted(static_metrics)
         
     | 
| 
      
 126 
     | 
    
         
            +
                  end
         
     | 
| 
      
 127 
     | 
    
         
            +
                end
         
     | 
| 
      
 128 
     | 
    
         
            +
              end
         
     | 
| 
      
 129 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,54 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module GraphQL
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Metrics
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Instrumentation
         
     | 
| 
      
 6 
     | 
    
         
            +
                  def before_query(query)
         
     | 
| 
      
 7 
     | 
    
         
            +
                    unless query_present_and_valid?(query)
         
     | 
| 
      
 8 
     | 
    
         
            +
                      # Setting this prevents Analyzer and Tracer from trying to gather runtime metrics for invalid queries.
         
     | 
| 
      
 9 
     | 
    
         
            +
                      query.context[GraphQL::Metrics::SKIP_GRAPHQL_METRICS_ANALYSIS] = true
         
     | 
| 
      
 10 
     | 
    
         
            +
                    end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                    # Even if queries are present and valid, applications may set this context value in order to opt out of
         
     | 
| 
      
 13 
     | 
    
         
            +
                    # having Analyzer and Tracer gather runtime metrics.
         
     | 
| 
      
 14 
     | 
    
         
            +
                    # If we're skipping runtime metrics, then both Instrumentation before_ and after_query can and should be
         
     | 
| 
      
 15 
     | 
    
         
            +
                    # short-circuited as well.
         
     | 
| 
      
 16 
     | 
    
         
            +
                    return if query.context[GraphQL::Metrics::SKIP_GRAPHQL_METRICS_ANALYSIS]
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                    ns = query.context.namespace(CONTEXT_NAMESPACE)
         
     | 
| 
      
 19 
     | 
    
         
            +
                    ns[GraphQL::Metrics::TIMINGS_CAPTURE_ENABLED] = true
         
     | 
| 
      
 20 
     | 
    
         
            +
                    ns[GraphQL::Metrics::INLINE_FIELD_TIMINGS] = {}
         
     | 
| 
      
 21 
     | 
    
         
            +
                    ns[GraphQL::Metrics::LAZY_FIELD_TIMINGS] = {}
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  def after_query(query)
         
     | 
| 
      
 25 
     | 
    
         
            +
                    return if query.context[GraphQL::Metrics::SKIP_GRAPHQL_METRICS_ANALYSIS]
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                    ns = query.context.namespace(CONTEXT_NAMESPACE)
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                    # NOTE: The start time stored at `ns[GraphQL::Metrics::QUERY_START_TIME_MONOTONIC]` is captured during query
         
     | 
| 
      
 30 
     | 
    
         
            +
                    # parsing, which occurs before `Instrumentation#before_query`.
         
     | 
| 
      
 31 
     | 
    
         
            +
                    query_duration = GraphQL::Metrics.current_time_monotonic - ns[GraphQL::Metrics::QUERY_START_TIME_MONOTONIC]
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                    runtime_query_metrics = {
         
     | 
| 
      
 34 
     | 
    
         
            +
                      query_start_time: ns[GraphQL::Metrics::QUERY_START_TIME],
         
     | 
| 
      
 35 
     | 
    
         
            +
                      query_duration: query_duration,
         
     | 
| 
      
 36 
     | 
    
         
            +
                      parsing_start_time_offset: ns[GraphQL::Metrics::PARSING_START_TIME_OFFSET],
         
     | 
| 
      
 37 
     | 
    
         
            +
                      parsing_duration: ns[GraphQL::Metrics::PARSING_DURATION],
         
     | 
| 
      
 38 
     | 
    
         
            +
                      validation_start_time_offset: ns[GraphQL::Metrics::VALIDATION_START_TIME_OFFSET],
         
     | 
| 
      
 39 
     | 
    
         
            +
                      validation_duration: ns[GraphQL::Metrics::VALIDATION_DURATION],
         
     | 
| 
      
 40 
     | 
    
         
            +
                    }
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                    analyzer = ns[GraphQL::Metrics::ANALYZER_INSTANCE_KEY]
         
     | 
| 
      
 43 
     | 
    
         
            +
                    analyzer.extract_fields_with_runtime_metrics
         
     | 
| 
      
 44 
     | 
    
         
            +
                    analyzer.extract_query(runtime_query_metrics: runtime_query_metrics)
         
     | 
| 
      
 45 
     | 
    
         
            +
                  end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                  private
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                  def query_present_and_valid?(query)
         
     | 
| 
      
 50 
     | 
    
         
            +
                    query.valid? && query.document.to_query_string.present?
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
              end
         
     | 
| 
      
 54 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,109 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module GraphQL
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Metrics
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Tracer
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # NOTE: These constants come from the graphql ruby gem.
         
     | 
| 
      
 7 
     | 
    
         
            +
                  GRAPHQL_GEM_LEXING_KEY = 'lex'
         
     | 
| 
      
 8 
     | 
    
         
            +
                  GRAPHQL_GEM_PARSING_KEY = 'parse'
         
     | 
| 
      
 9 
     | 
    
         
            +
                  GRAPHQL_GEM_VALIDATION_KEYS = ['validate', 'analyze_query']
         
     | 
| 
      
 10 
     | 
    
         
            +
                  GRAPHQL_GEM_TRACING_FIELD_KEYS = [
         
     | 
| 
      
 11 
     | 
    
         
            +
                    GRAPHQL_GEM_TRACING_FIELD_KEY = 'execute_field',
         
     | 
| 
      
 12 
     | 
    
         
            +
                    GRAPHQL_GEM_TRACING_LAZY_FIELD_KEY = 'execute_field_lazy'
         
     | 
| 
      
 13 
     | 
    
         
            +
                  ]
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  def trace(key, data, &block)
         
     | 
| 
      
 16 
     | 
    
         
            +
                    # NOTE: Context doesn't exist yet during lexing, parsing.
         
     | 
| 
      
 17 
     | 
    
         
            +
                    possible_context = data[:query]&.context
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                    skip_tracing = possible_context&.fetch(GraphQL::Metrics::SKIP_GRAPHQL_METRICS_ANALYSIS, false)
         
     | 
| 
      
 20 
     | 
    
         
            +
                    return yield if skip_tracing
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    # NOTE: Not all tracing events are handled here, but those that are are handled in this case statement in
         
     | 
| 
      
 23 
     | 
    
         
            +
                    # chronological order.
         
     | 
| 
      
 24 
     | 
    
         
            +
                    case key
         
     | 
| 
      
 25 
     | 
    
         
            +
                    when GRAPHQL_GEM_LEXING_KEY
         
     | 
| 
      
 26 
     | 
    
         
            +
                      return setup_tracing_before_lexing(&block)
         
     | 
| 
      
 27 
     | 
    
         
            +
                    when GRAPHQL_GEM_PARSING_KEY
         
     | 
| 
      
 28 
     | 
    
         
            +
                      return capture_parsing_time(&block)
         
     | 
| 
      
 29 
     | 
    
         
            +
                    when *GRAPHQL_GEM_VALIDATION_KEYS
         
     | 
| 
      
 30 
     | 
    
         
            +
                      context = possible_context
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                      return yield unless context.query.valid?
         
     | 
| 
      
 33 
     | 
    
         
            +
                      return capture_validation_time(context, &block)
         
     | 
| 
      
 34 
     | 
    
         
            +
                    when *GRAPHQL_GEM_TRACING_FIELD_KEYS
         
     | 
| 
      
 35 
     | 
    
         
            +
                      return yield if data[:query].context[SKIP_FIELD_AND_ARGUMENT_METRICS]
         
     | 
| 
      
 36 
     | 
    
         
            +
                      return yield unless GraphQL::Metrics.timings_capture_enabled?(data[:query].context)
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                      pre_context = nil
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                      context_key = case key
         
     | 
| 
      
 41 
     | 
    
         
            +
                      when GRAPHQL_GEM_TRACING_FIELD_KEY
         
     | 
| 
      
 42 
     | 
    
         
            +
                        GraphQL::Metrics::INLINE_FIELD_TIMINGS
         
     | 
| 
      
 43 
     | 
    
         
            +
                      when GRAPHQL_GEM_TRACING_LAZY_FIELD_KEY
         
     | 
| 
      
 44 
     | 
    
         
            +
                        GraphQL::Metrics::LAZY_FIELD_TIMINGS
         
     | 
| 
      
 45 
     | 
    
         
            +
                      end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                      trace_field(context_key, data, &block)
         
     | 
| 
      
 48 
     | 
    
         
            +
                    else
         
     | 
| 
      
 49 
     | 
    
         
            +
                      return yield
         
     | 
| 
      
 50 
     | 
    
         
            +
                    end
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                  private
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                  def pre_context
         
     | 
| 
      
 56 
     | 
    
         
            +
                    # NOTE: This is used to store timings from lexing, parsing, validation, before we have a context to store
         
     | 
| 
      
 57 
     | 
    
         
            +
                    # values in. Uses thread-safe Concurrent::ThreadLocalVar to store a set of values per thread.
         
     | 
| 
      
 58 
     | 
    
         
            +
                    @pre_context ||= Concurrent::ThreadLocalVar.new(OpenStruct.new)
         
     | 
| 
      
 59 
     | 
    
         
            +
                  end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                  def setup_tracing_before_lexing
         
     | 
| 
      
 62 
     | 
    
         
            +
                    pre_context.value.query_start_time = GraphQL::Metrics.current_time
         
     | 
| 
      
 63 
     | 
    
         
            +
                    pre_context.value.query_start_time_monotonic = GraphQL::Metrics.current_time_monotonic
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                    yield
         
     | 
| 
      
 66 
     | 
    
         
            +
                  end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                  def capture_parsing_time
         
     | 
| 
      
 69 
     | 
    
         
            +
                    timed_result = GraphQL::Metrics.time { yield }
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                    pre_context.value.parsing_start_time_offset = timed_result.start_time
         
     | 
| 
      
 72 
     | 
    
         
            +
                    pre_context.value.parsing_duration = timed_result.duration
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                    timed_result.result
         
     | 
| 
      
 75 
     | 
    
         
            +
                  end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                  def capture_validation_time(context)
         
     | 
| 
      
 78 
     | 
    
         
            +
                    timed_result = GraphQL::Metrics.time(pre_context.value.query_start_time_monotonic) { yield }
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                    ns = context.namespace(CONTEXT_NAMESPACE)
         
     | 
| 
      
 81 
     | 
    
         
            +
                    previous_validation_duration = ns[GraphQL::Metrics::VALIDATION_DURATION] || 0
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                    ns[QUERY_START_TIME] = pre_context.value.query_start_time
         
     | 
| 
      
 84 
     | 
    
         
            +
                    ns[QUERY_START_TIME_MONOTONIC] = pre_context.value.query_start_time_monotonic
         
     | 
| 
      
 85 
     | 
    
         
            +
                    ns[PARSING_START_TIME_OFFSET] = pre_context.value.parsing_start_time_offset
         
     | 
| 
      
 86 
     | 
    
         
            +
                    ns[PARSING_DURATION] = pre_context.value.parsing_duration
         
     | 
| 
      
 87 
     | 
    
         
            +
                    ns[VALIDATION_START_TIME_OFFSET] = timed_result.time_since_offset
         
     | 
| 
      
 88 
     | 
    
         
            +
                    ns[VALIDATION_DURATION] = timed_result.duration + previous_validation_duration
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                    timed_result.result
         
     | 
| 
      
 91 
     | 
    
         
            +
                  end
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                  def trace_field(context_key, data)
         
     | 
| 
      
 94 
     | 
    
         
            +
                    ns = data[:query].context.namespace(CONTEXT_NAMESPACE)
         
     | 
| 
      
 95 
     | 
    
         
            +
                    query_start_time_monotonic = ns[GraphQL::Metrics::QUERY_START_TIME_MONOTONIC]
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                    timed_result = GraphQL::Metrics.time(query_start_time_monotonic) { yield }
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                    path_excluding_numeric_indicies = data[:path].select { |p| p.is_a?(String) }
         
     | 
| 
      
 100 
     | 
    
         
            +
                    ns[context_key][path_excluding_numeric_indicies] ||= []
         
     | 
| 
      
 101 
     | 
    
         
            +
                    ns[context_key][path_excluding_numeric_indicies] << {
         
     | 
| 
      
 102 
     | 
    
         
            +
                      start_time_offset: timed_result.time_since_offset, duration: timed_result.duration
         
     | 
| 
      
 103 
     | 
    
         
            +
                    }
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                    timed_result.result
         
     | 
| 
      
 106 
     | 
    
         
            +
                  end
         
     | 
| 
      
 107 
     | 
    
         
            +
                end
         
     | 
| 
      
 108 
     | 
    
         
            +
              end
         
     | 
| 
      
 109 
     | 
    
         
            +
            end
         
     |