graphql 2.4.9 → 2.4.11
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/current.rb +5 -0
- 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/dataloader/active_record_association_source.rb +64 -0
- data/lib/graphql/dataloader/active_record_source.rb +26 -0
- data/lib/graphql/dataloader/async_dataloader.rb +17 -5
- data/lib/graphql/dataloader/null_dataloader.rb +1 -1
- data/lib/graphql/dataloader/source.rb +2 -2
- data/lib/graphql/dataloader.rb +37 -5
- data/lib/graphql/execution/interpreter/runtime.rb +26 -7
- data/lib/graphql/execution/interpreter.rb +9 -1
- data/lib/graphql/invalid_name_error.rb +1 -1
- data/lib/graphql/invalid_null_error.rb +6 -12
- data/lib/graphql/language/parser.rb +1 -1
- data/lib/graphql/query.rb +8 -4
- data/lib/graphql/schema/build_from_definition.rb +0 -1
- data/lib/graphql/schema/enum.rb +17 -2
- data/lib/graphql/schema/input_object.rb +1 -1
- data/lib/graphql/schema/interface.rb +1 -0
- data/lib/graphql/schema/member/has_dataloader.rb +60 -0
- data/lib/graphql/schema/member.rb +1 -0
- data/lib/graphql/schema/object.rb +17 -8
- data/lib/graphql/schema/resolver.rb +1 -5
- data/lib/graphql/schema/visibility/profile.rb +4 -4
- data/lib/graphql/schema/visibility.rb +14 -9
- data/lib/graphql/schema.rb +52 -10
- data/lib/graphql/static_validation/validator.rb +6 -1
- data/lib/graphql/tracing/active_support_notifications_trace.rb +6 -2
- data/lib/graphql/tracing/appoptics_trace.rb +3 -1
- 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 +147 -41
- data/lib/graphql/tracing/perfetto_trace/trace.proto +141 -0
- data/lib/graphql/tracing/perfetto_trace/trace_pb.rb +33 -0
- data/lib/graphql/tracing/perfetto_trace.rb +737 -0
- 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/trace.rb +124 -0
- data/lib/graphql/tracing.rb +2 -0
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +3 -0
- metadata +49 -3
- data/lib/graphql/schema/null_mask.rb +0 -11
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
class Schema
|
5
|
+
class Member
|
6
|
+
module HasDataloader
|
7
|
+
# @return [GraphQL::Dataloader] The dataloader for the currently-running query
|
8
|
+
def dataloader
|
9
|
+
context.dataloader
|
10
|
+
end
|
11
|
+
|
12
|
+
# A shortcut method for loading a key from a source.
|
13
|
+
# Identical to `dataloader.with(source_class, *source_args).load(load_key)`
|
14
|
+
# @param source_class [Class<GraphQL::Dataloader::Source>]
|
15
|
+
# @param source_args [Array<Object>] Any extra parameters defined in `source_class`'s `initialize` method
|
16
|
+
# @param load_key [Object] The key to look up using `def fetch`
|
17
|
+
def dataload(source_class, *source_args, load_key)
|
18
|
+
dataloader.with(source_class, *source_args).load(load_key)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Find an object with ActiveRecord via {Dataloader::ActiveRecordSource}.
|
22
|
+
# @param model [Class<ActiveRecord::Base>]
|
23
|
+
# @param find_by_value [Object] Usually an `id`, might be another value if `find_by:` is also provided
|
24
|
+
# @param find_by [Symbol, String] A column name to look the record up by. (Defaults to the model's primary key.)
|
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
|
30
|
+
def dataload_record(model, find_by_value, find_by: nil)
|
31
|
+
source = if find_by
|
32
|
+
dataloader.with(Dataloader::ActiveRecordSource, model, find_by: find_by)
|
33
|
+
else
|
34
|
+
dataloader.with(Dataloader::ActiveRecordSource, model)
|
35
|
+
end
|
36
|
+
|
37
|
+
source.load(find_by_value)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Look up an associated record using a Rails association.
|
41
|
+
# @param association_name [Symbol] A `belongs_to` or `has_one` association. (If a `has_many` association is named here, it will be selected without pagination.)
|
42
|
+
# @param record [ActiveRecord::Base] The object that the association belongs to.
|
43
|
+
# @param scope [ActiveRecord::Relation] A scope to look up the associated record in
|
44
|
+
# @return [ActiveRecord::Base, nil] The associated record, if there is one
|
45
|
+
# @example Looking up a belongs_to on the current object
|
46
|
+
# dataload_association(:parent) # Equivalent to `object.parent`, but dataloaded
|
47
|
+
# @example Looking up an associated record on some other object
|
48
|
+
# dataload_association(:post, comment) # Equivalent to `comment.post`, but dataloaded
|
49
|
+
def dataload_association(record = object, association_name, scope: nil)
|
50
|
+
source = if scope
|
51
|
+
dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name, scope)
|
52
|
+
else
|
53
|
+
dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name)
|
54
|
+
end
|
55
|
+
source.load(record)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
require 'graphql/schema/member/base_dsl_methods'
|
3
3
|
require 'graphql/schema/member/graphql_type_names'
|
4
4
|
require 'graphql/schema/member/has_ast_node'
|
5
|
+
require 'graphql/schema/member/has_dataloader'
|
5
6
|
require 'graphql/schema/member/has_directives'
|
6
7
|
require 'graphql/schema/member/has_deprecation_reason'
|
7
8
|
require 'graphql/schema/member/has_interfaces'
|
@@ -7,6 +7,7 @@ module GraphQL
|
|
7
7
|
class Object < GraphQL::Schema::Member
|
8
8
|
extend GraphQL::Schema::Member::HasFields
|
9
9
|
extend GraphQL::Schema::Member::HasInterfaces
|
10
|
+
include Member::HasDataloader
|
10
11
|
|
11
12
|
# Raised when an Object doesn't have any field defined and hasn't explicitly opted out of this requirement
|
12
13
|
class FieldsAreRequiredError < GraphQL::Error
|
@@ -65,20 +66,28 @@ module GraphQL
|
|
65
66
|
# @return [GraphQL::Schema::Object, GraphQL::Execution::Lazy]
|
66
67
|
# @raise [GraphQL::UnauthorizedError] if the user-provided hook returns `false`
|
67
68
|
def authorized_new(object, context)
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
69
|
+
context.query.current_trace.begin_authorized(self, object, context)
|
70
|
+
begin
|
71
|
+
maybe_lazy_auth_val = context.query.current_trace.authorized(query: context.query, type: self, object: object) do
|
72
|
+
begin
|
73
|
+
authorized?(object, context)
|
74
|
+
rescue GraphQL::UnauthorizedError => err
|
75
|
+
context.schema.unauthorized_object(err)
|
76
|
+
rescue StandardError => err
|
77
|
+
context.query.handle_or_reraise(err)
|
78
|
+
end
|
75
79
|
end
|
80
|
+
ensure
|
81
|
+
context.query.current_trace.end_authorized(self, object, context, maybe_lazy_auth_val)
|
76
82
|
end
|
77
83
|
|
78
84
|
auth_val = if context.schema.lazy?(maybe_lazy_auth_val)
|
79
85
|
GraphQL::Execution::Lazy.new do
|
86
|
+
context.query.current_trace.begin_authorized(self, object, context)
|
80
87
|
context.query.current_trace.authorized_lazy(query: context.query, type: self, object: object) do
|
81
|
-
context.schema.sync_lazy(maybe_lazy_auth_val)
|
88
|
+
res = context.schema.sync_lazy(maybe_lazy_auth_val)
|
89
|
+
context.query.current_trace.end_authorized(self, object, context, res)
|
90
|
+
res
|
82
91
|
end
|
83
92
|
end
|
84
93
|
else
|
@@ -28,6 +28,7 @@ module GraphQL
|
|
28
28
|
include Schema::Member::HasPath
|
29
29
|
extend Schema::Member::HasPath
|
30
30
|
extend Schema::Member::HasDirectives
|
31
|
+
include Schema::Member::HasDataloader
|
31
32
|
|
32
33
|
# @param object [Object] The application object that this field is being resolved on
|
33
34
|
# @param context [GraphQL::Query::Context]
|
@@ -50,11 +51,6 @@ module GraphQL
|
|
50
51
|
# @return [GraphQL::Query::Context]
|
51
52
|
attr_reader :context
|
52
53
|
|
53
|
-
# @return [GraphQL::Dataloader]
|
54
|
-
def dataloader
|
55
|
-
context.dataloader
|
56
|
-
end
|
57
|
-
|
58
54
|
# @return [GraphQL::Schema::Field]
|
59
55
|
attr_reader :field
|
60
56
|
|
@@ -18,7 +18,7 @@ module GraphQL
|
|
18
18
|
if ctx.respond_to?(:types) && (types = ctx.types).is_a?(self)
|
19
19
|
types
|
20
20
|
else
|
21
|
-
schema.visibility.profile_for(ctx
|
21
|
+
schema.visibility.profile_for(ctx)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -319,9 +319,9 @@ module GraphQL
|
|
319
319
|
case type.kind.name
|
320
320
|
when "INTERFACE"
|
321
321
|
pts = []
|
322
|
-
@schema.visibility.all_interface_type_memberships[type].each do |itm|
|
323
|
-
if @cached_visible[itm] &&
|
324
|
-
pts <<
|
322
|
+
@schema.visibility.all_interface_type_memberships[type].each do |(itm, impl_type)|
|
323
|
+
if @cached_visible[itm] && @cached_visible[impl_type] && referenced?(impl_type)
|
324
|
+
pts << impl_type
|
325
325
|
end
|
326
326
|
end
|
327
327
|
pts
|
@@ -13,6 +13,10 @@ module GraphQL
|
|
13
13
|
# @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?`)
|
14
14
|
# @param migration_errors [Boolean] if `true`, raise an error when `Visibility` and `Warden` return different results
|
15
15
|
def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails) ? Rails.env.production? : nil), migration_errors: false)
|
16
|
+
profiles&.each { |name, ctx|
|
17
|
+
ctx[:visibility_profile] = name
|
18
|
+
ctx.freeze
|
19
|
+
}
|
16
20
|
schema.visibility = self.new(schema, dynamic: dynamic, preload: preload, profiles: profiles, migration_errors: migration_errors)
|
17
21
|
if preload
|
18
22
|
schema.visibility.preload
|
@@ -81,8 +85,7 @@ module GraphQL
|
|
81
85
|
types_to_visit.compact!
|
82
86
|
ensure_all_loaded(types_to_visit)
|
83
87
|
@profiles.each do |profile_name, example_ctx|
|
84
|
-
|
85
|
-
prof = profile_for(example_ctx, profile_name)
|
88
|
+
prof = profile_for(example_ctx)
|
86
89
|
prof.all_types # force loading
|
87
90
|
end
|
88
91
|
end
|
@@ -145,7 +148,7 @@ module GraphQL
|
|
145
148
|
|
146
149
|
attr_reader :cached_profiles
|
147
150
|
|
148
|
-
def profile_for(context, visibility_profile)
|
151
|
+
def profile_for(context, visibility_profile = context[:visibility_profile])
|
149
152
|
if !@profiles.empty?
|
150
153
|
if visibility_profile.nil?
|
151
154
|
if @dynamic
|
@@ -160,7 +163,8 @@ module GraphQL
|
|
160
163
|
elsif !@profiles.include?(visibility_profile)
|
161
164
|
raise ArgumentError, "`#{visibility_profile.inspect}` isn't allowed for `visibility_profile:` (must be one of #{@profiles.keys.map(&:inspect).join(", ")}). Or, add `#{visibility_profile.inspect}` to the list of profiles in the schema definition."
|
162
165
|
else
|
163
|
-
@
|
166
|
+
profile_ctx = @profiles[visibility_profile]
|
167
|
+
@cached_profiles[visibility_profile] ||= @schema.visibility_profile_class.new(name: visibility_profile, context: profile_ctx, schema: @schema)
|
164
168
|
end
|
165
169
|
elsif context.is_a?(Query::NullContext)
|
166
170
|
top_level_profile
|
@@ -222,7 +226,9 @@ module GraphQL
|
|
222
226
|
elsif member.respond_to?(:interface_type_memberships)
|
223
227
|
member.interface_type_memberships.each do |itm|
|
224
228
|
@all_references[itm.abstract_type] << member
|
225
|
-
|
229
|
+
# `itm.object_type` may not actually be `member` if this implementation
|
230
|
+
# is inherited from a superclass
|
231
|
+
@interface_type_memberships[itm.abstract_type] << [itm, member]
|
226
232
|
end
|
227
233
|
elsif member < GraphQL::Schema::Union
|
228
234
|
@unions_for_references << member
|
@@ -271,13 +277,12 @@ module GraphQL
|
|
271
277
|
|
272
278
|
# TODO: somehow don't iterate over all these,
|
273
279
|
# only the ones that may have been modified
|
274
|
-
@interface_type_memberships.each do |int_type,
|
280
|
+
@interface_type_memberships.each do |int_type, type_membership_pairs|
|
275
281
|
referers = @all_references[int_type].select { |r| r.is_a?(GraphQL::Schema::Field) }
|
276
282
|
if !referers.empty?
|
277
|
-
|
278
|
-
implementor_type = type_membership.object_type
|
283
|
+
type_membership_pairs.each do |(type_membership, impl_type)|
|
279
284
|
# Add new items only:
|
280
|
-
@all_references[
|
285
|
+
@all_references[impl_type] |= referers
|
281
286
|
end
|
282
287
|
end
|
283
288
|
end
|
data/lib/graphql/schema.rb
CHANGED
@@ -7,7 +7,6 @@ require "graphql/schema/find_inherited_value"
|
|
7
7
|
require "graphql/schema/finder"
|
8
8
|
require "graphql/schema/introspection_system"
|
9
9
|
require "graphql/schema/late_bound_type"
|
10
|
-
require "graphql/schema/null_mask"
|
11
10
|
require "graphql/schema/timeout"
|
12
11
|
require "graphql/schema/type_expression"
|
13
12
|
require "graphql/schema/unique_within_type"
|
@@ -1115,9 +1114,6 @@ module GraphQL
|
|
1115
1114
|
|
1116
1115
|
# @api private
|
1117
1116
|
def handle_or_reraise(context, err)
|
1118
|
-
if context[:backtrace] || using_backtrace
|
1119
|
-
err = GraphQL::Backtrace::TracedError.new(err, context)
|
1120
|
-
end
|
1121
1117
|
handler = Execution::Errors.find_handler_for(self, err.class)
|
1122
1118
|
if handler
|
1123
1119
|
obj = context[:current_object]
|
@@ -1129,6 +1125,10 @@ module GraphQL
|
|
1129
1125
|
end
|
1130
1126
|
handler[:handler].call(err, obj, args, context, field)
|
1131
1127
|
else
|
1128
|
+
if (context[:backtrace] || using_backtrace) && !err.is_a?(GraphQL::ExecutionError)
|
1129
|
+
err = GraphQL::Backtrace::TracedError.new(err, context)
|
1130
|
+
end
|
1131
|
+
|
1132
1132
|
raise err
|
1133
1133
|
end
|
1134
1134
|
end
|
@@ -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)
|
@@ -27,6 +27,8 @@ module GraphQL
|
|
27
27
|
# @param max_errors [Integer] Maximum number of errors before aborting validation. Any positive number will limit the number of errors. Defaults to nil for no limit.
|
28
28
|
# @return [Array<Hash>]
|
29
29
|
def validate(query, validate: true, timeout: nil, max_errors: nil)
|
30
|
+
errors = nil
|
31
|
+
query.current_trace.begin_validate(query, validate)
|
30
32
|
query.current_trace.validate(validate: validate, query: query) do
|
31
33
|
begin_t = Time.now
|
32
34
|
errors = if validate == false
|
@@ -58,10 +60,13 @@ module GraphQL
|
|
58
60
|
}
|
59
61
|
end
|
60
62
|
rescue GraphQL::ExecutionError => e
|
63
|
+
errors = [e]
|
61
64
|
{
|
62
65
|
remaining_timeout: nil,
|
63
|
-
errors:
|
66
|
+
errors: errors,
|
64
67
|
}
|
68
|
+
ensure
|
69
|
+
query.current_trace.end_validate(query, validate, errors)
|
65
70
|
end
|
66
71
|
|
67
72
|
# Invoked when static validation times out.
|
@@ -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
|
@@ -83,7 +85,7 @@ module GraphQL
|
|
83
85
|
end
|
84
86
|
end
|
85
87
|
|
86
|
-
def execute_field_lazy(query:, field:, ast_node:, arguments:, object:)
|
88
|
+
def execute_field_lazy(query:, field:, ast_node:, arguments:, object:) # rubocop:disable Development/TraceCallsSuperCop
|
87
89
|
execute_field(query: query, field: field, ast_node: ast_node, arguments: arguments, object: object)
|
88
90
|
end
|
89
91
|
|
@@ -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
|