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.
@@ -0,0 +1,5 @@
1
+ inherit_from:
2
+ - http://shopify.github.io/ruby-style-guide/rubocop.yml
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 2.6
@@ -0,0 +1 @@
1
+ 2.6.5
@@ -0,0 +1,39 @@
1
+ 3.0.0
2
+ -----
3
+
4
+ A complete re-write of the gem.
5
+
6
+ Just about everything in the 2.0.0 public interface breaks, but everything gets substantially better, with more metrics
7
+ extracted, more consistent naming and structures, and it all runs faster too! 🎉
8
+
9
+ The core analyzer (which your app should subclass) is now a `GraphQL::Analysis::AST::Analyzer`, and the tracer and
10
+ instrumentation for timings metrics are now fully separate classes.
11
+
12
+ 2.0.1
13
+ -----
14
+
15
+ Fixes cases where instances of `GraphQLMetrics::Instrumentation` are passed to `Schema#new`, i.e. via `Schema.redefine`
16
+ (https://github.com/Shopify/graphql-metrics/commit/6624dcd0aa04006f092b850752bb05d3da688745#diff-d64de6d4fb3a1d05c273e19469c9852aR439)
17
+
18
+ 2.0.0
19
+ -----
20
+
21
+ 2.0.0 contains a breaking change.
22
+
23
+ See https://github.com/Shopify/graphql-metrics#usage
24
+
25
+ * `GraphQLMetrics::Extractor` was renamed `GraphQLMetrics::Instrumentation` <- Use the latter to migrate away from the
26
+ breaking change.
27
+ * `GraphQLMetrics::Extractor` was then re-introduced in order to support ad hoc static query metrics extraction,
28
+ without using subclasses as runtime instrumentation.
29
+
30
+
31
+ 1.0.1 to 1.1.5
32
+ -----
33
+
34
+ * Minor bug fixes
35
+
36
+ 1.0.0
37
+ -----
38
+
39
+ * Initialize release! 🎉
@@ -1,44 +1,47 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql-metrics (2.0.1)
4
+ graphql-metrics (3.0.0)
5
+ concurrent-ruby (~> 1.1.0)
6
+ graphql (~> 1.9.15)
5
7
 
6
8
  GEM
7
9
  remote: https://rubygems.org/
8
10
  specs:
9
- activesupport (5.1.6)
11
+ activesupport (5.1.7)
10
12
  concurrent-ruby (~> 1.0, >= 1.0.2)
11
13
  i18n (>= 0.7, < 2)
12
14
  minitest (~> 5.1)
13
15
  tzinfo (~> 1.1)
14
- byebug (10.0.2)
16
+ byebug (11.0.1)
15
17
  coderay (1.1.2)
16
- concurrent-ruby (1.0.5)
17
- diffy (3.2.1)
18
+ concurrent-ruby (1.1.5)
19
+ diffy (3.3.0)
18
20
  fakeredis (0.7.0)
19
21
  redis (>= 3.2, < 5.0)
20
- graphql (1.8.4)
21
- graphql-batch (0.3.9)
22
- graphql (>= 0.8, < 2)
22
+ graphql (1.9.16)
23
+ graphql-batch (0.4.1)
24
+ graphql (>= 1.3, < 2)
23
25
  promise.rb (~> 0.7.2)
24
- i18n (1.0.1)
26
+ hashdiff (1.0.0)
27
+ i18n (1.7.0)
25
28
  concurrent-ruby (~> 1.0)
26
29
  metaclass (0.0.4)
27
- method_source (0.9.0)
28
- minitest (5.11.3)
30
+ method_source (0.9.2)
31
+ minitest (5.13.0)
29
32
  minitest-focus (1.1.2)
30
33
  minitest (>= 4, < 6)
31
- mocha (1.5.0)
34
+ mocha (1.9.0)
32
35
  metaclass (~> 0.0.1)
33
36
  promise.rb (0.7.4)
34
- pry (0.11.3)
37
+ pry (0.12.2)
35
38
  coderay (~> 1.1.0)
36
39
  method_source (~> 0.9.0)
37
- pry-byebug (3.6.0)
38
- byebug (~> 10.0)
40
+ pry-byebug (3.7.0)
41
+ byebug (~> 11.0)
39
42
  pry (~> 0.10)
40
- rake (10.5.0)
41
- redis (4.0.1)
43
+ rake (13.0.1)
44
+ redis (4.1.3)
42
45
  thread_safe (0.3.6)
43
46
  tzinfo (1.2.5)
44
47
  thread_safe (~> 0.1)
@@ -48,12 +51,12 @@ PLATFORMS
48
51
 
49
52
  DEPENDENCIES
50
53
  activesupport (~> 5.1.5)
51
- bundler (~> 1.16)
54
+ bundler (~> 2.0.2)
52
55
  diffy
53
56
  fakeredis
54
- graphql (~> 1.8.2)
55
57
  graphql-batch
56
58
  graphql-metrics!
59
+ hashdiff
57
60
  minitest (~> 5.0)
58
61
  minitest-focus
59
62
  mocha
@@ -62,4 +65,4 @@ DEPENDENCIES
62
65
  rake
63
66
 
64
67
  BUNDLED WITH
65
- 1.16.3
68
+ 2.0.2
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # GraphQL Metrics Extractor
1
+ # GraphQL Metrics
2
2
 
3
- [![Build Status](https://travis-ci.org/Shopify/graphql-metrics.svg?branch=master)](https://travis-ci.org/Shopify/graphql-metrics)
3
+ ![](https://github.com/Shopify/graphql-metrics/workflows/Ruby/badge.svg)
4
4
 
5
5
  Extract as much much detail as you want from GraphQL queries, served up from your Ruby app and the [`graphql` gem](https://github.com/rmosolgo/graphql-ruby).
6
6
  Compatible with the [`graphql-batch` gem](https://github.com/Shopify/graphql-batch), to extract batch-loaded fields resolution timings.
@@ -15,12 +15,12 @@ gem 'graphql-metrics'
15
15
 
16
16
  You can require it with in your code as needed with:
17
17
  ```ruby
18
- require 'graphql_metrics'
18
+ require 'graphql/metrics'
19
19
  ```
20
20
 
21
21
  Or globally in the Gemfile with:
22
22
  ```ruby
23
- gem 'graphql-metrics', require: 'graphql_metrics'
23
+ gem 'graphql-metrics', require: 'graphql/metrics'
24
24
  ```
25
25
 
26
26
  And then execute:
@@ -33,132 +33,204 @@ Or install it yourself as:
33
33
 
34
34
  ## Usage
35
35
 
36
- You can get started quickly with all features enabled by instrumenting your queries
37
- with an extractor class (defined below) and with `TimedBatchExecutor` passed as
38
- a custom executor when initializing `GraphQL::Batch` instrumentation if you're using it.
36
+ Get started by defining your own Analyzer, inheriting from `GraphQL::Metrics::Analyzer`.
39
37
 
40
- ```ruby
41
- class Schema < GraphQL::Schema
42
- query QueryRoot
43
- mutation MutationRoot
44
-
45
- use LoggingExtractor # Replace me with your own subclass of GraphQLMetrics::Extractor!
46
- use GraphQL::Batch, executor_class: GraphQLMetrics::TimedBatchExecutor # Optional.
47
- end
48
- ```
38
+ The following analyzer demonstrates a simple way to capture commonly used metrics sourced from key parts of your schema
39
+ definition, the query document being served, as well as runtime query and resolver timings. In this toy example, all of
40
+ this data is simply stored on the GraphQL::Query context, under a namespace to avoid collisions with other analyzers
41
+ etc.
49
42
 
50
- Define your own extractor class, inheriting from `GraphQLMetrics::Extractor`, and
51
- implementing the methods below, as needed.
43
+ What you do with these captured metrics is up to you!
52
44
 
53
- Here's an example of a simple extractor that logs out all GraphQL query details.
45
+ ### Define your own analyzer subclass
54
46
 
55
47
  ```ruby
56
- class LoggingExtractor < GraphQLMetrics::Instrumentation
57
- def query_extracted(metrics, _metadata)
58
- Rails.logger.debug({
59
- query_string: metrics[:query_string], # "query Project { project(name: "GraphQL") { tagline } }"
60
- operation_type: metrics[:operation_type], # "query"
61
- operation_name: metrics[:operation_name], # "Project"
62
- duration: metrics[:duration] # 0.1
63
- })
48
+ class CaptureAllMetricsAnalyzer < GraphQL::Metrics::Analyzer
49
+ ANALYZER_NAMESPACE = :capture_all_metrics_analyzer_namespace
50
+
51
+ def initialize(query_or_multiplex)
52
+ super
53
+
54
+ # `query` is defined on instances of objects inheriting from GraphQL::Metrics::Analyzer
55
+ ns = query.context.namespace(ANALYZER_NAMESPACE)
56
+ ns[:simple_extractor_results] = {}
57
+ end
58
+
59
+ # @param metrics [Hash] Query metrics, including a few details about the query document itself, as well as runtime
60
+ # timings metrics, intended to be compatible with the Apollo Tracing spec:
61
+ # https://github.com/apollographql/apollo-tracing#response-format
62
+ #
63
+ # {
64
+ # operation_type: "query",
65
+ # operation_name: "PostDetails",
66
+ # query_start_time: 1573833076.027327,
67
+ # query_duration: 2.0207119999686256,
68
+ # parsing_start_time_offset: 0.0010339999571442604,
69
+ # parsing_duration: 0.0008190000080503523,
70
+ # validation_start_time_offset: 0.0030819999519735575,
71
+ # validation_duration: 0.01704599999357015,
72
+ # }
73
+ #
74
+ # You can use these metrics to track high-level query performance, along with any other details you wish to
75
+ # manually capture from `query` and/or `query.context`.
76
+ def query_extracted(metrics)
77
+ custom_metrics_from_context = {
78
+ request_id: query.context[:request_id],
79
+ # ...
80
+ }
81
+
82
+ # You can make use of captured metrics here (logging to Kafka, request logging etc.)
83
+ # log_metrics(:fields, metrics)
84
+ #
85
+ # Or store them on the query context:
86
+ store_metrics(:queries, metrics.merge(custom_metrics_from_context))
87
+ end
88
+
89
+ # For use after controller:
90
+ # class GraphQLController < ActionController::Base
91
+ # def graphql_query
92
+ # query_result = graphql_query.result.to_h
93
+ # do_something_with_metrics(query.context[:simple_extractor_results])
94
+ # render json: graphql_query.result
95
+ # end
96
+ # end
97
+
98
+ # @param metrics [Hash] Field selection metrics, including resolver timings metrics, also adhering to the Apollo
99
+ # Tracing spec referred to above.
100
+ #
101
+ # `resolver_timings` is populated any time a field is resolved (which may be many times, if the field is nested
102
+ # within a list field e.g. a Relay connection field).
103
+ #
104
+ # `lazy_resolver_timings` is only populated by fields that are resolved lazily (for example using the
105
+ # graphql-batch gem) or that are otherwise resolved with a Promise. Any time spent in the field's resolver to
106
+ # prepare work to be done "later" in a Promise, or batch loader will be captured in `resolver_timings`. The time
107
+ # spent actually doing lazy field loading, including time spent within a batch loader can be obtained from
108
+ # `lazy_resolver_timings`.
109
+ #
110
+ # {
111
+ # field_name: "id",
112
+ # return_type_name: "ID",
113
+ # parent_type_name: "Post",
114
+ # deprecated: false,
115
+ # path: ["post", "id"],
116
+ # resolver_timings: [
117
+ # start_time_offset: 0.011901999998372048,
118
+ # duration: 5.999987479299307e-06
119
+ # ],
120
+ # lazy_resolver_timings: [
121
+ # start_time_offset: 0.031901999998372048,
122
+ # duration: 5.999987479299307e-06
123
+ # ],
124
+ # }
125
+ def field_extracted(metrics)
126
+ store_metrics(:fields, metrics)
127
+ end
128
+
129
+ # @param metrics [Hash] Argument usage metrics, including a few details about the query document itself, as well
130
+ # as resolver timings metrics, also ahering to the Apollo Tracing spec referred to above.
131
+ # {
132
+ # argument_name: "id",
133
+ # argument_type_name: "ID",
134
+ # parent_field_name: "post",
135
+ # parent_field_type_name: "QueryRoot",
136
+ # default_used: false,
137
+ # value_is_null: false,
138
+ # value: <GraphQL::Query::Arguments::ArgumentValue>,
139
+ # }
140
+ #
141
+ # `value` is exposed here, in case you want to get access to the argument's definition, including the type
142
+ # class which defines it, e.g. `metrics[:value].definition.metadata[:type_class]`
143
+ def argument_extracted(metrics)
144
+ store_metrics(:arguments, metrics)
145
+ end
146
+
147
+ private
148
+
149
+ def store_metrics(context_key, metrics)
150
+ ns = query.context.namespace(ANALYZER_NAMESPACE)
151
+ ns[:simple_extractor_results][context_key] ||= []
152
+ ns[:simple_extractor_results][context_key] << metrics
153
+ end
64
154
  end
155
+ ```
65
156
 
66
- def field_extracted(metrics, _metadata)
67
- Rails.logger.debug({
68
- type_name: metrics[:type_name], # "QueryRoot"
69
- field_name: metrics[:field_name], # "project"
70
- deprecated: metrics[:deprecated], # false
71
- resolver_times: metrics[:resolver_times], # [0.1]
72
- })
73
- end
157
+ Once defined, you can opt into capturing all metrics seen above by simply including GraphQL::Metrics as a plugin on your
158
+ schema.
74
159
 
75
- # NOTE: Applicable only if you set `use GraphQL::Batch, executor_class: GraphQLMetrics::TimedBatchExecutor`
76
- # in your schema.
77
- def batch_loaded_field_extracted(metrics, _metadata)
78
- Rails.logger.debug({
79
- key: metrics[:key], # "CommentLoader/Comment"
80
- identifiers: metrics[:identifiers], # "Comment/_/string/_/symbol/Class/?"
81
- times: metrics[:times], # [0.1, 0.2, 4]
82
- perform_queue_sizes: metrics[:perform_queue_sizes], # [3]
83
- })
84
- end
160
+ ### Make use of your analyzer
85
161
 
86
- def argument_extracted(metrics, _metadata)
87
- Rails.logger.debug({
88
- name: metrics[:name], # "post"
89
- type: metrics[:type], # "postInput"
90
- value_is_null: metrics[:value_is_null], # false
91
- default_used: metrics[:default_used], # false
92
- parent_input_type: metrics[:parent_input_type], # "PostInput"
93
- field_name: metrics[:field_name], # "postCreate"
94
- field_base_type: metrics[:field_base_type], # "MutationRoot"
95
- })
96
- end
162
+ Ensure that your schema is using the graphql-ruby 1.9+ `GraphQL::Execution::Interpreter` and `GraphQL::Analysis::AST`
163
+ engine, and then simply add the below `GraphQL::Metrics` plugins.
97
164
 
98
- def variable_extracted(metrics, _metadata)
99
- Rails.logger.debug({
100
- operation_name: metrics[:operation_name], # "MyMutation"
101
- unwrapped_type_name: metrics[:unwrapped_type_name], # "PostInput"
102
- type: metrics[:type], # "PostInput!"
103
- default_value_type: metrics[:default_value_type], # "IMPLICIT_NULL"
104
- provided_value: metrics[:provided_value], # false
105
- default_used: metrics[:default_used], # false
106
- used_in_operation: metrics[:used_in_operation], # true
107
- })
108
- end
165
+ This opts you in to capturing all static and runtime metrics seen above.
109
166
 
110
- # Define this if you want to do something with the query just before query logging.
111
- def before_query_extracted(query, query_context)
112
- Rails.logger.debug({
113
- something_from_context: query_context[:something]
114
- })
115
- end
167
+ ```ruby
168
+ class Schema < GraphQL::Schema
169
+ query QueryRoot
170
+ mutation MutationRoot
116
171
 
117
- # Return something `truthy` if you want skip query extraction entirely, based on the query or
118
- # for example its context.
119
- def skip_extraction?(_query)
120
- false
121
- end
172
+ use GraphQL::Execution::Interpreter # Required.
173
+ use GraphQL::Analysis::AST # Required.
122
174
 
123
- # Return something `truthy` if you want skip producing field resolution
124
- # timing metrics. Applicable only if `field_extracted` is also defined.
125
- def skip_field_resolution_timing?(_query, _metadata)
126
- false
127
- end
175
+ instrument :query, GraphQL::Metrics::Instrumentation.new
176
+ query_analyzer SimpleAnalyzer
177
+ tracer GraphQL::Metrics::Tracer.new
128
178
 
129
- # Use or clear state after metrics extraction
130
- def after_query_teardown(_query)
131
- # Use or clear state after metrics extraction, i.e. Flush metrics to Datadog, Kafka etc.
132
- # i.e. kafka.producer.produce('graphql_metrics', @collected_metrics); kafka.producer.deliver_messages
133
- end
179
+ use GraphQL::Batch # Optional, but highly recommended. See https://github.com/Shopify/graphql-batch/.
134
180
  end
135
181
  ```
136
182
 
137
- You can also define ad hoc query Extractors that can work with instances of GraphQL::Query, for example:
183
+ ### Optionally, only gather static metrics
184
+
185
+ If you don't care to capture runtime metrics like query and resolver timings, you can use your analyzer a standalone
186
+ analyzer without `GraphQL::Metrics::Instrumentation` and `tracer GraphQL::Metrics::Tracer`, like so:
138
187
 
139
188
  ```ruby
140
- class TypeUsageExtractor < GraphQLMetrics::Extractor
141
- attr_reader :types_used
189
+ class Schema < GraphQL::Schema
190
+ query QueryRoot
191
+ mutation MutationRoot
142
192
 
143
- def initialize
144
- @types_used = Set.new
145
- end
193
+ use GraphQL::Execution::Interpreter # Required.
194
+ use GraphQL::Analysis::AST # Required.
146
195
 
147
- def field_extracted(metrics, _metadata)
148
- @types_used << metrics[:type_name]
149
- end
196
+ query_analyzer SimpleAnalyzer
150
197
  end
151
-
152
- # ...
153
-
154
- extractor = TypeUsageExtractor.new
155
- extractor.extract!(query)
156
- puts extractor.types_used
157
- # => ["Comment", "Post", "QueryRoot"]
158
198
  ```
159
199
 
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.
200
+ Your analyzer will still be called with `query_extracted`, `field_extracted`, but with timings metrics omitted.
201
+ `argument_extracted` will work exactly the same, whether instrumentation and tracing are used or not.
202
+
203
+ ## Order of execution
204
+
205
+ Because of the structure of graphql-ruby's plugin architecture, it may be difficult to build an intuition around the
206
+ order in which methods defined on `GraphQL::Metrics::Instrumentation`, `GraphQL::Metrics::Tracer` and subclasses of
207
+ `GraphQL::Metrics::Analyzer` run.
208
+
209
+ Although you ideally will not need to care about these details if you are simply using this gem to gather metrics in
210
+ your application as intended, here's a breakdown of the order of execution of the methods involved:
211
+
212
+ When used as instrumentation, an analyzer and tracing, the order of execution is:
213
+
214
+ * Tracer.setup_tracing_before_lexing
215
+ * Tracer.capture_parsing_time
216
+ * Instrumentation.before_query (context setup)
217
+ * Tracer.capture_validation_time (twice, once for `analyze_query`, then `analyze_multiplex`)
218
+ * Analyzer#initialize (bit more context setup, instance vars setup)
219
+ * Analyzer#result
220
+ * Tracer.trace_field (n times)
221
+ * Instrumentation.after_query (call query and field callbacks, now that we have all static and runtime metrics
222
+ gathered)
223
+ * Analyzer#extract_query
224
+ * Analyzer#query_extracted
225
+ * Analyzer#extract_fields_with_runtime_metrics
226
+ * calls Analyzer#field_extracted n times
227
+
228
+ When used as a simple analyzer, which doesn't gather or emit any runtime metrics (timings, arg values):
229
+ * Analyzer#initialize
230
+ * Analyzer#field_extracted n times
231
+ * Analyzer#result
232
+ * Analyzer#extract_query
233
+ * Analyzer#query_extracted
162
234
 
163
235
  ## Development
164
236
 
@@ -176,4 +248,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
176
248
 
177
249
  ## Code of Conduct
178
250
 
179
- 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).
251
+ Everyone interacting in the GraphQL::Metrics 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).