graphql-metrics 4.0.1 → 4.0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45de407a0b5642ad74d27cdb4ab00635590a38d8b719fc448a3e94d6244d7daf
4
- data.tar.gz: cae841292ce06f25031974127696594b426b41f469e4f7f74f37f1f3a8c8f521
3
+ metadata.gz: 794d916d7f0b35ed8c50814c7cee54af5881a9fa5102eb361d1486fde665b413
4
+ data.tar.gz: 831cd8c26e771fc5e97e483f5cda86bce63311c0a31a9d12a020cdf5be808241
5
5
  SHA512:
6
- metadata.gz: 861b2728422d3f8c314024084497ef761c4ca83491f3967ccffa2d540ced0de524d318a380f7966b6555a539fd30ffdfaec83cafee42f020813875148434f734
7
- data.tar.gz: 83bde1129c7492cd92ffc097e3072275f179262530a5fccbe6e61b5c485a08bb231a3af8e29ddf88a6f0f3cdfdf5bd7101f0266e042afe37d19232a07195d922
6
+ metadata.gz: aed42769f9dcbcb445e0e9b8ec9858d05c917542fa0c588bcc587d19c2fbc8bafd2d1fcf65f4d7742cd498a2ac1d39b66c6955827efa4da3582a44980f2346e1
7
+ data.tar.gz: 908bc4cf5d126d71e715379cd2346308b5e7ed17230ad4733eff2614827625768d0ec1186da282628c91c6ab3fe7dd3a92adc6b6e9e548b202eb3b9507f6a8fd
@@ -1,3 +1,25 @@
1
+ 4.0.6
2
+ -----
3
+ - [35](https://github.com/Shopify/graphql-metrics/pull/35) Fix query start time, start time offset bugs.
4
+
5
+ 4.0.5
6
+ -----
7
+ - [34](https://github.com/Shopify/graphql-metrics/pull/34) Fix default of pre-parsed query `parsing_duration` to be Float (`0.0`) rather than Integer (`0`).
8
+
9
+ 4.0.4
10
+ -----
11
+ - [33](https://github.com/Shopify/graphql-metrics/pull/33) Setup tracing using lex or execute_multiplex tracer events.
12
+
13
+ 4.0.3
14
+ -----
15
+ - [32](https://github.com/Shopify/graphql-metrics/pull/32) Split validate and analyze_query tracer events (encompasses #30).
16
+ - [30](https://github.com/Shopify/graphql-metrics/pull/30) Handle queries that have already been parsed (thank you @jturkel).
17
+ - [29](https://github.com/Shopify/graphql-metrics/pull/29) Remove runtime dependency on activesupport (thank you @jturkel).
18
+
19
+ 4.0.2
20
+ -----
21
+ - [25](https://github.com/Shopify/graphql-metrics/pull/25) Safely handle interrupted runtime metrics.
22
+
1
23
  4.0.1
2
24
  -----
3
25
  - [24](https://github.com/Shopify/graphql-metrics/pull/24) Safely call `arguments_for` to handle arguments which may
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql-metrics (4.0.1)
4
+ graphql-metrics (4.0.6)
5
5
  concurrent-ruby (~> 1.1.0)
6
6
  graphql (>= 1.10.8)
7
7
 
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 CaptureAllMetricsAnalyzer < GraphQL::Metrics::Analyzer
49
- ANALYZER_NAMESPACE = :capture_all_metrics_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,9 @@ 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,
83
+ # multiplex_start_time: 0.0008190000080503523,
72
84
  # }
73
85
  #
74
86
  # You can use these metrics to track high-level query performance, along with any other details you wish to
@@ -173,7 +185,7 @@ class Schema < GraphQL::Schema
173
185
  use GraphQL::Analysis::AST # Required.
174
186
 
175
187
  query_analyzer SimpleAnalyzer
176
-
188
+
177
189
  instrument :query, GraphQL::Metrics::Instrumentation.new # Both of these are required if either is used.
178
190
  tracer GraphQL::Metrics::Tracer.new # <-- Note!
179
191
 
@@ -212,10 +224,11 @@ your application as intended, here's a breakdown of the order of execution of th
212
224
 
213
225
  When used as instrumentation, an analyzer and tracing, the order of execution is:
214
226
 
215
- * Tracer.setup_tracing_before_lexing
227
+ * Tracer.setup_tracing
216
228
  * Tracer.capture_parsing_time
217
229
  * Instrumentation.before_query (context setup)
218
- * Tracer.capture_validation_time (twice, once for `analyze_query`, then `analyze_multiplex`)
230
+ * Tracer.capture_validation_time
231
+ * Tracer.capture_analysis_time
219
232
  * Analyzer#initialize (bit more context setup, instance vars setup)
220
233
  * Analyzer#result
221
234
  * Tracer.trace_field (n times)
@@ -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
@@ -22,12 +22,16 @@ module GraphQL
22
22
  ANALYZER_INSTANCE_KEY = :analyzer_instance
23
23
 
24
24
  # Context keys to store timings for query phases of execution, field resolver timings.
25
+ MULTIPLEX_START_TIME = :multiplex_start_time
26
+ MULTIPLEX_START_TIME_MONOTONIC = :multiplex_start_time_monotonic
25
27
  QUERY_START_TIME = :query_start_time
26
28
  QUERY_START_TIME_MONOTONIC = :query_start_time_monotonic
27
29
  PARSING_START_TIME_OFFSET = :parsing_start_time_offset
28
30
  PARSING_DURATION = :parsing_duration
29
31
  VALIDATION_START_TIME_OFFSET = :validation_start_time_offset
30
32
  VALIDATION_DURATION = :validation_duration
33
+ ANALYSIS_START_TIME_OFFSET = :analysis_start_time_offset
34
+ ANALYSIS_DURATION = :analysis_duration
31
35
  INLINE_FIELD_TIMINGS = :inline_field_timings
32
36
  LAZY_FIELD_TIMINGS = :lazy_field_timings
33
37
 
@@ -50,7 +50,7 @@ module GraphQL
50
50
  field_name: node.name,
51
51
  return_type_name: visitor.type_definition.graphql_name,
52
52
  parent_type_name: visitor.parent_type_definition.graphql_name,
53
- deprecated: visitor.field_definition.deprecation_reason.present?,
53
+ deprecated: !visitor.field_definition.deprecation_reason.nil?,
54
54
  path: visitor.response_path,
55
55
  }
56
56
 
@@ -61,17 +61,20 @@ module GraphQL
61
61
  end
62
62
  end
63
63
 
64
- def extract_fields_with_runtime_metrics
64
+ def extract_fields(with_runtime_metrics: true)
65
65
  return if query.context[SKIP_FIELD_AND_ARGUMENT_METRICS]
66
66
 
67
67
  ns = query.context.namespace(CONTEXT_NAMESPACE)
68
68
 
69
69
  @static_field_metrics.each do |static_metrics|
70
- resolver_timings = ns[GraphQL::Metrics::INLINE_FIELD_TIMINGS][static_metrics[:path]]
71
- lazy_resolver_timings = ns[GraphQL::Metrics::LAZY_FIELD_TIMINGS][static_metrics[:path]]
72
70
 
73
- static_metrics[:resolver_timings] = resolver_timings || []
74
- static_metrics[:lazy_resolver_timings] = lazy_resolver_timings || []
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
75
78
 
76
79
  field_extracted(static_metrics)
77
80
  end
@@ -25,23 +25,34 @@ 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
- # NOTE: The start time stored at `ns[GraphQL::Metrics::QUERY_START_TIME_MONOTONIC]` is captured during query
30
- # parsing, which occurs before `Instrumentation#before_query`.
31
- query_duration = GraphQL::Metrics.current_time_monotonic - ns[GraphQL::Metrics::QUERY_START_TIME_MONOTONIC]
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
- runtime_query_metrics = {
34
- query_start_time: ns[GraphQL::Metrics::QUERY_START_TIME],
35
- query_duration: query_duration,
36
- parsing_start_time_offset: ns[GraphQL::Metrics::PARSING_START_TIME_OFFSET],
37
- parsing_duration: ns[GraphQL::Metrics::PARSING_DURATION],
38
- validation_start_time_offset: ns[GraphQL::Metrics::VALIDATION_START_TIME_OFFSET],
39
- validation_duration: ns[GraphQL::Metrics::VALIDATION_DURATION],
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
+ multiplex_start_time: ns[GraphQL::Metrics::MULTIPLEX_START_TIME],
51
+ }
41
52
 
42
- analyzer = ns[GraphQL::Metrics::ANALYZER_INSTANCE_KEY]
43
- analyzer.extract_fields_with_runtime_metrics
44
- analyzer.extract_query(runtime_query_metrics: runtime_query_metrics)
53
+ analyzer.extract_fields
54
+ analyzer.extract_query(runtime_query_metrics: runtime_query_metrics)
55
+ end
45
56
  end
46
57
 
47
58
  private
@@ -51,6 +62,12 @@ module GraphQL
51
62
  # which did not reject "empty" documents in its parser.
52
63
  query.valid? && !query.selected_operation.nil?
53
64
  end
65
+
66
+ def runtime_metrics_interrupted?(context_namespace)
67
+ # NOTE: The start time stored at `ns[GraphQL::Metrics::QUERY_START_TIME_MONOTONIC]` is captured during query
68
+ # parsing, which occurs before `Instrumentation#before_query`.
69
+ context_namespace.key?(GraphQL::Metrics::QUERY_START_TIME_MONOTONIC) == false
70
+ end
54
71
  end
55
72
  end
56
73
  end
@@ -4,9 +4,12 @@ module GraphQL
4
4
  module Metrics
5
5
  class Tracer
6
6
  # NOTE: These constants come from the graphql ruby gem.
7
- GRAPHQL_GEM_LEXING_KEY = 'lex'
7
+ GRAPHQL_GEM_EXECUTE_MULTIPLEX_KEY = 'execute_multiplex'
8
8
  GRAPHQL_GEM_PARSING_KEY = 'parse'
9
- GRAPHQL_GEM_VALIDATION_KEYS = ['validate', 'analyze_query']
9
+ GRAPHQL_GEM_VALIDATION_KEY = 'validate'
10
+ GRAPHQL_GEM_ANALYZE_MULTIPLEX_KEY = 'analyze_multiplex'
11
+ GRAPHQL_GEM_ANALYZE_QUERY_KEY = 'analyze_query'
12
+ GRAPHQL_GEM_EXECUTE_QUERY_KEY = 'execute_query'
10
13
  GRAPHQL_GEM_TRACING_FIELD_KEYS = [
11
14
  GRAPHQL_GEM_TRACING_FIELD_KEY = 'execute_field',
12
15
  GRAPHQL_GEM_TRACING_LAZY_FIELD_KEY = 'execute_field_lazy'
@@ -22,21 +25,28 @@ module GraphQL
22
25
  # NOTE: Not all tracing events are handled here, but those that are are handled in this case statement in
23
26
  # chronological order.
24
27
  case key
25
- when GRAPHQL_GEM_LEXING_KEY
26
- return setup_tracing_before_lexing(&block)
28
+ when GRAPHQL_GEM_EXECUTE_MULTIPLEX_KEY
29
+ return capture_multiplex_start_time(&block)
27
30
  when GRAPHQL_GEM_PARSING_KEY
28
31
  return capture_parsing_time(&block)
29
- when *GRAPHQL_GEM_VALIDATION_KEYS
32
+ when GRAPHQL_GEM_VALIDATION_KEY
30
33
  context = possible_context
31
-
32
- return yield unless context.query.valid?
33
34
  return capture_validation_time(context, &block)
35
+ when GRAPHQL_GEM_ANALYZE_MULTIPLEX_KEY
36
+ # Ensures that we reset potentially long-lived PreContext objects between multiplexs. We reset at this point
37
+ # since all parsing and validation will be done by this point, and a GraphQL::Query::Context will exist.
38
+ pre_context.reset
39
+ return yield
40
+ when GRAPHQL_GEM_ANALYZE_QUERY_KEY
41
+ context = possible_context
42
+ return capture_analysis_time(context, &block)
43
+ when GRAPHQL_GEM_EXECUTE_QUERY_KEY
44
+ context = possible_context
45
+ capture_query_start_time(context, &block)
34
46
  when *GRAPHQL_GEM_TRACING_FIELD_KEYS
35
47
  return yield if data[:query].context[SKIP_FIELD_AND_ARGUMENT_METRICS]
36
48
  return yield unless GraphQL::Metrics.timings_capture_enabled?(data[:query].context)
37
49
 
38
- pre_context = nil
39
-
40
50
  context_key = case key
41
51
  when GRAPHQL_GEM_TRACING_FIELD_KEY
42
52
  GraphQL::Metrics::INLINE_FIELD_TIMINGS
@@ -52,44 +62,90 @@ module GraphQL
52
62
 
53
63
  private
54
64
 
65
+ PreContext = Struct.new(
66
+ :multiplex_start_time,
67
+ :multiplex_start_time_monotonic,
68
+ :parsing_start_time_offset,
69
+ :parsing_duration
70
+ ) do
71
+ def reset
72
+ self[:multiplex_start_time] = nil
73
+ self[:multiplex_start_time_monotonic] = nil
74
+ self[:parsing_start_time_offset] = nil
75
+ self[:parsing_duration] = nil
76
+ end
77
+ end
78
+
55
79
  def pre_context
56
80
  # NOTE: This is used to store timings from lexing, parsing, validation, before we have a context to store
57
81
  # values in. Uses thread-safe Concurrent::ThreadLocalVar to store a set of values per thread.
58
- @pre_context ||= Concurrent::ThreadLocalVar.new(OpenStruct.new)
82
+ @pre_context ||= Concurrent::ThreadLocalVar.new(PreContext.new)
83
+ @pre_context.value
59
84
  end
60
85
 
61
- def setup_tracing_before_lexing
62
- pre_context.value.query_start_time = GraphQL::Metrics.current_time
63
- pre_context.value.query_start_time_monotonic = GraphQL::Metrics.current_time_monotonic
86
+ def capture_multiplex_start_time
87
+ pre_context.multiplex_start_time = GraphQL::Metrics.current_time
88
+ pre_context.multiplex_start_time_monotonic = GraphQL::Metrics.current_time_monotonic
64
89
 
65
90
  yield
66
91
  end
67
92
 
68
93
  def capture_parsing_time
69
- timed_result = GraphQL::Metrics.time { yield }
94
+ # GraphQL::Query#result fires `parse` before the `execute_multiplex` event, so sometimes
95
+ # `pre_context.multiplex_start_time_monotonic` isn't set.
96
+ parsing_offset_time = pre_context.multiplex_start_time_monotonic || GraphQL::Metrics.current_time_monotonic
97
+ timed_result = GraphQL::Metrics.time(parsing_offset_time) { yield }
70
98
 
71
- pre_context.value.parsing_start_time_offset = timed_result.start_time
72
- pre_context.value.parsing_duration = timed_result.duration
99
+ pre_context.parsing_start_time_offset = timed_result.start_time
100
+ pre_context.parsing_duration = timed_result.duration
73
101
 
74
102
  timed_result.result
75
103
  end
76
104
 
105
+ # Also consolidates parsing timings (if any) from the `pre_context`
77
106
  def capture_validation_time(context)
78
- timed_result = GraphQL::Metrics.time(pre_context.value.query_start_time_monotonic) { yield }
107
+ if pre_context.parsing_duration.nil?
108
+ # Queries may already be parsed before execution (whether a single query or multiplex).
109
+ # If we don't have a parsing start time, use the multiplex start time.
110
+ pre_context.parsing_start_time_offset = pre_context.multiplex_start_time
111
+
112
+ # If we don't have a duration, consider parsing to have been instantaneous.
113
+ pre_context.parsing_duration = 0.0
114
+ end
115
+
116
+ timed_result = GraphQL::Metrics.time(pre_context.multiplex_start_time_monotonic) { yield }
79
117
 
80
118
  ns = context.namespace(CONTEXT_NAMESPACE)
81
- previous_validation_duration = ns[GraphQL::Metrics::VALIDATION_DURATION] || 0
82
119
 
83
- ns[QUERY_START_TIME] = pre_context.value.query_start_time
84
- ns[QUERY_START_TIME_MONOTONIC] = pre_context.value.query_start_time_monotonic
85
- ns[PARSING_START_TIME_OFFSET] = pre_context.value.parsing_start_time_offset
86
- ns[PARSING_DURATION] = pre_context.value.parsing_duration
120
+ ns[MULTIPLEX_START_TIME] = pre_context.multiplex_start_time
121
+ ns[MULTIPLEX_START_TIME_MONOTONIC] = pre_context.multiplex_start_time_monotonic
122
+ ns[PARSING_START_TIME_OFFSET] = pre_context.parsing_start_time_offset
123
+ ns[PARSING_DURATION] = pre_context.parsing_duration
87
124
  ns[VALIDATION_START_TIME_OFFSET] = timed_result.time_since_offset
88
- ns[VALIDATION_DURATION] = timed_result.duration + previous_validation_duration
125
+ ns[VALIDATION_DURATION] = timed_result.duration
89
126
 
90
127
  timed_result.result
91
128
  end
92
129
 
130
+ def capture_analysis_time(context)
131
+ ns = context.namespace(CONTEXT_NAMESPACE)
132
+
133
+ timed_result = GraphQL::Metrics.time(ns[MULTIPLEX_START_TIME_MONOTONIC]) { yield }
134
+
135
+ ns[ANALYSIS_START_TIME_OFFSET] = timed_result.time_since_offset
136
+ ns[ANALYSIS_DURATION] = timed_result.duration
137
+
138
+ timed_result.result
139
+ end
140
+
141
+ def capture_query_start_time(context)
142
+ ns = context.namespace(CONTEXT_NAMESPACE)
143
+ ns[QUERY_START_TIME] = GraphQL::Metrics.current_time
144
+ ns[QUERY_START_TIME_MONOTONIC] = GraphQL::Metrics.current_time_monotonic
145
+
146
+ yield
147
+ end
148
+
93
149
  def trace_field(context_key, data)
94
150
  ns = data[:query].context.namespace(CONTEXT_NAMESPACE)
95
151
  query_start_time_monotonic = ns[GraphQL::Metrics::QUERY_START_TIME_MONOTONIC]
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Metrics
5
- VERSION = "4.0.1"
5
+ VERSION = "4.0.6"
6
6
  end
7
7
  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.1
4
+ version: 4.0.6
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-06-05 00:00:00.000000000 Z
11
+ date: 2020-08-20 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