graphql-metrics 2.0.1 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|