graphql 2.4.10 → 2.4.12
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.
Potentially problematic release.
This version of graphql might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/graphql/analysis/visitor.rb +35 -40
- data/lib/graphql/analysis.rb +12 -9
- data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css +6 -0
- data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js +7 -0
- data/lib/graphql/dashboard/statics/dashboard.css +3 -0
- data/lib/graphql/dashboard/statics/dashboard.js +78 -0
- data/lib/graphql/dashboard/statics/header-icon.png +0 -0
- data/lib/graphql/dashboard/statics/icon.png +0 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb +18 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/traces/index.html.erb +63 -0
- data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +60 -0
- data/lib/graphql/dashboard.rb +142 -0
- data/lib/graphql/execution/interpreter/runtime.rb +1 -1
- data/lib/graphql/invalid_name_error.rb +1 -1
- data/lib/graphql/invalid_null_error.rb +5 -15
- data/lib/graphql/language/lexer.rb +7 -3
- data/lib/graphql/language/parser.rb +1 -1
- data/lib/graphql/schema/build_from_definition.rb +0 -1
- data/lib/graphql/schema/enum.rb +16 -1
- data/lib/graphql/schema/input_object.rb +1 -1
- data/lib/graphql/schema/loader.rb +0 -1
- data/lib/graphql/schema/member/has_dataloader.rb +4 -0
- data/lib/graphql/schema/resolver.rb +5 -1
- data/lib/graphql/schema.rb +51 -9
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -1
- data/lib/graphql/tracing/active_support_notifications_trace.rb +6 -2
- data/lib/graphql/tracing/appoptics_trace.rb +2 -0
- data/lib/graphql/tracing/appsignal_trace.rb +6 -0
- data/lib/graphql/tracing/data_dog_trace.rb +5 -0
- data/lib/graphql/tracing/detailed_trace/memory_backend.rb +60 -0
- data/lib/graphql/tracing/detailed_trace/redis_backend.rb +72 -0
- data/lib/graphql/tracing/detailed_trace.rb +93 -0
- data/lib/graphql/tracing/new_relic_trace.rb +47 -23
- data/lib/graphql/tracing/perfetto_trace.rb +13 -2
- data/lib/graphql/tracing/prometheus_trace.rb +22 -0
- data/lib/graphql/tracing/scout_trace.rb +6 -0
- data/lib/graphql/tracing/sentry_trace.rb +5 -0
- data/lib/graphql/tracing/statsd_trace.rb +9 -0
- data/lib/graphql/tracing.rb +1 -0
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +3 -0
- metadata +15 -2
@@ -2,33 +2,23 @@
|
|
2
2
|
module GraphQL
|
3
3
|
# Raised automatically when a field's resolve function returns `nil`
|
4
4
|
# for a non-null field.
|
5
|
-
class InvalidNullError < GraphQL::
|
5
|
+
class InvalidNullError < GraphQL::Error
|
6
6
|
# @return [GraphQL::BaseType] The owner of {#field}
|
7
7
|
attr_reader :parent_type
|
8
8
|
|
9
9
|
# @return [GraphQL::Field] The field which failed to return a value
|
10
10
|
attr_reader :field
|
11
11
|
|
12
|
-
# @return [
|
13
|
-
attr_reader :
|
12
|
+
# @return [GraphQL::Language::Nodes::Field] the field where the error occurred
|
13
|
+
attr_reader :ast_node
|
14
14
|
|
15
|
-
def initialize(parent_type, field,
|
15
|
+
def initialize(parent_type, field, ast_node)
|
16
16
|
@parent_type = parent_type
|
17
17
|
@field = field
|
18
|
-
@
|
18
|
+
@ast_node = ast_node
|
19
19
|
super("Cannot return null for non-nullable field #{@parent_type.graphql_name}.#{@field.graphql_name}")
|
20
20
|
end
|
21
21
|
|
22
|
-
# @return [Hash] An entry for the response's "errors" key
|
23
|
-
def to_h
|
24
|
-
{ "message" => message }
|
25
|
-
end
|
26
|
-
|
27
|
-
# @deprecated always false
|
28
|
-
def parent_error?
|
29
|
-
false
|
30
|
-
end
|
31
|
-
|
32
22
|
class << self
|
33
23
|
attr_accessor :parent_class
|
34
24
|
|
@@ -13,17 +13,21 @@ module GraphQL
|
|
13
13
|
@pos = nil
|
14
14
|
@max_tokens = max_tokens || Float::INFINITY
|
15
15
|
@tokens_count = 0
|
16
|
+
@finished = false
|
16
17
|
end
|
17
18
|
|
18
|
-
def
|
19
|
-
@
|
19
|
+
def finished?
|
20
|
+
@finished
|
20
21
|
end
|
21
22
|
|
22
23
|
attr_reader :pos, :tokens_count
|
23
24
|
|
24
25
|
def advance
|
25
26
|
@scanner.skip(IGNORE_REGEXP)
|
26
|
-
|
27
|
+
if @scanner.eos?
|
28
|
+
@finished = true
|
29
|
+
return false
|
30
|
+
end
|
27
31
|
@tokens_count += 1
|
28
32
|
if @tokens_count > @max_tokens
|
29
33
|
raise_parse_error("This query is too large to execute.")
|
@@ -110,7 +110,7 @@ module GraphQL
|
|
110
110
|
# Only ignored characters is not a valid document
|
111
111
|
raise GraphQL::ParseError.new("Unexpected end of document", nil, nil, @graphql_str)
|
112
112
|
end
|
113
|
-
while !@lexer.
|
113
|
+
while !@lexer.finished?
|
114
114
|
defns << definition
|
115
115
|
end
|
116
116
|
Document.new(pos: 0, definitions: defns, filename: @filename, source: self)
|
@@ -298,7 +298,6 @@ module GraphQL
|
|
298
298
|
description: enum_value_definition.description,
|
299
299
|
directives: builder.prepare_directives(enum_value_definition, type_resolver),
|
300
300
|
ast_node: enum_value_definition,
|
301
|
-
value_method: GraphQL::Schema::Enum.respond_to?(enum_value_definition.name.downcase) ? false : nil,
|
302
301
|
)
|
303
302
|
end
|
304
303
|
end
|
data/lib/graphql/schema/enum.rb
CHANGED
@@ -70,7 +70,9 @@ module GraphQL
|
|
70
70
|
kwargs[:owner] = self
|
71
71
|
value = enum_value_class.new(*args, **kwargs, &block)
|
72
72
|
|
73
|
-
|
73
|
+
if value_method || (value_methods && value_method != false)
|
74
|
+
generate_value_method(value, value_method)
|
75
|
+
end
|
74
76
|
|
75
77
|
key = value.graphql_name
|
76
78
|
prev_value = own_values[key]
|
@@ -159,6 +161,18 @@ module GraphQL
|
|
159
161
|
end
|
160
162
|
end
|
161
163
|
|
164
|
+
def value_methods(new_value = NOT_CONFIGURED)
|
165
|
+
if NOT_CONFIGURED.equal?(new_value)
|
166
|
+
if @value_methods != nil
|
167
|
+
@value_methods
|
168
|
+
else
|
169
|
+
find_inherited_value(:value_methods, false)
|
170
|
+
end
|
171
|
+
else
|
172
|
+
@value_methods = new_value
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
162
176
|
def kind
|
163
177
|
GraphQL::TypeKinds::ENUM
|
164
178
|
end
|
@@ -220,6 +234,7 @@ module GraphQL
|
|
220
234
|
# because they would end up with names like `#<Class0x1234>::UnresolvedValueError` which messes up bug trackers
|
221
235
|
child_class.const_set(:UnresolvedValueError, Class.new(Schema::Enum::UnresolvedValueError))
|
222
236
|
end
|
237
|
+
child_class.class_eval { @value_methods = nil }
|
223
238
|
super
|
224
239
|
end
|
225
240
|
|
@@ -23,6 +23,10 @@ module GraphQL
|
|
23
23
|
# @param find_by_value [Object] Usually an `id`, might be another value if `find_by:` is also provided
|
24
24
|
# @param find_by [Symbol, String] A column name to look the record up by. (Defaults to the model's primary key.)
|
25
25
|
# @return [ActiveRecord::Base, nil]
|
26
|
+
# @example Finding a record by ID
|
27
|
+
# dataload_record(Post, 5) # Like `Post.find(5)`, but dataloaded
|
28
|
+
# @example Finding a record by another attribute
|
29
|
+
# dataload_record(User, "matz", find_by: :handle) # Like `User.find_by(handle: "matz")`, but dataloaded
|
26
30
|
def dataload_record(model, find_by_value, find_by: nil)
|
27
31
|
source = if find_by
|
28
32
|
dataloader.with(Dataloader::ActiveRecordSource, model, find_by: find_by)
|
@@ -22,7 +22,6 @@ module GraphQL
|
|
22
22
|
include Schema::Member::GraphQLTypeNames
|
23
23
|
# Really we only need description & comment from here, but:
|
24
24
|
extend Schema::Member::BaseDSLMethods
|
25
|
-
extend Member::BaseDSLMethods::ConfigurationExtension
|
26
25
|
extend GraphQL::Schema::Member::HasArguments
|
27
26
|
extend GraphQL::Schema::Member::HasValidators
|
28
27
|
include Schema::Member::HasPath
|
@@ -404,6 +403,11 @@ module GraphQL
|
|
404
403
|
end
|
405
404
|
end
|
406
405
|
|
406
|
+
def inherited(child_class)
|
407
|
+
child_class.description(description)
|
408
|
+
super
|
409
|
+
end
|
410
|
+
|
407
411
|
private
|
408
412
|
|
409
413
|
attr_reader :own_extensions
|
data/lib/graphql/schema.rb
CHANGED
@@ -821,13 +821,13 @@ module GraphQL
|
|
821
821
|
|
822
822
|
attr_writer :validate_timeout
|
823
823
|
|
824
|
-
def validate_timeout(new_validate_timeout =
|
825
|
-
if new_validate_timeout
|
824
|
+
def validate_timeout(new_validate_timeout = NOT_CONFIGURED)
|
825
|
+
if !NOT_CONFIGURED.equal?(new_validate_timeout)
|
826
826
|
@validate_timeout = new_validate_timeout
|
827
827
|
elsif defined?(@validate_timeout)
|
828
828
|
@validate_timeout
|
829
829
|
else
|
830
|
-
find_inherited_value(:validate_timeout)
|
830
|
+
find_inherited_value(:validate_timeout) || 3
|
831
831
|
end
|
832
832
|
end
|
833
833
|
|
@@ -1298,7 +1298,10 @@ module GraphQL
|
|
1298
1298
|
def type_error(type_error, ctx)
|
1299
1299
|
case type_error
|
1300
1300
|
when GraphQL::InvalidNullError
|
1301
|
-
|
1301
|
+
execution_error = GraphQL::ExecutionError.new(type_error.message, ast_node: type_error.ast_node)
|
1302
|
+
execution_error.path = ctx[:current_path]
|
1303
|
+
|
1304
|
+
ctx.errors << execution_error
|
1302
1305
|
when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::IntegerEncodingError
|
1303
1306
|
raise type_error
|
1304
1307
|
when GraphQL::IntegerDecodingError
|
@@ -1366,6 +1369,16 @@ module GraphQL
|
|
1366
1369
|
}.freeze
|
1367
1370
|
end
|
1368
1371
|
|
1372
|
+
# @return [GraphQL::Tracing::DetailedTrace] if it has been configured for this schema
|
1373
|
+
attr_accessor :detailed_trace
|
1374
|
+
|
1375
|
+
# @param query [GraphQL::Query, GraphQL::Execution::Multiplex] Called with a multiplex when multiple queries are executed at once (with {.multiplex})
|
1376
|
+
# @return [Boolean] When `true`, save a detailed trace for this query.
|
1377
|
+
# @see Tracing::DetailedTrace DetailedTrace saves traces when this method returns true
|
1378
|
+
def detailed_trace?(query)
|
1379
|
+
raise "#{self} must implement `def.detailed_trace?(query)` to use DetailedTrace. Implement this method in your schema definition."
|
1380
|
+
end
|
1381
|
+
|
1369
1382
|
def tracer(new_tracer, silence_deprecation_warning: false)
|
1370
1383
|
if !silence_deprecation_warning
|
1371
1384
|
warn("`Schema.tracer(#{new_tracer.inspect})` is deprecated; use module-based `trace_with` instead. See: https://graphql-ruby.org/queries/tracing.html")
|
@@ -1383,14 +1396,22 @@ module GraphQL
|
|
1383
1396
|
find_inherited_value(:tracers, EMPTY_ARRAY) + own_tracers
|
1384
1397
|
end
|
1385
1398
|
|
1386
|
-
# Mix `trace_mod` into this schema's `Trace` class so that its methods
|
1387
|
-
#
|
1399
|
+
# Mix `trace_mod` into this schema's `Trace` class so that its methods will be called at runtime.
|
1400
|
+
#
|
1401
|
+
# You can attach a module to run in only _some_ circumstances by using `mode:`. When a module is added with `mode:`,
|
1402
|
+
# it will only run for queries with a matching `context[:trace_mode]`.
|
1403
|
+
#
|
1404
|
+
# Any custom trace modes _also_ include the default `trace_with ...` modules (that is, those added _without_ any particular `mode: ...` configuration).
|
1405
|
+
#
|
1406
|
+
# @example Adding a trace in a special mode
|
1407
|
+
# # only runs when `query.context[:trace_mode]` is `:special`
|
1408
|
+
# trace_with SpecialTrace, mode: :special
|
1388
1409
|
#
|
1389
1410
|
# @param trace_mod [Module] A module that implements tracing methods
|
1390
1411
|
# @param mode [Symbol] Trace module will only be used for this trade mode
|
1391
1412
|
# @param options [Hash] Keywords that will be passed to the tracing class during `#initialize`
|
1392
1413
|
# @return [void]
|
1393
|
-
# @see GraphQL::Tracing::Trace for available tracing methods
|
1414
|
+
# @see GraphQL::Tracing::Trace Tracing::Trace for available tracing methods
|
1394
1415
|
def trace_with(trace_mod, mode: :default, **options)
|
1395
1416
|
if mode.is_a?(Array)
|
1396
1417
|
mode.each { |m| trace_with(trace_mod, mode: m, **options) }
|
@@ -1440,12 +1461,33 @@ module GraphQL
|
|
1440
1461
|
#
|
1441
1462
|
# If no `mode:` is given, then {default_trace_mode} will be used.
|
1442
1463
|
#
|
1464
|
+
# If this schema is using {Tracing::DetailedTrace} and {.detailed_trace?} returns `true`, then
|
1465
|
+
# DetailedTrace's mode will override the passed-in `mode`.
|
1466
|
+
#
|
1443
1467
|
# @param mode [Symbol] Trace modules for this trade mode will be included
|
1444
1468
|
# @param options [Hash] Keywords that will be passed to the tracing class during `#initialize`
|
1445
1469
|
# @return [Tracing::Trace]
|
1446
1470
|
def new_trace(mode: nil, **options)
|
1447
|
-
|
1448
|
-
|
1471
|
+
should_sample = if detailed_trace
|
1472
|
+
if (query = options[:query])
|
1473
|
+
detailed_trace?(query)
|
1474
|
+
elsif (multiplex = options[:multiplex])
|
1475
|
+
if multiplex.queries.length == 1
|
1476
|
+
detailed_trace?(multiplex.queries.first)
|
1477
|
+
else
|
1478
|
+
detailed_trace?(multiplex)
|
1479
|
+
end
|
1480
|
+
end
|
1481
|
+
else
|
1482
|
+
false
|
1483
|
+
end
|
1484
|
+
|
1485
|
+
if should_sample
|
1486
|
+
mode = detailed_trace.trace_mode
|
1487
|
+
else
|
1488
|
+
target = options[:query] || options[:multiplex]
|
1489
|
+
mode ||= target && target.context[:trace_mode]
|
1490
|
+
end
|
1449
1491
|
|
1450
1492
|
trace_mode = mode || default_trace_mode
|
1451
1493
|
base_trace_options = trace_options_for(trace_mode)
|
@@ -345,7 +345,7 @@ module GraphQL
|
|
345
345
|
fields << Field.new(node, definition, owner_type, parents)
|
346
346
|
when GraphQL::Language::Nodes::InlineFragment
|
347
347
|
fragment_type = node.type ? @types.type(node.type.name) : owner_type
|
348
|
-
find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type:
|
348
|
+
find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: fragment_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
|
349
349
|
when GraphQL::Language::Nodes::FragmentSpread
|
350
350
|
fragment_spreads << FragmentSpread.new(node.name, parents)
|
351
351
|
end
|
@@ -4,8 +4,12 @@ require "graphql/tracing/notifications_trace"
|
|
4
4
|
|
5
5
|
module GraphQL
|
6
6
|
module Tracing
|
7
|
-
# This implementation forwards events to ActiveSupport::Notifications
|
8
|
-
#
|
7
|
+
# This implementation forwards events to ActiveSupport::Notifications with a `graphql` suffix.
|
8
|
+
#
|
9
|
+
# @example Sending execution events to ActiveSupport::Notifications
|
10
|
+
# class MySchema < GraphQL::Schema
|
11
|
+
# trace_with(GraphQL::Tracing::ActiveSupportNotificationsTrace)
|
12
|
+
# end
|
9
13
|
module ActiveSupportNotificationsTrace
|
10
14
|
include NotificationsTrace
|
11
15
|
def initialize(engine: ActiveSupport::Notifications, **rest)
|
@@ -22,10 +22,12 @@ module GraphQL
|
|
22
22
|
# These GraphQL events will show up as 'graphql.execute' spans
|
23
23
|
EXEC_KEYS = ['execute_multiplex', 'execute_query', 'execute_query_lazy'].freeze
|
24
24
|
|
25
|
+
|
25
26
|
# During auto-instrumentation this version of AppOpticsTracing is compared
|
26
27
|
# with the version provided in the appoptics_apm gem, so that the newer
|
27
28
|
# version of the class can be used
|
28
29
|
|
30
|
+
|
29
31
|
def self.version
|
30
32
|
Gem::Version.new('1.0.0')
|
31
33
|
end
|
@@ -4,6 +4,12 @@ require "graphql/tracing/platform_trace"
|
|
4
4
|
|
5
5
|
module GraphQL
|
6
6
|
module Tracing
|
7
|
+
# Instrumentation for reporting GraphQL-Ruby times to Appsignal.
|
8
|
+
#
|
9
|
+
# @example Installing the tracer
|
10
|
+
# class MySchema < GraphQL::Schema
|
11
|
+
# trace_with GraphQL::Tracing::AppsignalTrace
|
12
|
+
# end
|
7
13
|
module AppsignalTrace
|
8
14
|
include PlatformTrace
|
9
15
|
|
@@ -4,6 +4,11 @@ require "graphql/tracing/platform_trace"
|
|
4
4
|
|
5
5
|
module GraphQL
|
6
6
|
module Tracing
|
7
|
+
# A tracer for reporting to DataDog
|
8
|
+
# @example Adding this tracer to your schema
|
9
|
+
# class MySchema < GraphQL::Schema
|
10
|
+
# trace_with GraphQL::Tracing::DataDogTrace
|
11
|
+
# end
|
7
12
|
module DataDogTrace
|
8
13
|
# @param tracer [#trace] Deprecated
|
9
14
|
# @param analytics_enabled [Boolean] Deprecated
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Tracing
|
5
|
+
class DetailedTrace
|
6
|
+
# An in-memory trace storage backend. Suitable for testing and development only.
|
7
|
+
# It won't work for multi-process deployments and everything is erased when the app is restarted.
|
8
|
+
class MemoryBackend
|
9
|
+
def initialize(limit: nil)
|
10
|
+
@limit = limit
|
11
|
+
@traces = {}
|
12
|
+
@next_id = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def traces(last:, before:)
|
16
|
+
page = []
|
17
|
+
@traces.values.reverse_each do |trace|
|
18
|
+
if page.size == last
|
19
|
+
break
|
20
|
+
elsif before.nil? || trace.begin_ms < before
|
21
|
+
page << trace
|
22
|
+
end
|
23
|
+
end
|
24
|
+
page
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_trace(id)
|
28
|
+
@traces[id]
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete_trace(id)
|
32
|
+
@traces.delete(id.to_i)
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete_all_traces
|
37
|
+
@traces.clear
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def save_trace(operation_name, duration, begin_ms, trace_data)
|
42
|
+
id = @next_id
|
43
|
+
@next_id += 1
|
44
|
+
@traces[id] = DetailedTrace::StoredTrace.new(
|
45
|
+
id: id,
|
46
|
+
operation_name: operation_name,
|
47
|
+
duration_ms: duration,
|
48
|
+
begin_ms: begin_ms,
|
49
|
+
trace_data: trace_data
|
50
|
+
)
|
51
|
+
if @limit && @traces.size > @limit
|
52
|
+
del_keys = @traces.keys[0...-@limit]
|
53
|
+
del_keys.each { |k| @traces.delete(k) }
|
54
|
+
end
|
55
|
+
id
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Tracing
|
5
|
+
class DetailedTrace
|
6
|
+
class RedisBackend
|
7
|
+
KEY_PREFIX = "gql:trace:"
|
8
|
+
def initialize(redis:, limit: nil)
|
9
|
+
@redis = redis
|
10
|
+
@key = KEY_PREFIX + "traces"
|
11
|
+
@remrangebyrank_limit = limit ? -limit - 1 : nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def traces(last:, before:)
|
15
|
+
before = case before
|
16
|
+
when Numeric
|
17
|
+
"(#{before}"
|
18
|
+
when nil
|
19
|
+
"+inf"
|
20
|
+
end
|
21
|
+
str_pairs = @redis.zrange(@key, before, 0, byscore: true, rev: true, limit: [0, last || 100], withscores: true)
|
22
|
+
str_pairs.map do |(str_data, score)|
|
23
|
+
entry_to_trace(score, str_data)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete_trace(id)
|
28
|
+
@redis.zremrangebyscore(@key, id, id)
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_all_traces
|
33
|
+
@redis.del(@key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_trace(id)
|
37
|
+
str_data = @redis.zrange(@key, id, id, byscore: true).first
|
38
|
+
if str_data.nil?
|
39
|
+
nil
|
40
|
+
else
|
41
|
+
entry_to_trace(id, str_data)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def save_trace(operation_name, duration_ms, begin_ms, trace_data)
|
46
|
+
id = begin_ms
|
47
|
+
data = JSON.dump({ "o" => operation_name, "d" => duration_ms, "b" => begin_ms, "t" => Base64.encode64(trace_data) })
|
48
|
+
@redis.pipelined do |pipeline|
|
49
|
+
pipeline.zadd(@key, id, data)
|
50
|
+
if @remrangebyrank_limit
|
51
|
+
pipeline.zremrangebyrank(@key, 0, @remrangebyrank_limit)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
id
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def entry_to_trace(id, json_str)
|
60
|
+
data = JSON.parse(json_str)
|
61
|
+
StoredTrace.new(
|
62
|
+
id: id,
|
63
|
+
operation_name: data["o"],
|
64
|
+
duration_ms: data["d"].to_f,
|
65
|
+
begin_ms: data["b"].to_i,
|
66
|
+
trace_data: Base64.decode64(data["t"]),
|
67
|
+
)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "graphql/tracing/detailed_trace/memory_backend"
|
3
|
+
require "graphql/tracing/detailed_trace/redis_backend"
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Tracing
|
7
|
+
# `DetailedTrace` can make detailed profiles for a subset of production traffic.
|
8
|
+
#
|
9
|
+
# When `MySchema.detailed_trace?(query)` returns `true`, a profiler-specific `trace_mode: ...` will be used for the query,
|
10
|
+
# overriding the one in `context[:trace_mode]`.
|
11
|
+
#
|
12
|
+
# __Redis__: The sampler stores its results in a provided Redis database. Depending on your needs,
|
13
|
+
# You can configure this database to retail all data (persistent) or to expire data according to your rules.
|
14
|
+
# If you need to save traces indefinitely, you can download them from Perfetto after opening them there.
|
15
|
+
#
|
16
|
+
# @example Adding the sampler to your schema
|
17
|
+
# class MySchema < GraphQL::Schema
|
18
|
+
# # Add the sampler:
|
19
|
+
# use GraphQL::Tracing::DetailedTrace, redis: Redis.new(...), limit: 100
|
20
|
+
#
|
21
|
+
# # And implement this hook to tell it when to take a sample:
|
22
|
+
# def self.detailed_trace?(query)
|
23
|
+
# # Could use `query.context`, `query.selected_operation_name`, `query.query_string` here
|
24
|
+
# # Could call out to Flipper, etc
|
25
|
+
# rand <= 0.000_1 # one in ten thousand
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# @see Graphql::Dashboard GraphQL::Dashboard for viewing stored results
|
30
|
+
class DetailedTrace
|
31
|
+
# @param redis [Redis] If provided, profiles will be stored in Redis for later review
|
32
|
+
# @param limit [Integer] A maximum number of profiles to store
|
33
|
+
def self.use(schema, trace_mode: :profile_sample, memory: false, redis: nil, limit: nil)
|
34
|
+
storage = if redis
|
35
|
+
RedisBackend.new(redis: redis, limit: limit)
|
36
|
+
elsif memory
|
37
|
+
MemoryBackend.new(limit: limit)
|
38
|
+
else
|
39
|
+
raise ArgumentError, "Pass `redis: ...` to store traces in Redis for later review"
|
40
|
+
end
|
41
|
+
schema.detailed_trace = self.new(storage: storage, trace_mode: trace_mode)
|
42
|
+
schema.trace_with(PerfettoTrace, mode: trace_mode, save_profile: true)
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(storage:, trace_mode:)
|
46
|
+
@storage = storage
|
47
|
+
@trace_mode = trace_mode
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Symbol] The trace mode to use when {Schema.detailed_trace?} returns `true`
|
51
|
+
attr_reader :trace_mode
|
52
|
+
|
53
|
+
# @return [String] ID of saved trace
|
54
|
+
def save_trace(operation_name, duration_ms, begin_ms, trace_data)
|
55
|
+
@storage.save_trace(operation_name, duration_ms, begin_ms, trace_data)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param last [Integer]
|
59
|
+
# @param before [Integer] Timestamp in milliseconds since epoch
|
60
|
+
# @return [Enumerable<StoredTrace>]
|
61
|
+
def traces(last: nil, before: nil)
|
62
|
+
@storage.traces(last: last, before: before)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [StoredTrace, nil]
|
66
|
+
def find_trace(id)
|
67
|
+
@storage.find_trace(id)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [void]
|
71
|
+
def delete_trace(id)
|
72
|
+
@storage.delete_trace(id)
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [void]
|
76
|
+
def delete_all_traces
|
77
|
+
@storage.delete_all_traces
|
78
|
+
end
|
79
|
+
|
80
|
+
class StoredTrace
|
81
|
+
def initialize(id:, operation_name:, duration_ms:, begin_ms:, trace_data:)
|
82
|
+
@id = id
|
83
|
+
@operation_name = operation_name
|
84
|
+
@duration_ms = duration_ms
|
85
|
+
@begin_ms = begin_ms
|
86
|
+
@trace_data = trace_data
|
87
|
+
end
|
88
|
+
|
89
|
+
attr_reader :id, :operation_name, :duration_ms, :begin_ms, :trace_data
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|