graphql-metrics 1.1.5 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 95309c5ed04f9b4c10fe700e9e013ff19d3be644
4
- data.tar.gz: 3318e036297987653dae4fc03f28100537426fac
3
+ metadata.gz: a37ad6af9b19ef274d8f36142f5d73f92c43dc8c
4
+ data.tar.gz: b6a7180811bbef742741324af59dc6b78d25fb6a
5
5
  SHA512:
6
- metadata.gz: 95f17afd63cdd64624abee5c8f7595c65bd7c5b0e034a38e36b2b7d64cbb7f2582b6cee4a83ee5e7b58a30c6d977b8a6c2c5d7e1e025dd95197ef16cd59fc8e6
7
- data.tar.gz: 177ecdbc69be67a2c7c04463689b4878d66860282d2664abd91abac000af38979a9e994a4f4a87c2ecd53e370c36ef5d77aff77470646c0046aba13fec27b786
6
+ metadata.gz: 9a6f311bc6c4235ec3390784f13b2b6396a87970e549d2696001755879d0c196fd45275f01953e36bee7120b6db75abaaa7446e5cd95cf175df182d3f1a98dd8
7
+ data.tar.gz: b483a7642ee22f357a4e116fce3aacfed9e18d9d2f79782ca671e26d284fffc4381f63d599cde5095741e9a0290d0542476763748aec861b3d4907219061248e
data/.gitignore CHANGED
@@ -7,3 +7,5 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
  *.gem
10
+
11
+ /vendor/
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql-metrics (1.1.5)
4
+ graphql-metrics (2.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -26,6 +26,8 @@ GEM
26
26
  metaclass (0.0.4)
27
27
  method_source (0.9.0)
28
28
  minitest (5.11.3)
29
+ minitest-focus (1.1.2)
30
+ minitest (>= 4, < 6)
29
31
  mocha (1.5.0)
30
32
  metaclass (~> 0.0.1)
31
33
  promise.rb (0.7.4)
@@ -53,6 +55,7 @@ DEPENDENCIES
53
55
  graphql-batch
54
56
  graphql-metrics!
55
57
  minitest (~> 5.0)
58
+ minitest-focus
56
59
  mocha
57
60
  pry
58
61
  pry-byebug
data/README.md CHANGED
@@ -53,7 +53,7 @@ implementing the methods below, as needed.
53
53
  Here's an example of a simple extractor that logs out all GraphQL query details.
54
54
 
55
55
  ```ruby
56
- class LoggingExtractor < GraphQLMetrics::Extractor
56
+ class LoggingExtractor < GraphQLMetrics::Instrumentation
57
57
  def query_extracted(metrics, _metadata)
58
58
  Rails.logger.debug({
59
59
  query_string: metrics[:query_string], # "query Project { project(name: "GraphQL") { tagline } }"
@@ -103,6 +103,7 @@ class LoggingExtractor < GraphQLMetrics::Extractor
103
103
  default_value_type: metrics[:default_value_type], # "IMPLICIT_NULL"
104
104
  provided_value: metrics[:provided_value], # false
105
105
  default_used: metrics[:default_used], # false
106
+ used_in_operation: metrics[:used_in_operation], # true
106
107
  })
107
108
  end
108
109
 
@@ -133,6 +134,32 @@ class LoggingExtractor < GraphQLMetrics::Extractor
133
134
  end
134
135
  ```
135
136
 
137
+ You can also define ad hoc query Extractors that can work with instances of GraphQL::Query, for example:
138
+
139
+ ```ruby
140
+ class TypeUsageExtractor < GraphQLMetrics::Extractor
141
+ attr_reader :types_used
142
+
143
+ def initialize
144
+ @types_used = Set.new
145
+ end
146
+
147
+ def field_extracted(metrics, _metadata)
148
+ @types_used << metrics[:type_name]
149
+ end
150
+ end
151
+
152
+ # ...
153
+
154
+ extractor = TypeUsageExtractor.new
155
+ extractor.extract!(query)
156
+ puts extractor.types_used
157
+ # => ["Comment", "Post", "QueryRoot"]
158
+ ```
159
+
160
+ Note that resolver-timing related data like `duration` in `query_extracted` and `resolver_times` in `field_extracted`
161
+ won't be available when using an ad hoc Extractor, since the query isn't actually being run; it's only analyzed.
162
+
136
163
  ## Development
137
164
 
138
165
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -44,4 +44,5 @@ Gem::Specification.new do |spec|
44
44
  spec.add_development_dependency "mocha"
45
45
  spec.add_development_dependency "diffy"
46
46
  spec.add_development_dependency "fakeredis"
47
+ spec.add_development_dependency "minitest-focus"
47
48
  end
@@ -2,117 +2,76 @@
2
2
 
3
3
  module GraphQLMetrics
4
4
  class Extractor
5
- CONTEXT_NAMESPACE = :extracted_metrics
6
- TIMING_CACHE_KEY = :timing_cache
7
- START_TIME_KEY = :query_start_time
5
+ class DummyInstrumentor
6
+ def after_query_start_and_end_time
7
+ [nil, nil]
8
+ end
9
+
10
+ def after_query_resolver_times(_ast_node)
11
+ []
12
+ end
13
+
14
+ def ctx_namespace
15
+ {}
16
+ end
17
+ end
8
18
 
9
19
  EXPLICIT_NULL = 'EXPLICIT_NULL'
10
20
  IMPLICIT_NULL = 'IMPLICIT_NULL'
11
21
  NON_NULL = 'NON_NULL'
12
22
 
13
- attr_reader :query, :ctx_namespace
14
-
15
- def self.use(schema_definition)
16
- extractor = self.new
17
- return unless extractor.extractor_defines_any_visitors?
18
-
19
- extractor.setup_instrumentation(schema_definition)
20
- end
21
-
22
- def use(schema_definition)
23
- return unless extractor_defines_any_visitors?
24
- setup_instrumentation(schema_definition)
25
- end
23
+ attr_reader :query
26
24
 
27
- def setup_instrumentation(schema_definition)
28
- schema_definition.instrument(:query, self)
29
- schema_definition.instrument(:field, self)
25
+ def initialize(instrumentor = DummyInstrumentor.new)
26
+ @instrumentor = instrumentor
30
27
  end
31
28
 
32
- def before_query(query)
33
- return unless extractor_defines_any_visitors?
34
-
35
- ns = query.context.namespace(CONTEXT_NAMESPACE)
36
- ns[TIMING_CACHE_KEY] = {}
37
- ns[START_TIME_KEY] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
38
- rescue StandardError => ex
39
- handle_extraction_exception(ex)
29
+ def instrumentor
30
+ @instrumentor ||= DummyInstrumentor.new
40
31
  end
41
32
 
42
- def after_query(query)
43
- return unless extractor_defines_any_visitors?
44
-
33
+ def extract!(query)
45
34
  @query = query
35
+
46
36
  return unless query.valid?
47
- return if respond_to?(:skip_extraction?) && skip_extraction?(query)
48
- return unless @ctx_namespace = query.context.namespace(CONTEXT_NAMESPACE)
49
37
  return unless query.irep_selection
50
38
 
51
- before_query_extracted(query, query.context) if respond_to?(:before_query_extracted)
52
39
  extract_query
53
40
 
41
+ used_variables = extract_used_variables
42
+
54
43
  query.operations.each_value do |operation|
55
- extract_variables(operation)
44
+ extract_variables(operation, used_variables)
56
45
  end
57
46
 
58
47
  extract_node(query.irep_selection)
59
48
  extract_batch_loaders
60
-
61
- after_query_teardown(query) if respond_to?(:after_query_teardown)
62
- rescue StandardError => ex
63
- handle_extraction_exception(ex)
64
- end
65
-
66
- def instrument(type, field)
67
- return field unless respond_to?(:field_extracted)
68
- return field if type.introspection?
69
-
70
- old_resolve_proc = field.resolve_proc
71
- new_resolve_proc = ->(obj, args, ctx) do
72
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
73
- result = old_resolve_proc.call(obj, args, ctx)
74
-
75
- begin
76
- next result if respond_to?(:skip_field_resolution_timing?) &&
77
- skip_field_resolution_timing?(query, ctx)
78
-
79
- end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
80
-
81
- ns = ctx.namespace(CONTEXT_NAMESPACE)
82
-
83
- ns[TIMING_CACHE_KEY][ctx.ast_node] ||= []
84
- ns[TIMING_CACHE_KEY][ctx.ast_node] << end_time - start_time
85
-
86
- result
87
- rescue StandardError => ex
88
- handle_extraction_exception(ex)
89
- result
90
- end
91
- end
92
-
93
- field.redefine { resolve(new_resolve_proc) }
94
49
  end
95
50
 
96
51
  def extractor_defines_any_visitors?
97
- respond_to?(:query_extracted) ||
98
- respond_to?(:field_extracted) ||
99
- respond_to?(:argument_extracted) ||
100
- respond_to?(:variable_extracted) ||
101
- respond_to?(:batch_loaded_field_extracted) ||
102
- respond_to?(:before_query_extracted)
52
+ [self, instrumentor].any? do |extractor_definer|
53
+ extractor_definer.respond_to?(:query_extracted) ||
54
+ extractor_definer.respond_to?(:field_extracted) ||
55
+ extractor_definer.respond_to?(:argument_extracted) ||
56
+ extractor_definer.respond_to?(:variable_extracted) ||
57
+ extractor_definer.respond_to?(:batch_loaded_field_extracted) ||
58
+ extractor_definer.respond_to?(:before_query_extracted)
59
+ end
103
60
  end
104
61
 
105
62
  def handle_extraction_exception(ex)
106
63
  raise ex
107
64
  end
108
65
 
66
+ private
67
+
109
68
  def extract_batch_loaders
110
- return unless respond_to?(:batch_loaded_field_extracted)
69
+ return unless batch_loaded_field_extracted_method = extraction_method(:batch_loaded_field_extracted)
111
70
 
112
71
  TimedBatchExecutor.timings.each do |key, resolve_meta|
113
72
  key, identifiers = TimedBatchExecutor.serialize_loader_key(key)
114
73
 
115
- batch_loaded_field_extracted(
74
+ batch_loaded_field_extracted_method.call(
116
75
  {
117
76
  key: key,
118
77
  identifiers: identifiers,
@@ -131,19 +90,17 @@ module GraphQLMetrics
131
90
  end
132
91
 
133
92
  def extract_query
134
- return unless respond_to?(:query_extracted)
93
+ return unless query_extracted_method = extraction_method(:query_extracted)
135
94
 
136
- start_time = ctx_namespace[START_TIME_KEY]
137
- return unless start_time
95
+ start_time, end_time = instrumentor.after_query_start_and_end_time
96
+ duration = start_time && end_time ? end_time - start_time : nil
138
97
 
139
- end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
140
-
141
- query_extracted(
98
+ query_extracted_method.call(
142
99
  {
143
100
  query_string: query.document.to_query_string,
144
101
  operation_type: query.selected_operation.operation_type,
145
102
  operation_name: query.selected_operation_name,
146
- duration: end_time - start_time
103
+ duration: duration
147
104
  },
148
105
  {
149
106
  query: query,
@@ -156,20 +113,22 @@ module GraphQLMetrics
156
113
  end
157
114
 
158
115
  def extract_field(irep_node)
159
- return unless respond_to?(:field_extracted)
116
+ return unless field_extracted_method = extraction_method(:field_extracted)
160
117
  return unless irep_node.definition
161
118
 
162
- field_extracted(
119
+ resolver_times = instrumentor.after_query_resolver_times(irep_node.ast_node)
120
+
121
+ field_extracted_method.call(
163
122
  {
164
123
  type_name: irep_node.owner_type.name,
165
124
  field_name: irep_node.definition.name,
166
125
  deprecated: irep_node.definition.deprecation_reason.present?,
167
- resolver_times: ctx_namespace.dig(TIMING_CACHE_KEY, irep_node.ast_node) || [],
126
+ resolver_times: resolver_times || [],
168
127
  },
169
128
  {
170
129
  irep_node: irep_node,
171
130
  query: query,
172
- ctx_namespace: ctx_namespace
131
+ ctx_namespace: instrumentor.ctx_namespace
173
132
  }
174
133
  )
175
134
 
@@ -178,9 +137,9 @@ module GraphQLMetrics
178
137
  end
179
138
 
180
139
  def extract_argument(value, irep_node, types)
181
- return unless respond_to?(:argument_extracted)
140
+ return unless argument_extracted_method = extraction_method(:argument_extracted)
182
141
 
183
- argument_extracted(
142
+ argument_extracted_method.call(
184
143
  {
185
144
  name: value.definition.expose_as,
186
145
  type: value.definition.type.unwrap.to_s,
@@ -200,8 +159,8 @@ module GraphQLMetrics
200
159
  handle_extraction_exception(ex)
201
160
  end
202
161
 
203
- def extract_variables(operation)
204
- return unless respond_to?(:variable_extracted)
162
+ def extract_variables(operation, used_variables)
163
+ return unless variable_extracted_method = extraction_method(:variable_extracted)
205
164
 
206
165
  operation.variables.each do |variable|
207
166
  value_provided = query.provided_variables.key?(variable.name)
@@ -217,14 +176,15 @@ module GraphQLMetrics
217
176
 
218
177
  default_used = !value_provided && default_value_type != IMPLICIT_NULL
219
178
 
220
- variable_extracted(
179
+ variable_extracted_method.call(
221
180
  {
222
181
  operation_name: operation.name,
223
182
  unwrapped_type_name: unwrapped_type(variable.type),
224
183
  type: variable.type.to_query_string,
225
184
  default_value_type: default_value_type,
226
185
  provided_value: value_provided,
227
- default_used: default_used
186
+ default_used: default_used,
187
+ used_in_operation: used_variables.include?(variable.name)
228
188
  },
229
189
  {
230
190
  query: query
@@ -235,6 +195,10 @@ module GraphQLMetrics
235
195
  handle_extraction_exception(ex)
236
196
  end
237
197
 
198
+ def extract_used_variables
199
+ query.irep_selection.ast_node.variables.each_with_object(Set.new) { |v, set| set << v.name }
200
+ end
201
+
238
202
  def extract_arguments(irep_node)
239
203
  return unless irep_node.ast_node.is_a?(GraphQL::Language::Nodes::Field)
240
204
 
@@ -292,5 +256,22 @@ module GraphQLMetrics
292
256
  rescue StandardError => ex
293
257
  handle_extraction_exception(ex)
294
258
  end
259
+
260
+ def extraction_method(method_name)
261
+ @extraction_method_cache ||= {}
262
+ return @extraction_method_cache[method_name] if @extraction_method_cache.has_key?(method_name)
263
+
264
+ method = if respond_to?(method_name)
265
+ method(method_name)
266
+ elsif instrumentor && instrumentor.respond_to?(method_name)
267
+ instrumentor.method(method_name)
268
+ else
269
+ nil
270
+ end
271
+
272
+ method.tap do |method|
273
+ @extraction_method_cache[method_name] = method
274
+ end
275
+ end
295
276
  end
296
277
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQLMetrics
4
+ class Instrumentation
5
+ extend Forwardable
6
+
7
+ CONTEXT_NAMESPACE = :extracted_metrics
8
+ TIMING_CACHE_KEY = :timing_cache
9
+ START_TIME_KEY = :query_start_time
10
+
11
+ attr_reader :ctx_namespace, :query
12
+ def_delegators :extractor, :extractor_defines_any_visitors?
13
+
14
+ def self.use(schema_definition)
15
+ instrumentation = self.new
16
+ return unless instrumentation.extractor_defines_any_visitors?
17
+
18
+ instrumentation.setup_instrumentation(schema_definition)
19
+ end
20
+
21
+ def self.current_time
22
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
23
+ end
24
+
25
+ def use(schema_definition)
26
+ return unless extractor_defines_any_visitors?
27
+ extractor.setup_instrumentation(schema_definition)
28
+ end
29
+
30
+ def setup_instrumentation(schema_definition)
31
+ schema_definition.instrument(:query, self)
32
+ schema_definition.instrument(:field, self)
33
+ end
34
+
35
+ def extractor
36
+ @extractor ||= Extractor.new(self)
37
+ end
38
+
39
+ def before_query(query)
40
+ return unless extractor_defines_any_visitors?
41
+
42
+ ns = query.context.namespace(CONTEXT_NAMESPACE)
43
+ ns[TIMING_CACHE_KEY] = {}
44
+ ns[START_TIME_KEY] = self.class.current_time
45
+ rescue StandardError => ex
46
+ extractor.handle_extraction_exception(ex)
47
+ end
48
+
49
+ def after_query(query)
50
+ @query = query
51
+
52
+ return unless extractor_defines_any_visitors?
53
+ return if respond_to?(:skip_extraction?) && skip_extraction?(query)
54
+ return unless @ctx_namespace = query.context.namespace(CONTEXT_NAMESPACE)
55
+
56
+ before_query_extracted(query, query.context) if respond_to?(:before_query_extracted)
57
+
58
+ extractor.extract!(query)
59
+
60
+ after_query_teardown(query) if respond_to?(:after_query_teardown)
61
+ rescue StandardError => ex
62
+ extractor.handle_extraction_exception(ex)
63
+ end
64
+
65
+ def instrument(type, field)
66
+ return field unless respond_to?(:field_extracted) || extractor.respond_to?(:field_extracted)
67
+ return field if type.introspection?
68
+
69
+ old_resolve_proc = field.resolve_proc
70
+ new_resolve_proc = ->(obj, args, ctx) do
71
+ start_time = self.class.current_time
72
+ result = old_resolve_proc.call(obj, args, ctx)
73
+
74
+ begin
75
+ next result if respond_to?(:skip_field_resolution_timing?) &&
76
+ skip_field_resolution_timing?(query, ctx)
77
+
78
+ end_time = self.class.current_time
79
+
80
+ ns = ctx.namespace(CONTEXT_NAMESPACE)
81
+
82
+ ns[TIMING_CACHE_KEY][ctx.ast_node] ||= []
83
+ ns[TIMING_CACHE_KEY][ctx.ast_node] << end_time - start_time
84
+
85
+ result
86
+ rescue StandardError => ex
87
+ extractor.handle_extraction_exception(ex)
88
+ result
89
+ end
90
+ end
91
+
92
+ field.redefine { resolve(new_resolve_proc) }
93
+ end
94
+
95
+ def after_query_resolver_times(ast_node)
96
+ ctx_namespace.dig(Instrumentation::TIMING_CACHE_KEY).fetch(ast_node, [])
97
+ end
98
+
99
+ def after_query_start_and_end_time
100
+ start_time = ctx_namespace[Instrumentation::START_TIME_KEY]
101
+ return unless start_time
102
+
103
+ [start_time, self.class.current_time]
104
+ end
105
+ end
106
+ end
@@ -55,7 +55,7 @@ module GraphQLMetrics
55
55
 
56
56
  def resolve(loader)
57
57
  @resolve_meta = {
58
- start_time: Process.clock_gettime(Process::CLOCK_MONOTONIC),
58
+ start_time: Instrumentation.current_time,
59
59
  current_loader: loader,
60
60
  perform_queue_sizes: loader.send(:queue).size
61
61
  }
@@ -66,7 +66,7 @@ module GraphQLMetrics
66
66
  def around_promise_callbacks
67
67
  return super unless @resolve_meta
68
68
 
69
- end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
69
+ end_time = Instrumentation.current_time
70
70
 
71
71
  TIMINGS[@resolve_meta[:current_loader].loader_key] ||= { times: [], perform_queue_sizes: [] }
72
72
  TIMINGS[@resolve_meta[:current_loader].loader_key][:times] << end_time - @resolve_meta[:start_time]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphQLMetrics
4
- VERSION = "1.1.5"
4
+ VERSION = "2.0.0"
5
5
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "graphql_metrics/version"
4
+ require "graphql_metrics/instrumentation"
4
5
  require "graphql_metrics/extractor"
5
6
  require "graphql_metrics/timed_batch_executor"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-metrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.5
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christopher Butcher
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-09-28 00:00:00.000000000 Z
11
+ date: 2018-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: minitest-focus
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
167
181
  description: |
168
182
  Extract as much much detail as you want from GraphQL queries, served up from your Ruby app and the `graphql` gem.
169
183
  Compatible with the `graphql-batch` gem, to extract batch-loaded fields resolution timings.
@@ -186,6 +200,7 @@ files:
186
200
  - graphql_metrics.gemspec
187
201
  - lib/graphql_metrics.rb
188
202
  - lib/graphql_metrics/extractor.rb
203
+ - lib/graphql_metrics/instrumentation.rb
189
204
  - lib/graphql_metrics/timed_batch_executor.rb
190
205
  - lib/graphql_metrics/version.rb
191
206
  homepage: https://github.com/Shopify/graphql-metrics
@@ -208,7 +223,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
208
223
  version: '0'
209
224
  requirements: []
210
225
  rubyforge_project:
211
- rubygems_version: 2.5.2
226
+ rubygems_version: 2.5.2.3
212
227
  signing_key:
213
228
  specification_version: 4
214
229
  summary: GraphQL Metrics Extractor