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.

Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/current.rb +5 -0
  3. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css +6 -0
  4. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js +7 -0
  5. data/lib/graphql/dashboard/statics/dashboard.css +3 -0
  6. data/lib/graphql/dashboard/statics/dashboard.js +78 -0
  7. data/lib/graphql/dashboard/statics/header-icon.png +0 -0
  8. data/lib/graphql/dashboard/statics/icon.png +0 -0
  9. data/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb +18 -0
  10. data/lib/graphql/dashboard/views/graphql/dashboard/traces/index.html.erb +63 -0
  11. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +60 -0
  12. data/lib/graphql/dashboard.rb +142 -0
  13. data/lib/graphql/dataloader/active_record_association_source.rb +64 -0
  14. data/lib/graphql/dataloader/active_record_source.rb +26 -0
  15. data/lib/graphql/dataloader/async_dataloader.rb +17 -5
  16. data/lib/graphql/dataloader/null_dataloader.rb +1 -1
  17. data/lib/graphql/dataloader/source.rb +2 -2
  18. data/lib/graphql/dataloader.rb +37 -5
  19. data/lib/graphql/execution/interpreter/runtime.rb +26 -7
  20. data/lib/graphql/execution/interpreter.rb +9 -1
  21. data/lib/graphql/invalid_name_error.rb +1 -1
  22. data/lib/graphql/invalid_null_error.rb +6 -12
  23. data/lib/graphql/language/parser.rb +1 -1
  24. data/lib/graphql/query.rb +8 -4
  25. data/lib/graphql/schema/build_from_definition.rb +0 -1
  26. data/lib/graphql/schema/enum.rb +17 -2
  27. data/lib/graphql/schema/input_object.rb +1 -1
  28. data/lib/graphql/schema/interface.rb +1 -0
  29. data/lib/graphql/schema/member/has_dataloader.rb +60 -0
  30. data/lib/graphql/schema/member.rb +1 -0
  31. data/lib/graphql/schema/object.rb +17 -8
  32. data/lib/graphql/schema/resolver.rb +1 -5
  33. data/lib/graphql/schema/visibility/profile.rb +4 -4
  34. data/lib/graphql/schema/visibility.rb +14 -9
  35. data/lib/graphql/schema.rb +52 -10
  36. data/lib/graphql/static_validation/validator.rb +6 -1
  37. data/lib/graphql/tracing/active_support_notifications_trace.rb +6 -2
  38. data/lib/graphql/tracing/appoptics_trace.rb +3 -1
  39. data/lib/graphql/tracing/appsignal_trace.rb +6 -0
  40. data/lib/graphql/tracing/data_dog_trace.rb +5 -0
  41. data/lib/graphql/tracing/detailed_trace/memory_backend.rb +60 -0
  42. data/lib/graphql/tracing/detailed_trace/redis_backend.rb +72 -0
  43. data/lib/graphql/tracing/detailed_trace.rb +93 -0
  44. data/lib/graphql/tracing/new_relic_trace.rb +147 -41
  45. data/lib/graphql/tracing/perfetto_trace/trace.proto +141 -0
  46. data/lib/graphql/tracing/perfetto_trace/trace_pb.rb +33 -0
  47. data/lib/graphql/tracing/perfetto_trace.rb +737 -0
  48. data/lib/graphql/tracing/prometheus_trace.rb +22 -0
  49. data/lib/graphql/tracing/scout_trace.rb +6 -0
  50. data/lib/graphql/tracing/sentry_trace.rb +5 -0
  51. data/lib/graphql/tracing/statsd_trace.rb +9 -0
  52. data/lib/graphql/tracing/trace.rb +124 -0
  53. data/lib/graphql/tracing.rb +2 -0
  54. data/lib/graphql/version.rb +1 -1
  55. data/lib/graphql.rb +3 -0
  56. metadata +49 -3
  57. 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
- maybe_lazy_auth_val = context.query.current_trace.authorized(query: context.query, type: self, object: object) do
69
- begin
70
- authorized?(object, context)
71
- rescue GraphQL::UnauthorizedError => err
72
- context.schema.unauthorized_object(err)
73
- rescue StandardError => err
74
- context.query.handle_or_reraise(err)
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, nil)
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] && (ot = itm.object_type) && @cached_visible[ot] && referenced?(ot)
324
- pts << ot
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
- example_ctx[:visibility_profile] = profile_name
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
- @cached_profiles[visibility_profile] ||= @schema.visibility_profile_class.new(name: visibility_profile, context: context, schema: @schema)
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
- @interface_type_memberships[itm.abstract_type] << itm
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, type_memberships|
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
- type_memberships.each do |type_membership|
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[implementor_type] |= referers
285
+ @all_references[impl_type] |= referers
281
286
  end
282
287
  end
283
288
  end
@@ -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
- ctx.errors << type_error
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
- # will be called at runtime.
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
- target = options[:query] || options[:multiplex]
1448
- mode ||= target && target.context[:trace_mode]
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: [e],
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
- # with a `graphql` suffix.
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