graphql-metrics 1.1.5 → 2.0.0

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