graphql-metrics 4.0.0 → 4.0.5
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 +4 -4
- data/CHANGELOG.md +23 -0
- data/Gemfile.lock +1 -1
- data/README.md +17 -5
- data/RELEASING +15 -0
- data/lib/graphql/metrics.rb +2 -0
- data/lib/graphql/metrics/analyzer.rb +18 -8
- data/lib/graphql/metrics/instrumentation.rb +30 -14
- data/lib/graphql/metrics/tracer.rb +58 -22
- data/lib/graphql/metrics/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e35ba7c6756d4d431a32911e2bf8452f8f825ed1ff6d90c17ad0a0cb13f4d5f9
|
4
|
+
data.tar.gz: 1bb21be1384170bca7151a05ae36299c70193ca570bece6a7cdeb59f67e9ed57
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba5a5d9fe0b038410260a9fc7d05472a2a7b3012194bc727226429920225f93f617ce353f8121e07b0a27b6f13899c8895ef26bf37e9856df05482c692042603
|
7
|
+
data.tar.gz: baec02caa8599a575912c7e3e6696d11ac9bc0e88ebc5ed270eeaf7c0d8cf800bb5d1d48276f513a69c8921c43ef821a9f1239290a86027b9c104495e79c1ca3
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,26 @@
|
|
1
|
+
4.0.5
|
2
|
+
-----
|
3
|
+
- [34] Fix default of pre-parsed query `parsing_duration` to be Float (`0.0`) rather than Integer (`0`).
|
4
|
+
|
5
|
+
4.0.4
|
6
|
+
-----
|
7
|
+
- [33](https://github.com/Shopify/graphql-metrics/pull/33) Setup tracing using lex or execute_multiplex tracer events.
|
8
|
+
|
9
|
+
4.0.3
|
10
|
+
-----
|
11
|
+
- [32](https://github.com/Shopify/graphql-metrics/pull/32) Split validate and analyze_query tracer events (encompasses #30).
|
12
|
+
- [30](https://github.com/Shopify/graphql-metrics/pull/30) Handle queries that have already been parsed (thank you @jturkel).
|
13
|
+
- [29](https://github.com/Shopify/graphql-metrics/pull/29) Remove runtime dependency on activesupport (thank you @jturkel).
|
14
|
+
|
15
|
+
4.0.2
|
16
|
+
-----
|
17
|
+
- [25](https://github.com/Shopify/graphql-metrics/pull/25) Safely handle interrupted runtime metrics.
|
18
|
+
|
19
|
+
4.0.1
|
20
|
+
-----
|
21
|
+
- [24](https://github.com/Shopify/graphql-metrics/pull/24) Safely call `arguments_for` to handle arguments which may
|
22
|
+
raise `ExecutionError`s in their `prepare` methods.
|
23
|
+
|
1
24
|
4.0.0
|
2
25
|
-----
|
3
26
|
- [23](https://github.com/Shopify/graphql-metrics/pull/23) graphql-ruby 1.10.8+ compatibility
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -5,6 +5,8 @@
|
|
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.
|
7
7
|
|
8
|
+
Be sure to read the [CHANGELOG](CHANGELOG.md) to stay updated on feature additions, breaking changes made to this gem.
|
9
|
+
|
8
10
|
## Installation
|
9
11
|
|
10
12
|
Add this line to your application's Gemfile:
|
@@ -42,11 +44,18 @@ etc.
|
|
42
44
|
|
43
45
|
What you do with these captured metrics is up to you!
|
44
46
|
|
47
|
+
**NOTE**: Runtime metrics on for queries (like `query_duration`, `parsing_start_time_offset` etc.) as well as field
|
48
|
+
resolver timings (like `resolver_timings`, `lazy_resolver_timings`) may not be present in the extracted `metrics` hash,
|
49
|
+
even if you opt to collect them by using `GraphQL::Metrics::Analyzer` and `GraphQL::Metrics::Tracer`.
|
50
|
+
|
51
|
+
More specifically, if any non-`graphql-ruby` gem-related exceptions occur in your application during query document
|
52
|
+
parsing and validation runtime metrics will not be added to the `metrics` hash.
|
53
|
+
|
45
54
|
### Define your own analyzer subclass
|
46
55
|
|
47
56
|
```ruby
|
48
|
-
class
|
49
|
-
ANALYZER_NAMESPACE = :
|
57
|
+
class SimpleAnalyzer < GraphQL::Metrics::Analyzer
|
58
|
+
ANALYZER_NAMESPACE = :simple_analyzer_namespace
|
50
59
|
|
51
60
|
def initialize(query_or_multiplex)
|
52
61
|
super
|
@@ -69,6 +78,8 @@ What you do with these captured metrics is up to you!
|
|
69
78
|
# parsing_duration: 0.0008190000080503523,
|
70
79
|
# validation_start_time_offset: 0.0030819999519735575,
|
71
80
|
# validation_duration: 0.01704599999357015,
|
81
|
+
# analysis_start_time_offset: 0.0010339999571442604,
|
82
|
+
# analysis_duration: 0.0008190000080503523,
|
72
83
|
# }
|
73
84
|
#
|
74
85
|
# You can use these metrics to track high-level query performance, along with any other details you wish to
|
@@ -173,7 +184,7 @@ class Schema < GraphQL::Schema
|
|
173
184
|
use GraphQL::Analysis::AST # Required.
|
174
185
|
|
175
186
|
query_analyzer SimpleAnalyzer
|
176
|
-
|
187
|
+
|
177
188
|
instrument :query, GraphQL::Metrics::Instrumentation.new # Both of these are required if either is used.
|
178
189
|
tracer GraphQL::Metrics::Tracer.new # <-- Note!
|
179
190
|
|
@@ -212,10 +223,11 @@ your application as intended, here's a breakdown of the order of execution of th
|
|
212
223
|
|
213
224
|
When used as instrumentation, an analyzer and tracing, the order of execution is:
|
214
225
|
|
215
|
-
* Tracer.
|
226
|
+
* Tracer.setup_tracing
|
216
227
|
* Tracer.capture_parsing_time
|
217
228
|
* Instrumentation.before_query (context setup)
|
218
|
-
* Tracer.capture_validation_time
|
229
|
+
* Tracer.capture_validation_time
|
230
|
+
* Tracer.capture_analysis_time
|
219
231
|
* Analyzer#initialize (bit more context setup, instance vars setup)
|
220
232
|
* Analyzer#result
|
221
233
|
* Tracer.trace_field (n times)
|
data/RELEASING
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Releasing graphql-metrics
|
2
|
+
|
3
|
+
1. Check the Semantic Versioning page for info on how to version the new release: http://semver.org
|
4
|
+
2. Update VERSION in lib/graphql/metrics/version.rb
|
5
|
+
3. Add a CHANGELOG entry for the new release
|
6
|
+
4. Commit the changes with a commit message like "Packaging for release X.Y.Z"
|
7
|
+
5. Tag the release with the version (Leave REV blank for HEAD or provide a SHA)
|
8
|
+
$ git tag vX.Y.Z REV
|
9
|
+
6. Push out the changes
|
10
|
+
$ git push
|
11
|
+
7. Push out the tags
|
12
|
+
$ git push --tags
|
13
|
+
8. Publish the gem
|
14
|
+
$ gem build graphql_metrics.gemspec
|
15
|
+
$ gem push graphql-metrics-X.Y.Z.gem
|
data/lib/graphql/metrics.rb
CHANGED
@@ -28,6 +28,8 @@ module GraphQL
|
|
28
28
|
PARSING_DURATION = :parsing_duration
|
29
29
|
VALIDATION_START_TIME_OFFSET = :validation_start_time_offset
|
30
30
|
VALIDATION_DURATION = :validation_duration
|
31
|
+
ANALYSIS_START_TIME_OFFSET = :analysis_start_time_offset
|
32
|
+
ANALYSIS_DURATION = :analysis_duration
|
31
33
|
INLINE_FIELD_TIMINGS = :inline_field_timings
|
32
34
|
LAZY_FIELD_TIMINGS = :lazy_field_timings
|
33
35
|
|
@@ -35,15 +35,22 @@ module GraphQL
|
|
35
35
|
return if visitor.field_definition.introspection?
|
36
36
|
return if query.context[SKIP_FIELD_AND_ARGUMENT_METRICS]
|
37
37
|
|
38
|
-
|
38
|
+
# Arguments can raise execution errors within their `prepare` methods
|
39
|
+
# which aren't properly handled during analysis so we have to handle
|
40
|
+
# them ourselves safely and return `nil`.
|
41
|
+
argument_values = begin
|
42
|
+
query.arguments_for(node, visitor.field_definition)
|
43
|
+
rescue ::GraphQL::ExecutionError
|
44
|
+
nil
|
45
|
+
end
|
39
46
|
|
40
|
-
extract_arguments(argument_values, visitor.field_definition)
|
47
|
+
extract_arguments(argument_values, visitor.field_definition) if argument_values
|
41
48
|
|
42
49
|
static_metrics = {
|
43
50
|
field_name: node.name,
|
44
51
|
return_type_name: visitor.type_definition.graphql_name,
|
45
52
|
parent_type_name: visitor.parent_type_definition.graphql_name,
|
46
|
-
deprecated: visitor.field_definition.deprecation_reason.
|
53
|
+
deprecated: !visitor.field_definition.deprecation_reason.nil?,
|
47
54
|
path: visitor.response_path,
|
48
55
|
}
|
49
56
|
|
@@ -54,17 +61,20 @@ module GraphQL
|
|
54
61
|
end
|
55
62
|
end
|
56
63
|
|
57
|
-
def
|
64
|
+
def extract_fields(with_runtime_metrics: true)
|
58
65
|
return if query.context[SKIP_FIELD_AND_ARGUMENT_METRICS]
|
59
66
|
|
60
67
|
ns = query.context.namespace(CONTEXT_NAMESPACE)
|
61
68
|
|
62
69
|
@static_field_metrics.each do |static_metrics|
|
63
|
-
resolver_timings = ns[GraphQL::Metrics::INLINE_FIELD_TIMINGS][static_metrics[:path]]
|
64
|
-
lazy_resolver_timings = ns[GraphQL::Metrics::LAZY_FIELD_TIMINGS][static_metrics[:path]]
|
65
70
|
|
66
|
-
|
67
|
-
|
71
|
+
if with_runtime_metrics
|
72
|
+
resolver_timings = ns[GraphQL::Metrics::INLINE_FIELD_TIMINGS][static_metrics[:path]]
|
73
|
+
lazy_resolver_timings = ns[GraphQL::Metrics::LAZY_FIELD_TIMINGS][static_metrics[:path]]
|
74
|
+
|
75
|
+
static_metrics[:resolver_timings] = resolver_timings || []
|
76
|
+
static_metrics[:lazy_resolver_timings] = lazy_resolver_timings || []
|
77
|
+
end
|
68
78
|
|
69
79
|
field_extracted(static_metrics)
|
70
80
|
end
|
@@ -25,23 +25,33 @@ module GraphQL
|
|
25
25
|
return if query.context[GraphQL::Metrics::SKIP_GRAPHQL_METRICS_ANALYSIS]
|
26
26
|
|
27
27
|
ns = query.context.namespace(CONTEXT_NAMESPACE)
|
28
|
+
analyzer = ns[GraphQL::Metrics::ANALYZER_INSTANCE_KEY]
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
if runtime_metrics_interrupted?(ns)
|
31
|
+
# If runtime metrics were interrupted, then it's most likely that the application raised an exception and that
|
32
|
+
# query parsing (which is instrumented by GraphQL::Metrics::Tracer) was abruptly stopped.
|
33
|
+
#
|
34
|
+
# In this scenario, we still attempt to log whatever static query metrics we've collected, with runtime
|
35
|
+
# metrics (like query, field resolver timings) excluded.
|
36
|
+
analyzer.extract_fields(with_runtime_metrics: false)
|
37
|
+
analyzer.extract_query
|
38
|
+
else
|
39
|
+
query_duration = GraphQL::Metrics.current_time_monotonic - ns[GraphQL::Metrics::QUERY_START_TIME_MONOTONIC]
|
32
40
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
+
runtime_query_metrics = {
|
42
|
+
query_start_time: ns[GraphQL::Metrics::QUERY_START_TIME],
|
43
|
+
query_duration: query_duration,
|
44
|
+
parsing_start_time_offset: ns[GraphQL::Metrics::PARSING_START_TIME_OFFSET],
|
45
|
+
parsing_duration: ns[GraphQL::Metrics::PARSING_DURATION],
|
46
|
+
validation_start_time_offset: ns[GraphQL::Metrics::VALIDATION_START_TIME_OFFSET],
|
47
|
+
validation_duration: ns[GraphQL::Metrics::VALIDATION_DURATION],
|
48
|
+
analysis_start_time_offset: ns[GraphQL::Metrics::ANALYSIS_START_TIME_OFFSET],
|
49
|
+
analysis_duration: ns[GraphQL::Metrics::ANALYSIS_DURATION],
|
50
|
+
}
|
41
51
|
|
42
|
-
|
43
|
-
|
44
|
-
|
52
|
+
analyzer.extract_fields
|
53
|
+
analyzer.extract_query(runtime_query_metrics: runtime_query_metrics)
|
54
|
+
end
|
45
55
|
end
|
46
56
|
|
47
57
|
private
|
@@ -51,6 +61,12 @@ module GraphQL
|
|
51
61
|
# which did not reject "empty" documents in its parser.
|
52
62
|
query.valid? && !query.selected_operation.nil?
|
53
63
|
end
|
64
|
+
|
65
|
+
def runtime_metrics_interrupted?(context_namespace)
|
66
|
+
# NOTE: The start time stored at `ns[GraphQL::Metrics::QUERY_START_TIME_MONOTONIC]` is captured during query
|
67
|
+
# parsing, which occurs before `Instrumentation#before_query`.
|
68
|
+
context_namespace.key?(GraphQL::Metrics::QUERY_START_TIME_MONOTONIC) == false
|
69
|
+
end
|
54
70
|
end
|
55
71
|
end
|
56
72
|
end
|
@@ -4,9 +4,11 @@ module GraphQL
|
|
4
4
|
module Metrics
|
5
5
|
class Tracer
|
6
6
|
# NOTE: These constants come from the graphql ruby gem.
|
7
|
-
|
7
|
+
GRAPHQL_GEM_LEX_KEY = 'lex'
|
8
|
+
GRAPHQL_GEM_EXECUTE_MULTIPLEX_KEY = 'execute_multiplex'
|
8
9
|
GRAPHQL_GEM_PARSING_KEY = 'parse'
|
9
|
-
|
10
|
+
GRAPHQL_GEM_VALIDATION_KEY = 'validate'
|
11
|
+
GRAPHQL_GEM_ANALYZE_KEY = 'analyze_query'
|
10
12
|
GRAPHQL_GEM_TRACING_FIELD_KEYS = [
|
11
13
|
GRAPHQL_GEM_TRACING_FIELD_KEY = 'execute_field',
|
12
14
|
GRAPHQL_GEM_TRACING_LAZY_FIELD_KEY = 'execute_field_lazy'
|
@@ -22,21 +24,23 @@ module GraphQL
|
|
22
24
|
# NOTE: Not all tracing events are handled here, but those that are are handled in this case statement in
|
23
25
|
# chronological order.
|
24
26
|
case key
|
25
|
-
when
|
26
|
-
return
|
27
|
+
when GRAPHQL_GEM_LEX_KEY
|
28
|
+
return setup_tracing(&block)
|
29
|
+
when GRAPHQL_GEM_EXECUTE_MULTIPLEX_KEY
|
30
|
+
return setup_tracing(&block)
|
27
31
|
when GRAPHQL_GEM_PARSING_KEY
|
28
32
|
return capture_parsing_time(&block)
|
29
|
-
when
|
33
|
+
when GRAPHQL_GEM_VALIDATION_KEY
|
30
34
|
context = possible_context
|
31
|
-
|
32
|
-
return yield unless context.query.valid?
|
33
35
|
return capture_validation_time(context, &block)
|
36
|
+
when GRAPHQL_GEM_ANALYZE_KEY
|
37
|
+
context = possible_context
|
38
|
+
return capture_analysis_time(context, &block)
|
39
|
+
|
34
40
|
when *GRAPHQL_GEM_TRACING_FIELD_KEYS
|
35
41
|
return yield if data[:query].context[SKIP_FIELD_AND_ARGUMENT_METRICS]
|
36
42
|
return yield unless GraphQL::Metrics.timings_capture_enabled?(data[:query].context)
|
37
43
|
|
38
|
-
pre_context = nil
|
39
|
-
|
40
44
|
context_key = case key
|
41
45
|
when GRAPHQL_GEM_TRACING_FIELD_KEY
|
42
46
|
GraphQL::Metrics::INLINE_FIELD_TIMINGS
|
@@ -52,15 +56,30 @@ module GraphQL
|
|
52
56
|
|
53
57
|
private
|
54
58
|
|
59
|
+
PreContext = Struct.new(
|
60
|
+
:query_start_time,
|
61
|
+
:query_start_time_monotonic,
|
62
|
+
:parsing_start_time_offset,
|
63
|
+
:parsing_duration
|
64
|
+
) do
|
65
|
+
def reset_parsing_timings
|
66
|
+
self[:parsing_start_time_offset] = nil
|
67
|
+
self[:parsing_duration] = nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
55
71
|
def pre_context
|
56
72
|
# NOTE: This is used to store timings from lexing, parsing, validation, before we have a context to store
|
57
73
|
# values in. Uses thread-safe Concurrent::ThreadLocalVar to store a set of values per thread.
|
58
|
-
@pre_context ||= Concurrent::ThreadLocalVar.new(
|
74
|
+
@pre_context ||= Concurrent::ThreadLocalVar.new(PreContext.new)
|
75
|
+
@pre_context.value
|
59
76
|
end
|
60
77
|
|
61
|
-
def
|
62
|
-
pre_context.
|
63
|
-
|
78
|
+
def setup_tracing
|
79
|
+
return yield if pre_context.query_start_time
|
80
|
+
|
81
|
+
pre_context.query_start_time = GraphQL::Metrics.current_time
|
82
|
+
pre_context.query_start_time_monotonic = GraphQL::Metrics.current_time_monotonic
|
64
83
|
|
65
84
|
yield
|
66
85
|
end
|
@@ -68,24 +87,41 @@ module GraphQL
|
|
68
87
|
def capture_parsing_time
|
69
88
|
timed_result = GraphQL::Metrics.time { yield }
|
70
89
|
|
71
|
-
pre_context.
|
72
|
-
pre_context.
|
90
|
+
pre_context.parsing_start_time_offset = timed_result.start_time
|
91
|
+
pre_context.parsing_duration = timed_result.duration
|
73
92
|
|
74
93
|
timed_result.result
|
75
94
|
end
|
76
95
|
|
77
96
|
def capture_validation_time(context)
|
78
|
-
|
97
|
+
if pre_context.parsing_duration.nil?
|
98
|
+
pre_context.parsing_start_time_offset = 0.0
|
99
|
+
pre_context.parsing_duration = 0.0
|
100
|
+
end
|
101
|
+
|
102
|
+
timed_result = GraphQL::Metrics.time(pre_context.query_start_time_monotonic) { yield }
|
79
103
|
|
80
104
|
ns = context.namespace(CONTEXT_NAMESPACE)
|
81
|
-
previous_validation_duration = ns[GraphQL::Metrics::VALIDATION_DURATION] || 0
|
82
105
|
|
83
|
-
ns[QUERY_START_TIME] = pre_context.
|
84
|
-
ns[QUERY_START_TIME_MONOTONIC] = pre_context.
|
85
|
-
ns[PARSING_START_TIME_OFFSET] = pre_context.
|
86
|
-
ns[PARSING_DURATION] = pre_context.
|
106
|
+
ns[QUERY_START_TIME] = pre_context.query_start_time
|
107
|
+
ns[QUERY_START_TIME_MONOTONIC] = pre_context.query_start_time_monotonic
|
108
|
+
ns[PARSING_START_TIME_OFFSET] = pre_context.parsing_start_time_offset
|
109
|
+
ns[PARSING_DURATION] = pre_context.parsing_duration
|
87
110
|
ns[VALIDATION_START_TIME_OFFSET] = timed_result.time_since_offset
|
88
|
-
ns[VALIDATION_DURATION] = timed_result.duration
|
111
|
+
ns[VALIDATION_DURATION] = timed_result.duration
|
112
|
+
|
113
|
+
pre_context.reset_parsing_timings
|
114
|
+
|
115
|
+
timed_result.result
|
116
|
+
end
|
117
|
+
|
118
|
+
def capture_analysis_time(context)
|
119
|
+
ns = context.namespace(CONTEXT_NAMESPACE)
|
120
|
+
|
121
|
+
timed_result = GraphQL::Metrics.time(ns[QUERY_START_TIME_MONOTONIC]) { yield }
|
122
|
+
|
123
|
+
ns[ANALYSIS_START_TIME_OFFSET] = timed_result.time_since_offset
|
124
|
+
ns[ANALYSIS_DURATION] = timed_result.duration
|
89
125
|
|
90
126
|
timed_result.result
|
91
127
|
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: 4.0.
|
4
|
+
version: 4.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher Butcher
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -212,6 +212,7 @@ files:
|
|
212
212
|
- Gemfile.lock
|
213
213
|
- LICENSE.txt
|
214
214
|
- README.md
|
215
|
+
- RELEASING
|
215
216
|
- Rakefile
|
216
217
|
- bin/console
|
217
218
|
- bin/setup
|