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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbb0d2e1dfd6cf0d08b8c47afedddc2025407dd11c482e07df8e8e86ddbba329
4
- data.tar.gz: 5dfeeb2067df244cf6fe6d755cc6ff0d8995f423ad791f6617cc3368b2d0ac6a
3
+ metadata.gz: e35ba7c6756d4d431a32911e2bf8452f8f825ed1ff6d90c17ad0a0cb13f4d5f9
4
+ data.tar.gz: 1bb21be1384170bca7151a05ae36299c70193ca570bece6a7cdeb59f67e9ed57
5
5
  SHA512:
6
- metadata.gz: bf14bbede098cf4dbc7a6a9e4645d29c084655763a6b0ed7d51415f9b2efd62672f45228f6f0b3488fdfd6e4067ac4167c74e11b9abb8d59002ccf8709412737
7
- data.tar.gz: 6c643c41faaf2246fb7770a44f24af71435211652f804f0355c485b7444112e5286763dc43e89bd113694fb887a7507b62952348e5d10cad07417786be9000e0
6
+ metadata.gz: ba5a5d9fe0b038410260a9fc7d05472a2a7b3012194bc727226429920225f93f617ce353f8121e07b0a27b6f13899c8895ef26bf37e9856df05482c692042603
7
+ data.tar.gz: baec02caa8599a575912c7e3e6696d11ac9bc0e88ebc5ed270eeaf7c0d8cf800bb5d1d48276f513a69c8921c43ef821a9f1239290a86027b9c104495e79c1ca3
@@ -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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql-metrics (4.0.0)
4
+ graphql-metrics (4.0.5)
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,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.setup_tracing_before_lexing
226
+ * Tracer.setup_tracing
216
227
  * Tracer.capture_parsing_time
217
228
  * Instrumentation.before_query (context setup)
218
- * Tracer.capture_validation_time (twice, once for `analyze_query`, then `analyze_multiplex`)
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)
@@ -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
@@ -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
- argument_values = query.arguments_for(node, visitor.field_definition)
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.present?,
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 extract_fields_with_runtime_metrics
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
- static_metrics[:resolver_timings] = resolver_timings || []
67
- 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
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
- # 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
+ }
41
51
 
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)
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
- GRAPHQL_GEM_LEXING_KEY = 'lex'
7
+ GRAPHQL_GEM_LEX_KEY = 'lex'
8
+ GRAPHQL_GEM_EXECUTE_MULTIPLEX_KEY = 'execute_multiplex'
8
9
  GRAPHQL_GEM_PARSING_KEY = 'parse'
9
- GRAPHQL_GEM_VALIDATION_KEYS = ['validate', 'analyze_query']
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 GRAPHQL_GEM_LEXING_KEY
26
- return setup_tracing_before_lexing(&block)
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 *GRAPHQL_GEM_VALIDATION_KEYS
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(OpenStruct.new)
74
+ @pre_context ||= Concurrent::ThreadLocalVar.new(PreContext.new)
75
+ @pre_context.value
59
76
  end
60
77
 
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
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.value.parsing_start_time_offset = timed_result.start_time
72
- pre_context.value.parsing_duration = timed_result.duration
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
- timed_result = GraphQL::Metrics.time(pre_context.value.query_start_time_monotonic) { yield }
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.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
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 + previous_validation_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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Metrics
5
- VERSION = "4.0.0"
5
+ VERSION = "4.0.5"
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.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-04-29 00:00:00.000000000 Z
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