graphql-metrics 0.1.0 → 1.1.2
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/.gitignore +1 -0
- data/CODE_OF_CONDUCT.md +1 -1
- data/Gemfile +4 -0
- data/Gemfile.lock +39 -4
- data/LICENSE.txt +1 -1
- data/README.md +105 -8
- data/graphql_metrics.gemspec +15 -5
- data/lib/graphql_metrics.rb +4 -4
- data/lib/graphql_metrics/extractor.rb +296 -0
- data/lib/graphql_metrics/timed_batch_executor.rb +80 -0
- data/lib/graphql_metrics/version.rb +4 -2
- metadata +109 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 245c973caa0e704220cc9b2363543758171de916
|
4
|
+
data.tar.gz: 0200a7fbbaa3ce77f5f564ee8649ee22a1c9a498
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1077c5445ffa0052f6d8570acd44717550aa09de861461f24f8edd0b55c7eb7d87493fc463b94313f3499ae9b56015e4cfdc3cdd9e876beea7d2b6714569a3ea
|
7
|
+
data.tar.gz: fbc1aea880d0e801334706ddcf39b188d33d26827f856d8bc0f8bdcab04a096b6fc9c9265fa133a01f14075a57e0f2cedba740f1784da58ad9b05d0bfcfbdd0e
|
data/.gitignore
CHANGED
data/CODE_OF_CONDUCT.md
CHANGED
@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
|
|
55
55
|
## Enforcement
|
56
56
|
|
57
57
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
-
reported by contacting the project team
|
58
|
+
reported by contacting the project team via a Github issue on this repo. All
|
59
59
|
complaints will be reviewed and investigated and will result in a response that
|
60
60
|
is deemed necessary and appropriate to the circumstances. The project team is
|
61
61
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,22 +1,57 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
graphql-metrics (
|
4
|
+
graphql-metrics (1.1.2)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
|
9
|
+
activesupport (5.1.6)
|
10
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
11
|
+
i18n (>= 0.7, < 2)
|
12
|
+
minitest (~> 5.1)
|
13
|
+
tzinfo (~> 1.1)
|
14
|
+
coderay (1.1.2)
|
15
|
+
concurrent-ruby (1.0.5)
|
16
|
+
diffy (3.2.1)
|
17
|
+
fakeredis (0.7.0)
|
18
|
+
redis (>= 3.2, < 5.0)
|
19
|
+
graphql (1.8.4)
|
20
|
+
graphql-batch (0.3.9)
|
21
|
+
graphql (>= 0.8, < 2)
|
22
|
+
promise.rb (~> 0.7.2)
|
23
|
+
i18n (1.0.1)
|
24
|
+
concurrent-ruby (~> 1.0)
|
25
|
+
metaclass (0.0.4)
|
26
|
+
method_source (0.9.0)
|
27
|
+
minitest (5.11.3)
|
28
|
+
mocha (1.5.0)
|
29
|
+
metaclass (~> 0.0.1)
|
30
|
+
promise.rb (0.7.4)
|
31
|
+
pry (0.11.3)
|
32
|
+
coderay (~> 1.1.0)
|
33
|
+
method_source (~> 0.9.0)
|
10
34
|
rake (10.5.0)
|
35
|
+
redis (4.0.1)
|
36
|
+
thread_safe (0.3.6)
|
37
|
+
tzinfo (1.2.5)
|
38
|
+
thread_safe (~> 0.1)
|
11
39
|
|
12
40
|
PLATFORMS
|
13
41
|
ruby
|
14
42
|
|
15
43
|
DEPENDENCIES
|
44
|
+
activesupport (~> 5.1.5)
|
16
45
|
bundler (~> 1.16)
|
46
|
+
diffy
|
47
|
+
fakeredis
|
48
|
+
graphql (~> 1.8.2)
|
49
|
+
graphql-batch
|
17
50
|
graphql-metrics!
|
18
51
|
minitest (~> 5.0)
|
19
|
-
|
52
|
+
mocha
|
53
|
+
pry
|
54
|
+
rake
|
20
55
|
|
21
56
|
BUNDLED WITH
|
22
|
-
1.16.
|
57
|
+
1.16.3
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
#
|
1
|
+
# GraphQL Metrics Extractor
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
3
|
+
Extract as much much detail as you want from GraphQL queries, served up from your Ruby app and the `graphql` gem.
|
4
|
+
Compatible with the `graphql-batch` gem, to extract batch-loaded fields resolution timings.
|
6
5
|
|
7
6
|
## Installation
|
8
7
|
|
@@ -22,17 +21,115 @@ Or install it yourself as:
|
|
22
21
|
|
23
22
|
## Usage
|
24
23
|
|
25
|
-
|
24
|
+
You can get started quickly with all features enabled by instrumenting your queries
|
25
|
+
with an extractor class (defined below) and with `TimedBatchExecutor` passed as
|
26
|
+
a custom executor when initializing `GraphQL::Batch` instrumentation if you're using it.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class Schema < GraphQL::Schema
|
30
|
+
query QueryRoot
|
31
|
+
mutation MutationRoot
|
32
|
+
|
33
|
+
use LoggingExtractor # Replace me with your own subclass of GraphQLMetrics::Extractor!
|
34
|
+
use GraphQL::Batch, executor_class: GraphQLMetrics::TimedBatchExecutor # Optional.
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
Define your own extractor class, inheriting from `GraphQLMetrics::Extractor`, and
|
39
|
+
implementing the methods below, as needed.
|
40
|
+
|
41
|
+
Here's an example of a simple extractor that logs out all GraphQL query details.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class LoggingExtractor < GraphQLMetrics::Extractor
|
45
|
+
def query_extracted(metrics, _metadata)
|
46
|
+
Rails.logger.debug({
|
47
|
+
query_string: metrics[:query_string],
|
48
|
+
operation_type: metrics[:operation_type],
|
49
|
+
operation_name: metrics[:operation_name],
|
50
|
+
duration: metrics[:duration]
|
51
|
+
})
|
52
|
+
end
|
53
|
+
|
54
|
+
def field_extracted(metrics, _metadata)
|
55
|
+
Rails.logger.debug({
|
56
|
+
type_name: metrics[:type_name],
|
57
|
+
field_name: metrics[:field_name],
|
58
|
+
deprecated: metrics[:deprecated],
|
59
|
+
resolver_times: metrics[:resolver_times],
|
60
|
+
})
|
61
|
+
end
|
62
|
+
|
63
|
+
# NOTE: Applicaable only if you set `use GraphQL::Batch, executor_class: GraphQLMetrics::TimedBatchExecutor`
|
64
|
+
# in your schema.
|
65
|
+
def batch_loaded_field_extracted(metrics, _metadata)
|
66
|
+
Rails.logger.debug({
|
67
|
+
key: metrics[:key],
|
68
|
+
identifiers: metrics[:identifiers],
|
69
|
+
times: metrics[:times],
|
70
|
+
perform_queue_sizes: metrics[:perform_queue_sizes],
|
71
|
+
})
|
72
|
+
end
|
73
|
+
|
74
|
+
def argument_extracted(metrics, _metadata)
|
75
|
+
Rails.logger.debug({
|
76
|
+
name: metrics[:name],
|
77
|
+
type: metrics[:type],
|
78
|
+
value_is_null: metrics[:value_is_null],
|
79
|
+
default_used: metrics[:default_used],
|
80
|
+
parent_input_type: metrics[:parent_input_type],
|
81
|
+
field_name: metrics[:field_name],
|
82
|
+
field_base_type: metrics[:field_base_type],
|
83
|
+
})
|
84
|
+
end
|
85
|
+
|
86
|
+
def variable_extracted(metrics, _metadata)
|
87
|
+
Rails.logger.debug({
|
88
|
+
operation_name: metrics[:operation_name],
|
89
|
+
unwrapped_type_name: metrics[:unwrapped_type_name],
|
90
|
+
type: metrics[:type],
|
91
|
+
default_value_type: metrics[:default_value_type],
|
92
|
+
provided_value: metrics[:provided_value],
|
93
|
+
default_used: metrics[:default_used],
|
94
|
+
})
|
95
|
+
end
|
96
|
+
|
97
|
+
# Define this if you want to do something with the query just before query logging.
|
98
|
+
def before_query_extracted(query, query_context)
|
99
|
+
Rails.logger.debug({
|
100
|
+
something_from_context: query_context[:something]
|
101
|
+
})
|
102
|
+
end
|
103
|
+
|
104
|
+
# Return something `truthy` if you want skip query extraction entirely, based on the query or
|
105
|
+
# for example its context.
|
106
|
+
def skip_extraction?(_query)
|
107
|
+
false
|
108
|
+
end
|
109
|
+
|
110
|
+
# Return something `truthy` if you want skip producing field resolution
|
111
|
+
# timing metrics. Applicable only if `field_extracted` is also defined.
|
112
|
+
def skip_field_resolution_timing?(_query, _metadata)
|
113
|
+
false
|
114
|
+
end
|
115
|
+
|
116
|
+
# Use or clear state after metrics extraction
|
117
|
+
def after_query_teardown(_query)
|
118
|
+
# Use or clear state after metrics extraction, i.e. Flush metrics to Datadog, Kafka etc.
|
119
|
+
# i.e. kafka.producer.produce('graphql_metrics', @collected_metrics); kafka.producer.deliver_messages
|
120
|
+
end
|
121
|
+
end
|
122
|
+
```
|
26
123
|
|
27
124
|
## Development
|
28
125
|
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
126
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
127
|
|
31
128
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
129
|
|
33
130
|
## Contributing
|
34
131
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
132
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/graphql_metrics. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
36
133
|
|
37
134
|
## License
|
38
135
|
|
@@ -40,4 +137,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
40
137
|
|
41
138
|
## Code of Conduct
|
42
139
|
|
43
|
-
Everyone interacting in the
|
140
|
+
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).
|
data/graphql_metrics.gemspec
CHANGED
@@ -5,13 +5,16 @@ require "graphql_metrics/version"
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "graphql-metrics"
|
8
|
-
spec.version =
|
8
|
+
spec.version = GraphQLMetrics::VERSION
|
9
9
|
spec.authors = ["Christopher Butcher"]
|
10
|
-
spec.email = ["
|
10
|
+
spec.email = ["gems@shopify.com"]
|
11
11
|
|
12
|
-
spec.summary = '
|
13
|
-
spec.description =
|
14
|
-
|
12
|
+
spec.summary = 'GraphQL Metrics Extractor'
|
13
|
+
spec.description = <<~DESCRIPTION
|
14
|
+
Extract as much much detail as you want from GraphQL queries, served up from your Ruby app and the `graphql` gem.
|
15
|
+
Compatible with the `graphql-batch` gem, to extract batch-loaded fields resolution timings.
|
16
|
+
DESCRIPTION
|
17
|
+
spec.homepage = 'https://github.com/Shopify/graphql-metrics'
|
15
18
|
spec.license = "MIT"
|
16
19
|
|
17
20
|
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
@@ -33,4 +36,11 @@ Gem::Specification.new do |spec|
|
|
33
36
|
spec.add_development_dependency "bundler", "~> 1.16"
|
34
37
|
spec.add_development_dependency "rake", "~> 10.0"
|
35
38
|
spec.add_development_dependency "minitest", "~> 5.0"
|
39
|
+
spec.add_development_dependency 'graphql-batch'
|
40
|
+
spec.add_development_dependency "graphql", "~> 1.8.2"
|
41
|
+
spec.add_development_dependency "activesupport", "~> 5.1.5"
|
42
|
+
spec.add_development_dependency "pry"
|
43
|
+
spec.add_development_dependency "mocha"
|
44
|
+
spec.add_development_dependency "diffy"
|
45
|
+
spec.add_development_dependency "fakeredis"
|
36
46
|
end
|
data/lib/graphql_metrics.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
require "graphql_metrics/version"
|
4
|
+
require "graphql_metrics/extractor"
|
5
|
+
require "graphql_metrics/timed_batch_executor"
|
@@ -0,0 +1,296 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQLMetrics
|
4
|
+
class Extractor
|
5
|
+
CONTEXT_NAMESPACE = :extracted_metrics
|
6
|
+
TIMING_CACHE_KEY = :timing_cache
|
7
|
+
START_TIME_KEY = :query_start_time
|
8
|
+
|
9
|
+
EXPLICIT_NULL = 'EXPLICIT_NULL'
|
10
|
+
IMPLICIT_NULL = 'IMPLICIT_NULL'
|
11
|
+
NON_NULL = 'NON_NULL'
|
12
|
+
|
13
|
+
attr_reader :query, :ctx_namespace
|
14
|
+
|
15
|
+
def self.use(schema_definition)
|
16
|
+
extractor = self.new
|
17
|
+
return unless extractor.extractor_defines_any_visitors?
|
18
|
+
|
19
|
+
extractor.setup_instrumentation(schema_definition)
|
20
|
+
end
|
21
|
+
|
22
|
+
def use(schema_definition)
|
23
|
+
return unless extractor_defines_any_visitors?
|
24
|
+
setup_instrumentation(schema_definition)
|
25
|
+
end
|
26
|
+
|
27
|
+
def setup_instrumentation(schema_definition)
|
28
|
+
schema_definition.instrument(:query, self)
|
29
|
+
schema_definition.instrument(:field, self)
|
30
|
+
end
|
31
|
+
|
32
|
+
def before_query(query)
|
33
|
+
return unless extractor_defines_any_visitors?
|
34
|
+
|
35
|
+
ns = query.context.namespace(CONTEXT_NAMESPACE)
|
36
|
+
ns[TIMING_CACHE_KEY] = {}
|
37
|
+
ns[START_TIME_KEY] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
38
|
+
rescue StandardError => ex
|
39
|
+
handle_extraction_exception(ex)
|
40
|
+
end
|
41
|
+
|
42
|
+
def after_query(query)
|
43
|
+
return unless extractor_defines_any_visitors?
|
44
|
+
|
45
|
+
@query = query
|
46
|
+
return unless query.valid?
|
47
|
+
return if respond_to?(:skip_extraction?) && skip_extraction?(query)
|
48
|
+
return unless @ctx_namespace = query.context.namespace(CONTEXT_NAMESPACE)
|
49
|
+
return unless query.irep_selection
|
50
|
+
|
51
|
+
before_query_extracted(query, query.context) if respond_to?(:before_query_extracted)
|
52
|
+
extract_query
|
53
|
+
|
54
|
+
query.operations.each_value do |operation|
|
55
|
+
extract_variables(operation)
|
56
|
+
end
|
57
|
+
|
58
|
+
extract_node(query.irep_selection)
|
59
|
+
extract_batch_loaders
|
60
|
+
|
61
|
+
after_query_teardown(query) if respond_to?(:after_query_teardown)
|
62
|
+
rescue StandardError => ex
|
63
|
+
handle_extraction_exception(ex)
|
64
|
+
end
|
65
|
+
|
66
|
+
def instrument(type, field)
|
67
|
+
return field unless respond_to?(:field_extracted)
|
68
|
+
return field if type.introspection?
|
69
|
+
|
70
|
+
old_resolve_proc = field.resolve_proc
|
71
|
+
new_resolve_proc = ->(obj, args, ctx) do
|
72
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
73
|
+
result = old_resolve_proc.call(obj, args, ctx)
|
74
|
+
|
75
|
+
begin
|
76
|
+
next result if respond_to?(:skip_field_resolution_timing?) &&
|
77
|
+
skip_field_resolution_timing?(query, ctx)
|
78
|
+
|
79
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
80
|
+
|
81
|
+
ns = ctx.namespace(CONTEXT_NAMESPACE)
|
82
|
+
|
83
|
+
ns[TIMING_CACHE_KEY][ctx.ast_node] ||= []
|
84
|
+
ns[TIMING_CACHE_KEY][ctx.ast_node] << end_time - start_time
|
85
|
+
|
86
|
+
result
|
87
|
+
rescue StandardError => ex
|
88
|
+
handle_extraction_exception(ex)
|
89
|
+
result
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
field.redefine { resolve(new_resolve_proc) }
|
94
|
+
end
|
95
|
+
|
96
|
+
def extractor_defines_any_visitors?
|
97
|
+
respond_to?(:query_extracted) ||
|
98
|
+
respond_to?(:field_extracted) ||
|
99
|
+
respond_to?(:argument_extracted) ||
|
100
|
+
respond_to?(:variable_extracted) ||
|
101
|
+
respond_to?(:batch_loaded_field_extracted) ||
|
102
|
+
respond_to?(:before_query_extracted)
|
103
|
+
end
|
104
|
+
|
105
|
+
def handle_extraction_exception(ex)
|
106
|
+
raise ex
|
107
|
+
end
|
108
|
+
|
109
|
+
def extract_batch_loaders
|
110
|
+
return unless respond_to?(:batch_loaded_field_extracted)
|
111
|
+
|
112
|
+
TimedBatchExecutor.timings.each do |key, resolve_meta|
|
113
|
+
key, identifiers = TimedBatchExecutor.serialize_loader_key(key)
|
114
|
+
|
115
|
+
batch_loaded_field_extracted(
|
116
|
+
{
|
117
|
+
key: key,
|
118
|
+
identifiers: identifiers,
|
119
|
+
times: resolve_meta[:times],
|
120
|
+
perform_queue_sizes: resolve_meta[:perform_queue_sizes],
|
121
|
+
},
|
122
|
+
{
|
123
|
+
query: query,
|
124
|
+
}
|
125
|
+
)
|
126
|
+
end
|
127
|
+
rescue StandardError => ex
|
128
|
+
handle_extraction_exception(ex)
|
129
|
+
ensure
|
130
|
+
TimedBatchExecutor.clear_timings
|
131
|
+
end
|
132
|
+
|
133
|
+
def extract_query
|
134
|
+
return unless respond_to?(:query_extracted)
|
135
|
+
|
136
|
+
start_time = ctx_namespace[START_TIME_KEY]
|
137
|
+
return unless start_time
|
138
|
+
|
139
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
140
|
+
|
141
|
+
query_extracted(
|
142
|
+
{
|
143
|
+
query_string: query.document.to_query_string,
|
144
|
+
operation_type: query.selected_operation.operation_type,
|
145
|
+
operation_name: query.selected_operation_name,
|
146
|
+
duration: end_time - start_time
|
147
|
+
},
|
148
|
+
{
|
149
|
+
query: query,
|
150
|
+
start_time: start_time,
|
151
|
+
end_time: end_time
|
152
|
+
}
|
153
|
+
)
|
154
|
+
rescue StandardError => ex
|
155
|
+
handle_extraction_exception(ex)
|
156
|
+
end
|
157
|
+
|
158
|
+
def extract_field(irep_node)
|
159
|
+
return unless respond_to?(:field_extracted)
|
160
|
+
return unless irep_node.definition
|
161
|
+
|
162
|
+
field_extracted(
|
163
|
+
{
|
164
|
+
type_name: irep_node.owner_type.name,
|
165
|
+
field_name: irep_node.definition.name,
|
166
|
+
deprecated: irep_node.definition.deprecation_reason.present?,
|
167
|
+
resolver_times: ctx_namespace.dig(TIMING_CACHE_KEY, irep_node.ast_node) || [],
|
168
|
+
},
|
169
|
+
{
|
170
|
+
irep_node: irep_node,
|
171
|
+
query: query,
|
172
|
+
ctx_namespace: ctx_namespace
|
173
|
+
}
|
174
|
+
)
|
175
|
+
|
176
|
+
rescue StandardError => ex
|
177
|
+
handle_extraction_exception(ex)
|
178
|
+
end
|
179
|
+
|
180
|
+
def extract_argument(value, irep_node, types)
|
181
|
+
return unless respond_to?(:argument_extracted)
|
182
|
+
|
183
|
+
argument_extracted(
|
184
|
+
{
|
185
|
+
name: value.definition.expose_as,
|
186
|
+
type: value.definition.type.unwrap.to_s,
|
187
|
+
value_is_null: value.value.nil?,
|
188
|
+
default_used: value.default_used?,
|
189
|
+
parent_input_type: types.map(&:unwrap).last&.to_s,
|
190
|
+
field_name: irep_node.definition.name,
|
191
|
+
field_base_type: irep_node&.owner_type.to_s
|
192
|
+
},
|
193
|
+
{
|
194
|
+
query: query,
|
195
|
+
irep_node: irep_node,
|
196
|
+
value: value,
|
197
|
+
}
|
198
|
+
)
|
199
|
+
rescue StandardError => ex
|
200
|
+
handle_extraction_exception(ex)
|
201
|
+
end
|
202
|
+
|
203
|
+
def extract_variables(operation)
|
204
|
+
return unless respond_to?(:variable_extracted)
|
205
|
+
|
206
|
+
operation.variables.each do |variable|
|
207
|
+
value_provided = query.provided_variables.key?(variable.name)
|
208
|
+
|
209
|
+
default_value_type = case variable.default_value
|
210
|
+
when GraphQL::Language::Nodes::NullValue
|
211
|
+
EXPLICIT_NULL
|
212
|
+
when nil
|
213
|
+
IMPLICIT_NULL
|
214
|
+
else
|
215
|
+
NON_NULL
|
216
|
+
end
|
217
|
+
|
218
|
+
default_used = !value_provided && default_value_type != IMPLICIT_NULL
|
219
|
+
|
220
|
+
variable_extracted(
|
221
|
+
{
|
222
|
+
operation_name: operation.name,
|
223
|
+
unwrapped_type_name: unwrapped_type(variable.type),
|
224
|
+
type: variable.type.to_query_string,
|
225
|
+
default_value_type: default_value_type,
|
226
|
+
provided_value: value_provided,
|
227
|
+
default_used: default_used
|
228
|
+
},
|
229
|
+
{
|
230
|
+
query: query
|
231
|
+
}
|
232
|
+
)
|
233
|
+
end
|
234
|
+
rescue StandardError => ex
|
235
|
+
handle_extraction_exception(ex)
|
236
|
+
end
|
237
|
+
|
238
|
+
def extract_arguments(irep_node)
|
239
|
+
return unless irep_node.ast_node.is_a?(GraphQL::Language::Nodes::Field)
|
240
|
+
|
241
|
+
traverse_arguments(irep_node.arguments.argument_values.values, irep_node)
|
242
|
+
rescue GraphQL::ExecutionError
|
243
|
+
# no-op. See https://github.com/rmosolgo/graphql-ruby/issues/982.
|
244
|
+
nil
|
245
|
+
rescue StandardError => ex
|
246
|
+
handle_extraction_exception(ex)
|
247
|
+
end
|
248
|
+
|
249
|
+
def extract_node(irep_node)
|
250
|
+
extract_field(irep_node)
|
251
|
+
extract_arguments(irep_node)
|
252
|
+
|
253
|
+
irep_node.typed_children.each_value do |children|
|
254
|
+
children.each_value do |child_irep_node|
|
255
|
+
extract_node(child_irep_node)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
rescue StandardError => ex
|
259
|
+
handle_extraction_exception(ex)
|
260
|
+
end
|
261
|
+
|
262
|
+
def traverse_arguments(value, irep_node, types = [])
|
263
|
+
case value
|
264
|
+
when Array
|
265
|
+
value.each do |v|
|
266
|
+
traverse_arguments(v, irep_node, types)
|
267
|
+
end
|
268
|
+
when Hash
|
269
|
+
value.each_value do |v|
|
270
|
+
traverse_arguments(v, irep_node, types)
|
271
|
+
end
|
272
|
+
when ::GraphQL::Query::Arguments
|
273
|
+
value.each_value do |arg_val|
|
274
|
+
traverse_arguments(arg_val, irep_node, types)
|
275
|
+
end
|
276
|
+
when ::GraphQL::Query::Arguments::ArgumentValue
|
277
|
+
extract_argument(value, irep_node, types)
|
278
|
+
traverse_arguments(value.value, irep_node, types + [value.definition.type])
|
279
|
+
when ::GraphQL::Schema::InputObject
|
280
|
+
traverse_arguments(value.arguments.argument_values.values, irep_node, types)
|
281
|
+
end
|
282
|
+
rescue StandardError => ex
|
283
|
+
handle_extraction_exception(ex)
|
284
|
+
end
|
285
|
+
|
286
|
+
def unwrapped_type(type)
|
287
|
+
if type.is_a?(GraphQL::Language::Nodes::WrapperType)
|
288
|
+
unwrapped_type(type.of_type)
|
289
|
+
else
|
290
|
+
type.name
|
291
|
+
end
|
292
|
+
rescue StandardError => ex
|
293
|
+
handle_extraction_exception(ex)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQLMetrics
|
4
|
+
BASE_CLASS = if defined?(GraphQL::Batch::Executor)
|
5
|
+
GraphQL::Batch::Executor
|
6
|
+
else
|
7
|
+
class NoExecutor
|
8
|
+
class << self
|
9
|
+
def resolve(_loader)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def around_promise_callbacks
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
NoExecutor
|
20
|
+
end
|
21
|
+
|
22
|
+
class TimedBatchExecutor < BASE_CLASS
|
23
|
+
TIMINGS = {}
|
24
|
+
private_constant :TIMINGS
|
25
|
+
|
26
|
+
class << self
|
27
|
+
def timings
|
28
|
+
TIMINGS
|
29
|
+
end
|
30
|
+
|
31
|
+
def clear_timings
|
32
|
+
TIMINGS.clear
|
33
|
+
end
|
34
|
+
|
35
|
+
def serialize_loader_key(loader_key)
|
36
|
+
identifiers = []
|
37
|
+
|
38
|
+
serialized = loader_key.map do |group_arg|
|
39
|
+
if [Class, Symbol, String].include?(group_arg.class)
|
40
|
+
group_arg
|
41
|
+
elsif group_arg.is_a?(Numeric)
|
42
|
+
identifiers << group_arg
|
43
|
+
'_'
|
44
|
+
elsif group_arg.respond_to?(:id)
|
45
|
+
identifiers << group_arg.id
|
46
|
+
"#{group_arg.class}/_"
|
47
|
+
else
|
48
|
+
'?'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
[serialized.map(&:to_s).join('/'), identifiers.map(&:to_s)]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def resolve(loader)
|
57
|
+
@resolve_meta = {
|
58
|
+
start_time: Process.clock_gettime(Process::CLOCK_MONOTONIC),
|
59
|
+
current_loader: loader,
|
60
|
+
perform_queue_sizes: loader.send(:queue).size
|
61
|
+
}
|
62
|
+
|
63
|
+
super
|
64
|
+
end
|
65
|
+
|
66
|
+
def around_promise_callbacks
|
67
|
+
return super unless @resolve_meta
|
68
|
+
|
69
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
70
|
+
|
71
|
+
TIMINGS[@resolve_meta[:current_loader].loader_key] ||= { times: [], perform_queue_sizes: [] }
|
72
|
+
TIMINGS[@resolve_meta[:current_loader].loader_key][:times] << end_time - @resolve_meta[:start_time]
|
73
|
+
TIMINGS[@resolve_meta[:current_loader].loader_key][:perform_queue_sizes] << @resolve_meta[:perform_queue_sizes]
|
74
|
+
|
75
|
+
@resolve_meta = nil
|
76
|
+
|
77
|
+
super
|
78
|
+
end
|
79
|
+
end
|
80
|
+
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
|
+
version: 1.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher Butcher
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-08-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,9 +52,109 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '5.0'
|
55
|
-
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: graphql-batch
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: graphql
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.8.2
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.8.2
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activesupport
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 5.1.5
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 5.1.5
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: mocha
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: diffy
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: fakeredis
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description: |
|
154
|
+
Extract as much much detail as you want from GraphQL queries, served up from your Ruby app and the `graphql` gem.
|
155
|
+
Compatible with the `graphql-batch` gem, to extract batch-loaded fields resolution timings.
|
56
156
|
email:
|
57
|
-
-
|
157
|
+
- gems@shopify.com
|
58
158
|
executables: []
|
59
159
|
extensions: []
|
60
160
|
extra_rdoc_files: []
|
@@ -71,8 +171,10 @@ files:
|
|
71
171
|
- bin/setup
|
72
172
|
- graphql_metrics.gemspec
|
73
173
|
- lib/graphql_metrics.rb
|
174
|
+
- lib/graphql_metrics/extractor.rb
|
175
|
+
- lib/graphql_metrics/timed_batch_executor.rb
|
74
176
|
- lib/graphql_metrics/version.rb
|
75
|
-
homepage: https://github.com/
|
177
|
+
homepage: https://github.com/Shopify/graphql-metrics
|
76
178
|
licenses:
|
77
179
|
- MIT
|
78
180
|
metadata: {}
|
@@ -92,8 +194,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
194
|
version: '0'
|
93
195
|
requirements: []
|
94
196
|
rubyforge_project:
|
95
|
-
rubygems_version: 2.
|
197
|
+
rubygems_version: 2.5.2
|
96
198
|
signing_key:
|
97
199
|
specification_version: 4
|
98
|
-
summary:
|
200
|
+
summary: GraphQL Metrics Extractor
|
99
201
|
test_files: []
|