graphql-metrics 0.1.0 → 1.1.2

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
- SHA256:
3
- metadata.gz: cfae050ba496a760734e0ea43114f0bcd11064d3a16dc695aef8086d72b00b68
4
- data.tar.gz: b70dafa165c463e5bf498c57e68d74c8bd0d1a379c6e8b7342e036b9c40545da
2
+ SHA1:
3
+ metadata.gz: 245c973caa0e704220cc9b2363543758171de916
4
+ data.tar.gz: 0200a7fbbaa3ce77f5f564ee8649ee22a1c9a498
5
5
  SHA512:
6
- metadata.gz: 832c4347ebd7bb6edb43917aeabb49c5b6565df9f9e00cc2d0b31b6e200bfbd402e26b46d6780951230d78c4f3dd4761189d45e633af28a1b58c498a1126e9b5
7
- data.tar.gz: 6f0ecfb8e52b91c012195210800b68ab0d190deb9f6f42a2fed37fbd88f9401a89e4041275d8dcb44e9d8d4d0350f7dba4458082a3c56f5580b2474e1b7cfe26
6
+ metadata.gz: 1077c5445ffa0052f6d8570acd44717550aa09de861461f24f8edd0b55c7eb7d87493fc463b94313f3499ae9b56015e4cfdc3cdd9e876beea7d2b6714569a3ea
7
+ data.tar.gz: fbc1aea880d0e801334706ddcf39b188d33d26827f856d8bc0f8bdcab04a096b6fc9c9265fa133a01f14075a57e0f2cedba740f1784da58ad9b05d0bfcfbdd0e
data/.gitignore CHANGED
@@ -6,3 +6,4 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ *.gem
@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
55
55
  ## Enforcement
56
56
 
57
57
  Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
- reported by contacting the project team at christopher.butcher@shopify.com. All
58
+ reported by contacting the project team via a Github issue on this repo. All
59
59
  complaints will be reviewed and investigated and will result in a response that
60
60
  is deemed necessary and appropriate to the circumstances. The project team is
61
61
  obligated to maintain confidentiality with regard to the reporter of an incident.
data/Gemfile CHANGED
@@ -4,3 +4,7 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  # Specify your gem's dependencies in graphql_metrics.gemspec
6
6
  gemspec
7
+
8
+ group :deployment do
9
+ gem 'rake'
10
+ end
@@ -1,22 +1,57 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql-metrics (0.1.0)
4
+ graphql-metrics (1.1.2)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- minitest (5.10.3)
9
+ activesupport (5.1.6)
10
+ concurrent-ruby (~> 1.0, >= 1.0.2)
11
+ i18n (>= 0.7, < 2)
12
+ minitest (~> 5.1)
13
+ tzinfo (~> 1.1)
14
+ coderay (1.1.2)
15
+ concurrent-ruby (1.0.5)
16
+ diffy (3.2.1)
17
+ fakeredis (0.7.0)
18
+ redis (>= 3.2, < 5.0)
19
+ graphql (1.8.4)
20
+ graphql-batch (0.3.9)
21
+ graphql (>= 0.8, < 2)
22
+ promise.rb (~> 0.7.2)
23
+ i18n (1.0.1)
24
+ concurrent-ruby (~> 1.0)
25
+ metaclass (0.0.4)
26
+ method_source (0.9.0)
27
+ minitest (5.11.3)
28
+ mocha (1.5.0)
29
+ metaclass (~> 0.0.1)
30
+ promise.rb (0.7.4)
31
+ pry (0.11.3)
32
+ coderay (~> 1.1.0)
33
+ method_source (~> 0.9.0)
10
34
  rake (10.5.0)
35
+ redis (4.0.1)
36
+ thread_safe (0.3.6)
37
+ tzinfo (1.2.5)
38
+ thread_safe (~> 0.1)
11
39
 
12
40
  PLATFORMS
13
41
  ruby
14
42
 
15
43
  DEPENDENCIES
44
+ activesupport (~> 5.1.5)
16
45
  bundler (~> 1.16)
46
+ diffy
47
+ fakeredis
48
+ graphql (~> 1.8.2)
49
+ graphql-batch
17
50
  graphql-metrics!
18
51
  minitest (~> 5.0)
19
- rake (~> 10.0)
52
+ mocha
53
+ pry
54
+ rake
20
55
 
21
56
  BUNDLED WITH
22
- 1.16.1
57
+ 1.16.3
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018 Christopher Butcher
3
+ Copyright (c) 2014 - 2018 Shopify
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,8 +1,7 @@
1
- # Graphql Metrics
1
+ # GraphQL Metrics Extractor
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/graphql_metrics`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Extract as much much detail as you want from GraphQL queries, served up from your Ruby app and the `graphql` gem.
4
+ Compatible with the `graphql-batch` gem, to extract batch-loaded fields resolution timings.
6
5
 
7
6
  ## Installation
8
7
 
@@ -22,17 +21,115 @@ Or install it yourself as:
22
21
 
23
22
  ## Usage
24
23
 
25
- TODO: Write usage instructions here
24
+ You can get started quickly with all features enabled by instrumenting your queries
25
+ with an extractor class (defined below) and with `TimedBatchExecutor` passed as
26
+ a custom executor when initializing `GraphQL::Batch` instrumentation if you're using it.
27
+
28
+ ```ruby
29
+ class Schema < GraphQL::Schema
30
+ query QueryRoot
31
+ mutation MutationRoot
32
+
33
+ use LoggingExtractor # Replace me with your own subclass of GraphQLMetrics::Extractor!
34
+ use GraphQL::Batch, executor_class: GraphQLMetrics::TimedBatchExecutor # Optional.
35
+ end
36
+ ```
37
+
38
+ Define your own extractor class, inheriting from `GraphQLMetrics::Extractor`, and
39
+ implementing the methods below, as needed.
40
+
41
+ Here's an example of a simple extractor that logs out all GraphQL query details.
42
+
43
+ ```ruby
44
+ class LoggingExtractor < GraphQLMetrics::Extractor
45
+ def query_extracted(metrics, _metadata)
46
+ Rails.logger.debug({
47
+ query_string: metrics[:query_string],
48
+ operation_type: metrics[:operation_type],
49
+ operation_name: metrics[:operation_name],
50
+ duration: metrics[:duration]
51
+ })
52
+ end
53
+
54
+ def field_extracted(metrics, _metadata)
55
+ Rails.logger.debug({
56
+ type_name: metrics[:type_name],
57
+ field_name: metrics[:field_name],
58
+ deprecated: metrics[:deprecated],
59
+ resolver_times: metrics[:resolver_times],
60
+ })
61
+ end
62
+
63
+ # NOTE: Applicaable only if you set `use GraphQL::Batch, executor_class: GraphQLMetrics::TimedBatchExecutor`
64
+ # in your schema.
65
+ def batch_loaded_field_extracted(metrics, _metadata)
66
+ Rails.logger.debug({
67
+ key: metrics[:key],
68
+ identifiers: metrics[:identifiers],
69
+ times: metrics[:times],
70
+ perform_queue_sizes: metrics[:perform_queue_sizes],
71
+ })
72
+ end
73
+
74
+ def argument_extracted(metrics, _metadata)
75
+ Rails.logger.debug({
76
+ name: metrics[:name],
77
+ type: metrics[:type],
78
+ value_is_null: metrics[:value_is_null],
79
+ default_used: metrics[:default_used],
80
+ parent_input_type: metrics[:parent_input_type],
81
+ field_name: metrics[:field_name],
82
+ field_base_type: metrics[:field_base_type],
83
+ })
84
+ end
85
+
86
+ def variable_extracted(metrics, _metadata)
87
+ Rails.logger.debug({
88
+ operation_name: metrics[:operation_name],
89
+ unwrapped_type_name: metrics[:unwrapped_type_name],
90
+ type: metrics[:type],
91
+ default_value_type: metrics[:default_value_type],
92
+ provided_value: metrics[:provided_value],
93
+ default_used: metrics[:default_used],
94
+ })
95
+ end
96
+
97
+ # Define this if you want to do something with the query just before query logging.
98
+ def before_query_extracted(query, query_context)
99
+ Rails.logger.debug({
100
+ something_from_context: query_context[:something]
101
+ })
102
+ end
103
+
104
+ # Return something `truthy` if you want skip query extraction entirely, based on the query or
105
+ # for example its context.
106
+ def skip_extraction?(_query)
107
+ false
108
+ end
109
+
110
+ # Return something `truthy` if you want skip producing field resolution
111
+ # timing metrics. Applicable only if `field_extracted` is also defined.
112
+ def skip_field_resolution_timing?(_query, _metadata)
113
+ false
114
+ end
115
+
116
+ # Use or clear state after metrics extraction
117
+ def after_query_teardown(_query)
118
+ # Use or clear state after metrics extraction, i.e. Flush metrics to Datadog, Kafka etc.
119
+ # i.e. kafka.producer.produce('graphql_metrics', @collected_metrics); kafka.producer.deliver_messages
120
+ end
121
+ end
122
+ ```
26
123
 
27
124
  ## Development
28
125
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
126
+ 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.
30
127
 
31
128
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
129
 
33
130
  ## Contributing
34
131
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/graphql_metrics. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
132
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/graphql_metrics. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
133
 
37
134
  ## License
38
135
 
@@ -40,4 +137,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
40
137
 
41
138
  ## Code of Conduct
42
139
 
43
- Everyone interacting in the GraphqlMetrics project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/graphql-metrics/blob/master/CODE_OF_CONDUCT.md).
140
+ Everyone interacting in the GraphQLMetrics project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/graphql-metrics/blob/master/CODE_OF_CONDUCT.md).
@@ -5,13 +5,16 @@ require "graphql_metrics/version"
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "graphql-metrics"
8
- spec.version = GraphqlMetrics::VERSION
8
+ spec.version = GraphQLMetrics::VERSION
9
9
  spec.authors = ["Christopher Butcher"]
10
- spec.email = ["cbutcher@gmail.com"]
10
+ spec.email = ["gems@shopify.com"]
11
11
 
12
- spec.summary = 'SUMMARY'
13
- spec.description = 'DESCRIPTION'
14
- spec.homepage = 'https://github.com/chrisbutcher/graphql-metrics'
12
+ spec.summary = 'GraphQL Metrics Extractor'
13
+ spec.description = <<~DESCRIPTION
14
+ Extract as much much detail as you want from GraphQL queries, served up from your Ruby app and the `graphql` gem.
15
+ Compatible with the `graphql-batch` gem, to extract batch-loaded fields resolution timings.
16
+ DESCRIPTION
17
+ spec.homepage = 'https://github.com/Shopify/graphql-metrics'
15
18
  spec.license = "MIT"
16
19
 
17
20
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
@@ -33,4 +36,11 @@ Gem::Specification.new do |spec|
33
36
  spec.add_development_dependency "bundler", "~> 1.16"
34
37
  spec.add_development_dependency "rake", "~> 10.0"
35
38
  spec.add_development_dependency "minitest", "~> 5.0"
39
+ spec.add_development_dependency 'graphql-batch'
40
+ spec.add_development_dependency "graphql", "~> 1.8.2"
41
+ spec.add_development_dependency "activesupport", "~> 5.1.5"
42
+ spec.add_development_dependency "pry"
43
+ spec.add_development_dependency "mocha"
44
+ spec.add_development_dependency "diffy"
45
+ spec.add_development_dependency "fakeredis"
36
46
  end
@@ -1,5 +1,5 @@
1
- require "graphql_metrics/version"
1
+ # frozen_string_literal: true
2
2
 
3
- module GraphqlMetrics
4
- # Your code goes here...
5
- end
3
+ require "graphql_metrics/version"
4
+ require "graphql_metrics/extractor"
5
+ require "graphql_metrics/timed_batch_executor"
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQLMetrics
4
+ class Extractor
5
+ CONTEXT_NAMESPACE = :extracted_metrics
6
+ TIMING_CACHE_KEY = :timing_cache
7
+ START_TIME_KEY = :query_start_time
8
+
9
+ EXPLICIT_NULL = 'EXPLICIT_NULL'
10
+ IMPLICIT_NULL = 'IMPLICIT_NULL'
11
+ NON_NULL = 'NON_NULL'
12
+
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
26
+
27
+ def setup_instrumentation(schema_definition)
28
+ schema_definition.instrument(:query, self)
29
+ schema_definition.instrument(:field, self)
30
+ end
31
+
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)
40
+ end
41
+
42
+ def after_query(query)
43
+ return unless extractor_defines_any_visitors?
44
+
45
+ @query = query
46
+ 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
+ return unless query.irep_selection
50
+
51
+ before_query_extracted(query, query.context) if respond_to?(:before_query_extracted)
52
+ extract_query
53
+
54
+ query.operations.each_value do |operation|
55
+ extract_variables(operation)
56
+ end
57
+
58
+ extract_node(query.irep_selection)
59
+ 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
+ end
95
+
96
+ 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)
103
+ end
104
+
105
+ def handle_extraction_exception(ex)
106
+ raise ex
107
+ end
108
+
109
+ def extract_batch_loaders
110
+ return unless respond_to?(:batch_loaded_field_extracted)
111
+
112
+ TimedBatchExecutor.timings.each do |key, resolve_meta|
113
+ key, identifiers = TimedBatchExecutor.serialize_loader_key(key)
114
+
115
+ batch_loaded_field_extracted(
116
+ {
117
+ key: key,
118
+ identifiers: identifiers,
119
+ times: resolve_meta[:times],
120
+ perform_queue_sizes: resolve_meta[:perform_queue_sizes],
121
+ },
122
+ {
123
+ query: query,
124
+ }
125
+ )
126
+ end
127
+ rescue StandardError => ex
128
+ handle_extraction_exception(ex)
129
+ ensure
130
+ TimedBatchExecutor.clear_timings
131
+ end
132
+
133
+ def extract_query
134
+ return unless respond_to?(:query_extracted)
135
+
136
+ start_time = ctx_namespace[START_TIME_KEY]
137
+ return unless start_time
138
+
139
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
140
+
141
+ query_extracted(
142
+ {
143
+ query_string: query.document.to_query_string,
144
+ operation_type: query.selected_operation.operation_type,
145
+ operation_name: query.selected_operation_name,
146
+ duration: end_time - start_time
147
+ },
148
+ {
149
+ query: query,
150
+ start_time: start_time,
151
+ end_time: end_time
152
+ }
153
+ )
154
+ rescue StandardError => ex
155
+ handle_extraction_exception(ex)
156
+ end
157
+
158
+ def extract_field(irep_node)
159
+ return unless respond_to?(:field_extracted)
160
+ return unless irep_node.definition
161
+
162
+ field_extracted(
163
+ {
164
+ type_name: irep_node.owner_type.name,
165
+ field_name: irep_node.definition.name,
166
+ deprecated: irep_node.definition.deprecation_reason.present?,
167
+ resolver_times: ctx_namespace.dig(TIMING_CACHE_KEY, irep_node.ast_node) || [],
168
+ },
169
+ {
170
+ irep_node: irep_node,
171
+ query: query,
172
+ ctx_namespace: ctx_namespace
173
+ }
174
+ )
175
+
176
+ rescue StandardError => ex
177
+ handle_extraction_exception(ex)
178
+ end
179
+
180
+ def extract_argument(value, irep_node, types)
181
+ return unless respond_to?(:argument_extracted)
182
+
183
+ argument_extracted(
184
+ {
185
+ name: value.definition.expose_as,
186
+ type: value.definition.type.unwrap.to_s,
187
+ value_is_null: value.value.nil?,
188
+ default_used: value.default_used?,
189
+ parent_input_type: types.map(&:unwrap).last&.to_s,
190
+ field_name: irep_node.definition.name,
191
+ field_base_type: irep_node&.owner_type.to_s
192
+ },
193
+ {
194
+ query: query,
195
+ irep_node: irep_node,
196
+ value: value,
197
+ }
198
+ )
199
+ rescue StandardError => ex
200
+ handle_extraction_exception(ex)
201
+ end
202
+
203
+ def extract_variables(operation)
204
+ return unless respond_to?(:variable_extracted)
205
+
206
+ operation.variables.each do |variable|
207
+ value_provided = query.provided_variables.key?(variable.name)
208
+
209
+ default_value_type = case variable.default_value
210
+ when GraphQL::Language::Nodes::NullValue
211
+ EXPLICIT_NULL
212
+ when nil
213
+ IMPLICIT_NULL
214
+ else
215
+ NON_NULL
216
+ end
217
+
218
+ default_used = !value_provided && default_value_type != IMPLICIT_NULL
219
+
220
+ variable_extracted(
221
+ {
222
+ operation_name: operation.name,
223
+ unwrapped_type_name: unwrapped_type(variable.type),
224
+ type: variable.type.to_query_string,
225
+ default_value_type: default_value_type,
226
+ provided_value: value_provided,
227
+ default_used: default_used
228
+ },
229
+ {
230
+ query: query
231
+ }
232
+ )
233
+ end
234
+ rescue StandardError => ex
235
+ handle_extraction_exception(ex)
236
+ end
237
+
238
+ def extract_arguments(irep_node)
239
+ return unless irep_node.ast_node.is_a?(GraphQL::Language::Nodes::Field)
240
+
241
+ traverse_arguments(irep_node.arguments.argument_values.values, irep_node)
242
+ rescue GraphQL::ExecutionError
243
+ # no-op. See https://github.com/rmosolgo/graphql-ruby/issues/982.
244
+ nil
245
+ rescue StandardError => ex
246
+ handle_extraction_exception(ex)
247
+ end
248
+
249
+ def extract_node(irep_node)
250
+ extract_field(irep_node)
251
+ extract_arguments(irep_node)
252
+
253
+ irep_node.typed_children.each_value do |children|
254
+ children.each_value do |child_irep_node|
255
+ extract_node(child_irep_node)
256
+ end
257
+ end
258
+ rescue StandardError => ex
259
+ handle_extraction_exception(ex)
260
+ end
261
+
262
+ def traverse_arguments(value, irep_node, types = [])
263
+ case value
264
+ when Array
265
+ value.each do |v|
266
+ traverse_arguments(v, irep_node, types)
267
+ end
268
+ when Hash
269
+ value.each_value do |v|
270
+ traverse_arguments(v, irep_node, types)
271
+ end
272
+ when ::GraphQL::Query::Arguments
273
+ value.each_value do |arg_val|
274
+ traverse_arguments(arg_val, irep_node, types)
275
+ end
276
+ when ::GraphQL::Query::Arguments::ArgumentValue
277
+ extract_argument(value, irep_node, types)
278
+ traverse_arguments(value.value, irep_node, types + [value.definition.type])
279
+ when ::GraphQL::Schema::InputObject
280
+ traverse_arguments(value.arguments.argument_values.values, irep_node, types)
281
+ end
282
+ rescue StandardError => ex
283
+ handle_extraction_exception(ex)
284
+ end
285
+
286
+ def unwrapped_type(type)
287
+ if type.is_a?(GraphQL::Language::Nodes::WrapperType)
288
+ unwrapped_type(type.of_type)
289
+ else
290
+ type.name
291
+ end
292
+ rescue StandardError => ex
293
+ handle_extraction_exception(ex)
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQLMetrics
4
+ BASE_CLASS = if defined?(GraphQL::Batch::Executor)
5
+ GraphQL::Batch::Executor
6
+ else
7
+ class NoExecutor
8
+ class << self
9
+ def resolve(_loader)
10
+ super
11
+ end
12
+
13
+ def around_promise_callbacks
14
+ super
15
+ end
16
+ end
17
+ end
18
+
19
+ NoExecutor
20
+ end
21
+
22
+ class TimedBatchExecutor < BASE_CLASS
23
+ TIMINGS = {}
24
+ private_constant :TIMINGS
25
+
26
+ class << self
27
+ def timings
28
+ TIMINGS
29
+ end
30
+
31
+ def clear_timings
32
+ TIMINGS.clear
33
+ end
34
+
35
+ def serialize_loader_key(loader_key)
36
+ identifiers = []
37
+
38
+ serialized = loader_key.map do |group_arg|
39
+ if [Class, Symbol, String].include?(group_arg.class)
40
+ group_arg
41
+ elsif group_arg.is_a?(Numeric)
42
+ identifiers << group_arg
43
+ '_'
44
+ elsif group_arg.respond_to?(:id)
45
+ identifiers << group_arg.id
46
+ "#{group_arg.class}/_"
47
+ else
48
+ '?'
49
+ end
50
+ end
51
+
52
+ [serialized.map(&:to_s).join('/'), identifiers.map(&:to_s)]
53
+ end
54
+ end
55
+
56
+ def resolve(loader)
57
+ @resolve_meta = {
58
+ start_time: Process.clock_gettime(Process::CLOCK_MONOTONIC),
59
+ current_loader: loader,
60
+ perform_queue_sizes: loader.send(:queue).size
61
+ }
62
+
63
+ super
64
+ end
65
+
66
+ def around_promise_callbacks
67
+ return super unless @resolve_meta
68
+
69
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
70
+
71
+ TIMINGS[@resolve_meta[:current_loader].loader_key] ||= { times: [], perform_queue_sizes: [] }
72
+ TIMINGS[@resolve_meta[:current_loader].loader_key][:times] << end_time - @resolve_meta[:start_time]
73
+ TIMINGS[@resolve_meta[:current_loader].loader_key][:perform_queue_sizes] << @resolve_meta[:perform_queue_sizes]
74
+
75
+ @resolve_meta = nil
76
+
77
+ super
78
+ end
79
+ end
80
+ end
@@ -1,3 +1,5 @@
1
- module GraphqlMetrics
2
- VERSION = "0.1.0"
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQLMetrics
4
+ VERSION = "1.1.2"
3
5
  end
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: 0.1.0
4
+ version: 1.1.2
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-03-27 00:00:00.000000000 Z
11
+ date: 2018-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,9 +52,109 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.0'
55
- description: DESCRIPTION
55
+ - !ruby/object:Gem::Dependency
56
+ name: graphql-batch
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: graphql
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.8.2
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.8.2
83
+ - !ruby/object:Gem::Dependency
84
+ name: activesupport
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 5.1.5
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 5.1.5
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: mocha
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: diffy
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: fakeredis
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: |
154
+ Extract as much much detail as you want from GraphQL queries, served up from your Ruby app and the `graphql` gem.
155
+ Compatible with the `graphql-batch` gem, to extract batch-loaded fields resolution timings.
56
156
  email:
57
- - cbutcher@gmail.com
157
+ - gems@shopify.com
58
158
  executables: []
59
159
  extensions: []
60
160
  extra_rdoc_files: []
@@ -71,8 +171,10 @@ files:
71
171
  - bin/setup
72
172
  - graphql_metrics.gemspec
73
173
  - lib/graphql_metrics.rb
174
+ - lib/graphql_metrics/extractor.rb
175
+ - lib/graphql_metrics/timed_batch_executor.rb
74
176
  - lib/graphql_metrics/version.rb
75
- homepage: https://github.com/chrisbutcher/graphql-metrics
177
+ homepage: https://github.com/Shopify/graphql-metrics
76
178
  licenses:
77
179
  - MIT
78
180
  metadata: {}
@@ -92,8 +194,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
194
  version: '0'
93
195
  requirements: []
94
196
  rubyforge_project:
95
- rubygems_version: 2.7.6
197
+ rubygems_version: 2.5.2
96
198
  signing_key:
97
199
  specification_version: 4
98
- summary: SUMMARY
200
+ summary: GraphQL Metrics Extractor
99
201
  test_files: []