graphql 2.4.3 → 2.4.13
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 +4 -4
- data/lib/graphql/analysis/analyzer.rb +2 -1
- data/lib/graphql/analysis/visitor.rb +38 -41
- data/lib/graphql/analysis.rb +15 -12
- data/lib/graphql/autoload.rb +38 -0
- data/lib/graphql/backtrace/table.rb +95 -55
- data/lib/graphql/backtrace.rb +1 -19
- data/lib/graphql/current.rb +6 -1
- 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 +21 -9
- data/lib/graphql/dataloader/null_dataloader.rb +1 -1
- data/lib/graphql/dataloader/source.rb +3 -3
- data/lib/graphql/dataloader.rb +43 -14
- data/lib/graphql/execution/interpreter/resolve.rb +3 -3
- data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +11 -4
- data/lib/graphql/execution/interpreter/runtime.rb +67 -40
- data/lib/graphql/execution/interpreter.rb +16 -6
- data/lib/graphql/execution/multiplex.rb +0 -4
- data/lib/graphql/introspection/directive_location_enum.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/cache.rb +13 -0
- data/lib/graphql/language/document_from_schema_definition.rb +8 -7
- data/lib/graphql/language/lexer.rb +11 -4
- data/lib/graphql/language/nodes.rb +3 -0
- data/lib/graphql/language/parser.rb +2 -2
- data/lib/graphql/language/printer.rb +8 -8
- data/lib/graphql/language/static_visitor.rb +37 -33
- data/lib/graphql/language/visitor.rb +59 -55
- data/lib/graphql/pagination/connection.rb +1 -1
- data/lib/graphql/query/context/scoped_context.rb +1 -1
- data/lib/graphql/query/context.rb +6 -5
- data/lib/graphql/query/variable_validation_error.rb +1 -1
- data/lib/graphql/query.rb +20 -22
- data/lib/graphql/railtie.rb +7 -0
- data/lib/graphql/schema/addition.rb +1 -1
- data/lib/graphql/schema/argument.rb +3 -5
- data/lib/graphql/schema/build_from_definition.rb +8 -7
- data/lib/graphql/schema/directive/flagged.rb +1 -1
- data/lib/graphql/schema/directive.rb +2 -2
- data/lib/graphql/schema/enum.rb +36 -1
- data/lib/graphql/schema/enum_value.rb +1 -1
- data/lib/graphql/schema/field/scope_extension.rb +1 -1
- data/lib/graphql/schema/field.rb +12 -12
- data/lib/graphql/schema/field_extension.rb +1 -1
- data/lib/graphql/schema/has_single_input_argument.rb +3 -1
- data/lib/graphql/schema/input_object.rb +70 -34
- data/lib/graphql/schema/interface.rb +3 -2
- data/lib/graphql/schema/loader.rb +1 -1
- data/lib/graphql/schema/member/has_arguments.rb +25 -17
- data/lib/graphql/schema/member/has_dataloader.rb +60 -0
- data/lib/graphql/schema/member/has_directives.rb +4 -4
- data/lib/graphql/schema/member/has_fields.rb +19 -1
- data/lib/graphql/schema/member/has_interfaces.rb +5 -5
- data/lib/graphql/schema/member/has_validators.rb +1 -1
- data/lib/graphql/schema/member/scoped.rb +1 -1
- data/lib/graphql/schema/member/type_system_helpers.rb +1 -1
- data/lib/graphql/schema/member.rb +1 -0
- data/lib/graphql/schema/object.rb +25 -8
- data/lib/graphql/schema/relay_classic_mutation.rb +0 -1
- data/lib/graphql/schema/resolver.rb +11 -10
- data/lib/graphql/schema/subscription.rb +52 -6
- data/lib/graphql/schema/union.rb +1 -1
- data/lib/graphql/schema/validator/required_validator.rb +23 -6
- data/lib/graphql/schema/validator.rb +1 -1
- data/lib/graphql/schema/visibility/migration.rb +1 -0
- data/lib/graphql/schema/visibility/profile.rb +69 -237
- data/lib/graphql/schema/visibility/visit.rb +190 -0
- data/lib/graphql/schema/visibility.rb +169 -28
- data/lib/graphql/schema/warden.rb +18 -5
- data/lib/graphql/schema.rb +90 -43
- data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +1 -1
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +1 -1
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -1
- data/lib/graphql/static_validation/rules/no_definitions_are_present.rb +1 -1
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +1 -1
- data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +1 -1
- data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +1 -1
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -1
- data/lib/graphql/static_validation/validation_context.rb +1 -0
- data/lib/graphql/static_validation/validator.rb +6 -1
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -1
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +12 -10
- data/lib/graphql/subscriptions/event.rb +12 -1
- data/lib/graphql/subscriptions/serialize.rb +1 -1
- data/lib/graphql/subscriptions.rb +1 -1
- data/lib/graphql/testing/helpers.rb +2 -2
- data/lib/graphql/tracing/active_support_notifications_trace.rb +7 -3
- data/lib/graphql/tracing/active_support_notifications_tracing.rb +1 -1
- data/lib/graphql/tracing/appoptics_trace.rb +9 -1
- data/lib/graphql/tracing/appoptics_tracing.rb +2 -0
- data/lib/graphql/tracing/appsignal_trace.rb +12 -0
- data/lib/graphql/tracing/appsignal_tracing.rb +2 -0
- data/lib/graphql/tracing/call_legacy_tracers.rb +66 -0
- data/lib/graphql/tracing/data_dog_trace.rb +11 -0
- data/lib/graphql/tracing/data_dog_tracing.rb +2 -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/legacy_hooks_trace.rb +1 -0
- data/lib/graphql/tracing/legacy_trace.rb +4 -61
- data/lib/graphql/tracing/new_relic_trace.rb +164 -41
- data/lib/graphql/tracing/new_relic_tracing.rb +2 -0
- data/lib/graphql/tracing/notifications_trace.rb +4 -0
- data/lib/graphql/tracing/notifications_tracing.rb +2 -0
- data/lib/graphql/tracing/null_trace.rb +9 -0
- 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/platform_trace.rb +5 -0
- data/lib/graphql/tracing/prometheus_trace/graphql_collector.rb +2 -0
- data/lib/graphql/tracing/prometheus_trace.rb +31 -0
- data/lib/graphql/tracing/prometheus_tracing.rb +2 -0
- data/lib/graphql/tracing/scout_trace.rb +11 -0
- data/lib/graphql/tracing/scout_tracing.rb +2 -0
- data/lib/graphql/tracing/sentry_trace.rb +11 -0
- data/lib/graphql/tracing/statsd_trace.rb +15 -0
- data/lib/graphql/tracing/statsd_tracing.rb +2 -0
- data/lib/graphql/tracing/trace.rb +128 -1
- data/lib/graphql/tracing.rb +30 -30
- data/lib/graphql/types/relay/connection_behaviors.rb +3 -3
- data/lib/graphql/types/relay/edge_behaviors.rb +2 -2
- data/lib/graphql/types.rb +18 -11
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +55 -47
- metadata +152 -10
- data/lib/graphql/backtrace/inspect_result.rb +0 -38
- data/lib/graphql/backtrace/trace.rb +0 -93
- data/lib/graphql/backtrace/tracer.rb +0 -80
- data/lib/graphql/schema/null_mask.rb +0 -11
@@ -24,7 +24,7 @@ module GraphQL
|
|
24
24
|
.map!(&:name)
|
25
25
|
|
26
26
|
missing_names = required_argument_names - present_argument_names
|
27
|
-
if missing_names.
|
27
|
+
if !missing_names.empty?
|
28
28
|
add_error(GraphQL::StaticValidation::RequiredArgumentsArePresentError.new(
|
29
29
|
"#{ast_node.class.name.split("::").last} '#{ast_node.name}' is missing required arguments: #{missing_names.join(", ")}",
|
30
30
|
nodes: ast_node,
|
@@ -4,7 +4,7 @@ module GraphQL
|
|
4
4
|
module VariableNamesAreUnique
|
5
5
|
def on_operation_definition(node, parent)
|
6
6
|
var_defns = node.variables
|
7
|
-
if var_defns.
|
7
|
+
if !var_defns.empty?
|
8
8
|
vars_by_name = Hash.new { |h, k| h[k] = [] }
|
9
9
|
var_defns.each { |v| vars_by_name[v.name] << v }
|
10
10
|
vars_by_name.each do |name, defns|
|
@@ -21,7 +21,7 @@ module GraphQL
|
|
21
21
|
end
|
22
22
|
node_values = node_values.select { |value| value.is_a? GraphQL::Language::Nodes::VariableIdentifier }
|
23
23
|
|
24
|
-
if node_values.
|
24
|
+
if !node_values.empty?
|
25
25
|
argument_owner = case parent
|
26
26
|
when GraphQL::Language::Nodes::Field
|
27
27
|
context.field_definition
|
@@ -29,6 +29,7 @@ module GraphQL
|
|
29
29
|
@visitor = visitor_class.new(document, self)
|
30
30
|
end
|
31
31
|
|
32
|
+
# TODO stop using def_delegators because of Array allocations
|
32
33
|
def_delegators :@visitor,
|
33
34
|
:path, :type_definition, :field_definition, :argument_definition,
|
34
35
|
:parent_type_definition, :directive_definition, :object_types, :dependencies
|
@@ -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.
|
@@ -171,7 +171,7 @@ module GraphQL
|
|
171
171
|
events_by_fingerprint = @events[topic]
|
172
172
|
object = nil
|
173
173
|
events_by_fingerprint.each do |_fingerprint, events|
|
174
|
-
if events.
|
174
|
+
if !events.empty? && events.first == initial_event
|
175
175
|
# The fingerprint has told us that this response should be shared by all subscribers,
|
176
176
|
# so just run it once, then deliver the result to every subscriber
|
177
177
|
first_event = events.first
|
@@ -20,12 +20,22 @@ module GraphQL
|
|
20
20
|
def after_resolve(value:, context:, object:, arguments:, **rest)
|
21
21
|
if value.is_a?(GraphQL::ExecutionError)
|
22
22
|
value
|
23
|
+
elsif @field.resolver&.method_defined?(:subscription_written?) &&
|
24
|
+
(subscription_namespace = context.namespace(:subscriptions)) &&
|
25
|
+
(subscriptions_by_path = subscription_namespace[:subscriptions])
|
26
|
+
(subscription_instance = subscriptions_by_path[context.current_path])
|
27
|
+
# If it was already written, don't append this event to be written later
|
28
|
+
if !subscription_instance.subscription_written?
|
29
|
+
events = context.namespace(:subscriptions)[:events]
|
30
|
+
events << subscription_instance.event
|
31
|
+
end
|
32
|
+
value
|
23
33
|
elsif (events = context.namespace(:subscriptions)[:events])
|
24
34
|
# This is the first execution, so gather an Event
|
25
35
|
# for the backend to register:
|
26
36
|
event = Subscriptions::Event.new(
|
27
37
|
name: field.name,
|
28
|
-
arguments:
|
38
|
+
arguments: arguments,
|
29
39
|
context: context,
|
30
40
|
field: field,
|
31
41
|
)
|
@@ -33,7 +43,7 @@ module GraphQL
|
|
33
43
|
value
|
34
44
|
elsif context.query.subscription_topic == Subscriptions::Event.serialize(
|
35
45
|
field.name,
|
36
|
-
|
46
|
+
arguments,
|
37
47
|
field,
|
38
48
|
scope: (field.subscription_scope ? context[field.subscription_scope] : nil),
|
39
49
|
)
|
@@ -45,14 +55,6 @@ module GraphQL
|
|
45
55
|
context.skip
|
46
56
|
end
|
47
57
|
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
def arguments_without_field_extras(arguments:)
|
52
|
-
arguments.dup.tap do |event_args|
|
53
|
-
field.extras.each { |k| event_args.delete(k) }
|
54
|
-
end
|
55
|
-
end
|
56
58
|
end
|
57
59
|
end
|
58
60
|
end
|
@@ -20,7 +20,7 @@ module GraphQL
|
|
20
20
|
|
21
21
|
def initialize(name:, arguments:, field: nil, context: nil, scope: nil)
|
22
22
|
@name = name
|
23
|
-
@arguments = arguments
|
23
|
+
@arguments = self.class.arguments_without_field_extras(arguments: arguments, field: field)
|
24
24
|
@context = context
|
25
25
|
field ||= context.field
|
26
26
|
scope_key = field.subscription_scope
|
@@ -39,6 +39,7 @@ module GraphQL
|
|
39
39
|
# @return [String] an identifier for this unit of subscription
|
40
40
|
def self.serialize(_name, arguments, field, scope:, context: GraphQL::Query::NullContext.instance)
|
41
41
|
subscription = field.resolver || GraphQL::Schema::Subscription
|
42
|
+
arguments = arguments_without_field_extras(field: field, arguments: arguments)
|
42
43
|
normalized_args = stringify_args(field, arguments.to_h, context)
|
43
44
|
subscription.topic_for(arguments: normalized_args, field: field, scope: scope)
|
44
45
|
end
|
@@ -60,6 +61,16 @@ module GraphQL
|
|
60
61
|
end
|
61
62
|
|
62
63
|
class << self
|
64
|
+
def arguments_without_field_extras(arguments:, field:)
|
65
|
+
if !field.extras.empty?
|
66
|
+
arguments = arguments.dup
|
67
|
+
field.extras.each do |extra_key|
|
68
|
+
arguments.delete(extra_key)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
arguments
|
72
|
+
end
|
73
|
+
|
63
74
|
private
|
64
75
|
|
65
76
|
# This method does not support cyclic references in the Hash,
|
@@ -146,7 +146,7 @@ module GraphQL
|
|
146
146
|
elsif obj.is_a?(Date) || obj.is_a?(Time)
|
147
147
|
# DateTime extends Date; for TimeWithZone, call `.utc` first.
|
148
148
|
{ TIMESTAMP_KEY => [obj.class.name, obj.strftime(TIMESTAMP_FORMAT)] }
|
149
|
-
elsif obj.is_a?(OpenStruct)
|
149
|
+
elsif defined?(OpenStruct) && obj.is_a?(OpenStruct)
|
150
150
|
{ OPEN_STRUCT_KEY => dump_value(obj.to_h) }
|
151
151
|
elsif defined?(ActiveRecord::Relation) && obj.is_a?(ActiveRecord::Relation)
|
152
152
|
dump_value(obj.to_a)
|
@@ -58,7 +58,7 @@ module GraphQL
|
|
58
58
|
query_context[:current_field] = visible_field
|
59
59
|
field_args = visible_field.coerce_arguments(graphql_result, arguments, query_context)
|
60
60
|
field_args = schema.sync_lazy(field_args)
|
61
|
-
if visible_field.extras.
|
61
|
+
if !visible_field.extras.empty?
|
62
62
|
extra_args = {}
|
63
63
|
visible_field.extras.each do |extra|
|
64
64
|
extra_args[extra] = case extra
|
@@ -92,7 +92,7 @@ module GraphQL
|
|
92
92
|
end
|
93
93
|
graphql_result
|
94
94
|
else
|
95
|
-
unfiltered_type =
|
95
|
+
unfiltered_type = schema.use_visibility_profile? ? schema.visibility.get_type(type_name) : schema.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop
|
96
96
|
if unfiltered_type
|
97
97
|
raise TypeNotVisibleError.new(type_name: type_name)
|
98
98
|
else
|
@@ -1,11 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
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)
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "graphql/tracing/platform_trace"
|
4
|
+
|
3
5
|
module GraphQL
|
4
6
|
module Tracing
|
5
7
|
|
@@ -20,14 +22,18 @@ module GraphQL
|
|
20
22
|
# These GraphQL events will show up as 'graphql.execute' spans
|
21
23
|
EXEC_KEYS = ['execute_multiplex', 'execute_query', 'execute_query_lazy'].freeze
|
22
24
|
|
25
|
+
|
23
26
|
# During auto-instrumentation this version of AppOpticsTracing is compared
|
24
27
|
# with the version provided in the appoptics_apm gem, so that the newer
|
25
28
|
# version of the class can be used
|
26
29
|
|
30
|
+
|
27
31
|
def self.version
|
28
32
|
Gem::Version.new('1.0.0')
|
29
33
|
end
|
30
34
|
|
35
|
+
# rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time
|
36
|
+
|
31
37
|
[
|
32
38
|
'lex',
|
33
39
|
'parse',
|
@@ -55,6 +61,8 @@ module GraphQL
|
|
55
61
|
RUBY
|
56
62
|
end
|
57
63
|
|
64
|
+
# rubocop:enable Development/NoEvalCop
|
65
|
+
|
58
66
|
def execute_field(query:, field:, ast_node:, arguments:, object:)
|
59
67
|
return_type = field.type.unwrap
|
60
68
|
trace_field = if return_type.kind.scalar? || return_type.kind.enum?
|
@@ -81,7 +89,7 @@ module GraphQL
|
|
81
89
|
end
|
82
90
|
end
|
83
91
|
|
84
|
-
def execute_field_lazy(query:, field:, ast_node:, arguments:, object:)
|
92
|
+
def execute_field_lazy(query:, field:, ast_node:, arguments:, object:) # rubocop:disable Development/TraceCallsSuperCop
|
85
93
|
execute_field(query: query, field: field, ast_node: ast_node, arguments: arguments, object: object)
|
86
94
|
end
|
87
95
|
|
@@ -1,7 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "graphql/tracing/platform_trace"
|
4
|
+
|
3
5
|
module GraphQL
|
4
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
|
5
13
|
module AppsignalTrace
|
6
14
|
include PlatformTrace
|
7
15
|
|
@@ -13,6 +21,8 @@ module GraphQL
|
|
13
21
|
super
|
14
22
|
end
|
15
23
|
|
24
|
+
# rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time
|
25
|
+
|
16
26
|
{
|
17
27
|
"lex" => "lex.graphql",
|
18
28
|
"parse" => "parse.graphql",
|
@@ -43,6 +53,8 @@ module GraphQL
|
|
43
53
|
RUBY
|
44
54
|
end
|
45
55
|
|
56
|
+
# rubocop:enable Development/NoEvalCop
|
57
|
+
|
46
58
|
def platform_execute_field(platform_key)
|
47
59
|
Appsignal.instrument(platform_key) do
|
48
60
|
yield
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Tracing
|
5
|
+
# This trace class calls legacy-style tracer with payload hashes.
|
6
|
+
# New-style `trace_with` modules significantly reduce the overhead of tracing,
|
7
|
+
# but that advantage is lost when legacy-style tracers are also used (since the payload hashes are still constructed).
|
8
|
+
module CallLegacyTracers
|
9
|
+
def lex(query_string:)
|
10
|
+
(@multiplex || @query).trace("lex", { query_string: query_string }) { super }
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(query_string:)
|
14
|
+
(@multiplex || @query).trace("parse", { query_string: query_string }) { super }
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate(query:, validate:)
|
18
|
+
query.trace("validate", { validate: validate, query: query }) { super }
|
19
|
+
end
|
20
|
+
|
21
|
+
def analyze_multiplex(multiplex:)
|
22
|
+
multiplex.trace("analyze_multiplex", { multiplex: multiplex }) { super }
|
23
|
+
end
|
24
|
+
|
25
|
+
def analyze_query(query:)
|
26
|
+
query.trace("analyze_query", { query: query }) { super }
|
27
|
+
end
|
28
|
+
|
29
|
+
def execute_multiplex(multiplex:)
|
30
|
+
multiplex.trace("execute_multiplex", { multiplex: multiplex }) { super }
|
31
|
+
end
|
32
|
+
|
33
|
+
def execute_query(query:)
|
34
|
+
query.trace("execute_query", { query: query }) { super }
|
35
|
+
end
|
36
|
+
|
37
|
+
def execute_query_lazy(query:, multiplex:)
|
38
|
+
multiplex.trace("execute_query_lazy", { multiplex: multiplex, query: query }) { super }
|
39
|
+
end
|
40
|
+
|
41
|
+
def execute_field(field:, query:, ast_node:, arguments:, object:)
|
42
|
+
query.trace("execute_field", { field: field, query: query, ast_node: ast_node, arguments: arguments, object: object, owner: field.owner, path: query.context[:current_path] }) { super }
|
43
|
+
end
|
44
|
+
|
45
|
+
def execute_field_lazy(field:, query:, ast_node:, arguments:, object:)
|
46
|
+
query.trace("execute_field_lazy", { field: field, query: query, ast_node: ast_node, arguments: arguments, object: object, owner: field.owner, path: query.context[:current_path] }) { super }
|
47
|
+
end
|
48
|
+
|
49
|
+
def authorized(query:, type:, object:)
|
50
|
+
query.trace("authorized", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super }
|
51
|
+
end
|
52
|
+
|
53
|
+
def authorized_lazy(query:, type:, object:)
|
54
|
+
query.trace("authorized_lazy", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super }
|
55
|
+
end
|
56
|
+
|
57
|
+
def resolve_type(query:, type:, object:)
|
58
|
+
query.trace("resolve_type", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super }
|
59
|
+
end
|
60
|
+
|
61
|
+
def resolve_type_lazy(query:, type:, object:)
|
62
|
+
query.trace("resolve_type_lazy", { context: query.context, type: type, object: object, path: query.context[:current_path] }) { super }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -1,7 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "graphql/tracing/platform_trace"
|
4
|
+
|
3
5
|
module GraphQL
|
4
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
|
5
12
|
module DataDogTrace
|
6
13
|
# @param tracer [#trace] Deprecated
|
7
14
|
# @param analytics_enabled [Boolean] Deprecated
|
@@ -20,6 +27,8 @@ module GraphQL
|
|
20
27
|
super
|
21
28
|
end
|
22
29
|
|
30
|
+
# rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time
|
31
|
+
|
23
32
|
{
|
24
33
|
'lex' => 'lex.graphql',
|
25
34
|
'parse' => 'parse.graphql',
|
@@ -69,6 +78,8 @@ module GraphQL
|
|
69
78
|
RUBY
|
70
79
|
end
|
71
80
|
|
81
|
+
# rubocop:enable Development/NoEvalCop
|
82
|
+
|
72
83
|
def execute_field_span(span_key, query, field, ast_node, arguments, object)
|
73
84
|
return_type = field.type.unwrap
|
74
85
|
trace_field = if return_type.kind.scalar? || return_type.kind.enum?
|
@@ -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
|