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.
- 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/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.5
|
data/CHANGELOG.md
ADDED
@@ -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! 🎉
|
data/Gemfile.lock
CHANGED
@@ -1,44 +1,47 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
graphql-metrics (
|
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.
|
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 (
|
16
|
+
byebug (11.0.1)
|
15
17
|
coderay (1.1.2)
|
16
|
-
concurrent-ruby (1.
|
17
|
-
diffy (3.
|
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.
|
21
|
-
graphql-batch (0.
|
22
|
-
graphql (>=
|
22
|
+
graphql (1.9.16)
|
23
|
+
graphql-batch (0.4.1)
|
24
|
+
graphql (>= 1.3, < 2)
|
23
25
|
promise.rb (~> 0.7.2)
|
24
|
-
|
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.
|
28
|
-
minitest (5.
|
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.
|
34
|
+
mocha (1.9.0)
|
32
35
|
metaclass (~> 0.0.1)
|
33
36
|
promise.rb (0.7.4)
|
34
|
-
pry (0.
|
37
|
+
pry (0.12.2)
|
35
38
|
coderay (~> 1.1.0)
|
36
39
|
method_source (~> 0.9.0)
|
37
|
-
pry-byebug (3.
|
38
|
-
byebug (~>
|
40
|
+
pry-byebug (3.7.0)
|
41
|
+
byebug (~> 11.0)
|
39
42
|
pry (~> 0.10)
|
40
|
-
rake (
|
41
|
-
redis (4.
|
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 (~>
|
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
|
-
|
68
|
+
2.0.2
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
# GraphQL Metrics
|
1
|
+
# GraphQL Metrics
|
2
2
|
|
3
|
-
|
3
|
+

|
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 '
|
18
|
+
require 'graphql/metrics'
|
19
19
|
```
|
20
20
|
|
21
21
|
Or globally in the Gemfile with:
|
22
22
|
```ruby
|
23
|
-
gem 'graphql-metrics', require: '
|
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
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
51
|
-
implementing the methods below, as needed.
|
43
|
+
What you do with these captured metrics is up to you!
|
52
44
|
|
53
|
-
|
45
|
+
### Define your own analyzer subclass
|
54
46
|
|
55
47
|
```ruby
|
56
|
-
class
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
67
|
-
|
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
|
-
|
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
|
-
|
87
|
-
|
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
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
})
|
115
|
-
end
|
167
|
+
```ruby
|
168
|
+
class Schema < GraphQL::Schema
|
169
|
+
query QueryRoot
|
170
|
+
mutation MutationRoot
|
116
171
|
|
117
|
-
|
118
|
-
|
119
|
-
def skip_extraction?(_query)
|
120
|
-
false
|
121
|
-
end
|
172
|
+
use GraphQL::Execution::Interpreter # Required.
|
173
|
+
use GraphQL::Analysis::AST # Required.
|
122
174
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
false
|
127
|
-
end
|
175
|
+
instrument :query, GraphQL::Metrics::Instrumentation.new
|
176
|
+
query_analyzer SimpleAnalyzer
|
177
|
+
tracer GraphQL::Metrics::Tracer.new
|
128
178
|
|
129
|
-
#
|
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
|
-
|
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
|
141
|
-
|
189
|
+
class Schema < GraphQL::Schema
|
190
|
+
query QueryRoot
|
191
|
+
mutation MutationRoot
|
142
192
|
|
143
|
-
|
144
|
-
|
145
|
-
end
|
193
|
+
use GraphQL::Execution::Interpreter # Required.
|
194
|
+
use GraphQL::Analysis::AST # Required.
|
146
195
|
|
147
|
-
|
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
|
-
|
161
|
-
|
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
|
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).
|