Skip to content

Commit aa7e0da

Browse files
marcotcivoanjo
andauthored
GraphQL: Report multiple query errors (#4177)
* GraphQL: Report multiple query errors * Update sig/datadog/tracing/contrib/graphql/ext.rbs Co-authored-by: Ivo Anjo <ivo.anjo@datadoghq.com> * Misc code reviews * Revert gemfile --------- Co-authored-by: Ivo Anjo <ivo.anjo@datadoghq.com>
1 parent 887e26d commit aa7e0da

File tree

11 files changed

+183
-17
lines changed

11 files changed

+183
-17
lines changed

.github/forced-tests-list.json

+3
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@
66
"PARAMETRIC":
77
[
88
"tests/parametric/test_span_events.py"
9+
],
10+
"DEFAULT": [
11+
"tests/test_graphql.py"
912
]
1013
}

docs/GettingStarted.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -880,8 +880,8 @@ The `instrument :graphql` method accepts the following parameters. Additional op
880880
| ------------------------ | -------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
881881
| `enabled` | `DD_TRACE_GRAPHQL_ENABLED` | `Bool` | Whether the integration should create spans. | `true` |
882882
| `schemas` | | `Array` | Array of `GraphQL::Schema` objects (that support class-based schema only) to trace. If you do not provide any, then tracing will applied to all the schemas. | `[]` |
883-
| `with_unified_tracer` | | `Bool` | (Recommended) Enable to instrument with `UnifiedTrace` tracer for `graphql` >= v2.2, **enabling support for Endpoints list** in the Service Catalog. `with_deprecated_tracer` has priority over this. Default is `false`, using `GraphQL::Tracing::DataDogTrace` instead | `false` |
884-
| `with_deprecated_tracer` | | `Bool` | Enable to instrument with deprecated `GraphQL::Tracing::DataDogTracing`. This has priority over `with_unified_tracer`. Default is `false`, using `GraphQL::Tracing::DataDogTrace` instead | `false` |
883+
| `with_unified_tracer` | `DD_TRACE_GRAPHQL_WITH_UNIFIED_TRACER` | `Bool` | (Recommended) Enable to instrument with `UnifiedTrace` tracer for `graphql` >= v2.2, **enabling support for Endpoints list** in the Service Catalog. `with_deprecated_tracer` has priority over this. Default is `false`, using `GraphQL::Tracing::DataDogTrace` instead. This option is disabled by default to maintain backwards compatibility, but **will become the default in `datadog` 3.0.0**. | `false` |
884+
| `with_deprecated_tracer` | | `Bool` | (Not recommended) Enable to instrument with deprecated `GraphQL::Tracing::DataDogTracing`. This has priority over `with_unified_tracer`. Default is `false`, using `GraphQL::Tracing::DataDogTrace` instead | `false` |
885885
| `service_name` | | `String` | Service name used for graphql instrumentation | `'ruby-graphql'` |
886886

887887
Once an instrumentation strategy is selected (`with_unified_tracer: true`, `with_deprecated_tracer: true`, or *no option set* which defaults to `GraphQL::Tracing::DataDogTrace`), it is not possible to change the instrumentation strategy in the same Ruby process.

lib/datadog/tracing/contrib/graphql/configuration/settings.rb

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class Settings < Contrib::Configuration::Settings
4444
end
4545

4646
option :with_unified_tracer do |o|
47+
o.env Ext::ENV_WITH_UNIFIED_TRACER
4748
o.type :bool
4849
o.default false
4950
end

lib/datadog/tracing/contrib/graphql/ext.rb

+4
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ module Ext
1111
# @!visibility private
1212
ENV_ANALYTICS_ENABLED = 'DD_TRACE_GRAPHQL_ANALYTICS_ENABLED'
1313
ENV_ANALYTICS_SAMPLE_RATE = 'DD_TRACE_GRAPHQL_ANALYTICS_SAMPLE_RATE'
14+
ENV_WITH_UNIFIED_TRACER = 'DD_TRACE_GRAPHQL_WITH_UNIFIED_TRACER'
1415
SERVICE_NAME = 'graphql'
1516
TAG_COMPONENT = 'graphql'
17+
18+
# Span event name for query-level errors
19+
EVENT_QUERY_ERROR = 'dd.graphql.query.error'
1620
end
1721
end
1822
end

lib/datadog/tracing/contrib/graphql/unified_trace.rb

+83-11
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,26 @@ def execute_multiplex(*args, multiplex:, **kwargs)
4747
end
4848

4949
def execute_query(*args, query:, **kwargs)
50-
trace(proc { super }, 'execute', query.selected_operation_name, query: query) do |span|
51-
span.set_tag('graphql.source', query.query_string)
52-
span.set_tag('graphql.operation.type', query.selected_operation.operation_type)
53-
span.set_tag('graphql.operation.name', query.selected_operation_name) if query.selected_operation_name
54-
query.variables.instance_variable_get(:@storage).each do |key, value|
55-
span.set_tag("graphql.variables.#{key}", value)
56-
end
57-
end
50+
trace(
51+
proc { super },
52+
'execute',
53+
query.selected_operation_name,
54+
lambda { |span|
55+
span.set_tag('graphql.source', query.query_string)
56+
span.set_tag('graphql.operation.type', query.selected_operation.operation_type)
57+
if query.selected_operation_name
58+
span.set_tag(
59+
'graphql.operation.name',
60+
query.selected_operation_name
61+
)
62+
end
63+
query.variables.instance_variable_get(:@storage).each do |key, value|
64+
span.set_tag("graphql.variables.#{key}", value)
65+
end
66+
},
67+
->(span) { add_query_error_events(span, query.context.errors) },
68+
query: query,
69+
)
5870
end
5971

6072
def execute_query_lazy(*args, query:, multiplex:, **kwargs)
@@ -131,7 +143,16 @@ def platform_resolve_type_key(type, *args, **kwargs)
131143

132144
private
133145

134-
def trace(callable, trace_key, resource, **kwargs)
146+
# Traces the given callable with the given trace key, resource, and kwargs.
147+
#
148+
# @param callable [Proc] the original method call
149+
# @param trace_key [String] the sub-operation name (`"graphql.#{trace_key}"`)
150+
# @param resource [String] the resource name for the trace
151+
# @param before [Proc, nil] a callable to run before the trace, same as the block parameter
152+
# @param after [Proc, nil] a callable to run after the trace, which has access to query values after execution
153+
# @param kwargs [Hash] the arguments to pass to `prepare_span`
154+
# @yield [Span] the block to run before the trace, same as the `before` parameter
155+
def trace(callable, trace_key, resource, before = nil, after = nil, **kwargs, &before_block)
135156
config = Datadog.configuration.tracing[:graphql]
136157

137158
Tracing.trace(
@@ -144,11 +165,20 @@ def trace(callable, trace_key, resource, **kwargs)
144165
Contrib::Analytics.set_sample_rate(span, config[:analytics_sample_rate])
145166
end
146167

147-
yield(span) if block_given?
168+
# A sanity check for us.
169+
raise 'Please provide either `before` or a block, but not both' if before && before_block
170+
171+
if (before_callable = before || before_block)
172+
before_callable.call(span)
173+
end
148174

149175
prepare_span(trace_key, kwargs, span) if @has_prepare_span
150176

151-
callable.call
177+
ret = callable.call
178+
179+
after.call(span) if after
180+
181+
ret
152182
end
153183
end
154184

@@ -163,6 +193,48 @@ def multiplex_resource(multiplex)
163193
operations
164194
end
165195
end
196+
197+
# Create a Span Event for each error that occurs at query level.
198+
#
199+
# These are represented in the Datadog App as special GraphQL errors,
200+
# given their event name `dd.graphql.query.error`.
201+
def add_query_error_events(span, errors)
202+
errors.each do |error|
203+
e = Core::Error.build_from(error)
204+
205+
# {::GraphQL::Error#to_h} returns the error formatted in compliance with the GraphQL spec.
206+
# This is an unwritten contract in the `graphql` library.
207+
# See for an example: https://github.com/rmosolgo/graphql-ruby/blob/0afa241775e5a113863766cce126214dee093464/lib/graphql/execution_error.rb#L32
208+
err = error.to_h
209+
210+
span.span_events << Datadog::Tracing::SpanEvent.new(
211+
Ext::EVENT_QUERY_ERROR,
212+
attributes: {
213+
message: err['message'],
214+
type: e.type,
215+
stacktrace: e.backtrace,
216+
locations: serialize_error_locations(err['locations']),
217+
path: err['path'],
218+
}
219+
)
220+
end
221+
end
222+
223+
# Serialize error's `locations` array as an array of Strings, given
224+
# Span Events do not support hashes nested inside arrays.
225+
#
226+
# Here's an example in which `locations`:
227+
# [
228+
# {"line" => 3, "column" => 10},
229+
# {"line" => 7, "column" => 8},
230+
# ]
231+
# is serialized as:
232+
# ["3:10", "7:8"]
233+
def serialize_error_locations(locations)
234+
locations.map do |location|
235+
"#{location['line']}:#{location['column']}"
236+
end
237+
end
166238
end
167239
end
168240
end

sig/datadog/tracing/contrib/graphql/ext.rbs

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ module Datadog
99

1010
ENV_ANALYTICS_SAMPLE_RATE: "DD_TRACE_GRAPHQL_ANALYTICS_SAMPLE_RATE"
1111

12+
ENV_WITH_UNIFIED_TRACER: "DD_TRACE_GRAPHQL_WITH_UNIFIED_TRACER"
13+
EVENT_QUERY_ERROR: "dd.graphql.query.error"
1214
SERVICE_NAME: "graphql"
1315

1416
TAG_COMPONENT: "graphql"

sig/datadog/tracing/contrib/graphql/unified_trace.rbs

+6-2
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,12 @@ module Datadog
6565
type traceKwargsValues = GraphQL::Query | GraphQL::Schema::Union | GraphQL::Schema::Object | GraphQL::Schema::Field | GraphQL::Execution::Multiplex | GraphQL::Language::Nodes::Field | Hash[Symbol, String] | String | bool | nil
6666

6767
type traceResult = lexerArray | GraphQL::Language::Nodes::Document | { remaining_timeout: Float?, error: Array[StandardError] } | Array[Object] | GraphQL::Schema::Object? | [GraphQL::Schema::Object, nil]
68-
69-
def trace: (Proc callable, String trace_key, String resource, **Hash[Symbol, traceKwargsValues ] kwargs) ?{ (Datadog::Tracing::SpanOperation) -> void } -> traceResult
68+
69+
def add_query_error_events: (SpanOperation span, Array[::GraphQL::Error] errors) -> void
70+
71+
def serialize_error_locations: (Array[{"line" => Integer, "column" => Integer}] locations)-> Array[String]
72+
73+
def trace: (Proc callable, String trace_key, String resource, ?Hash[Symbol, traceKwargsValues ] kwargs, ?before: ^(SpanOperation)-> void, ?after: ^(SpanOperation)-> void) ?{ (SpanOperation) -> void } -> traceResult
7074

7175
def multiplex_resource: (GraphQL::Execution::Multiplex multiplex) -> String?
7276
end

spec/datadog/tracing/contrib/graphql/support/application_schema.rb

+6
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ def userByName(name:)
6363
OpenStruct.new(id: 1, name: name)
6464
end
6565

66+
field :unexpected_error, UserType, description: 'Raises error'
67+
68+
def unexpected_error
69+
raise 'Unexpected error'
70+
end
71+
6672
field :mutationUserByName, UserType, null: false, description: 'Find an user by name' do
6773
argument :name, ::GraphQL::Types::String, required: true
6874
end

spec/datadog/tracing/contrib/graphql/test_schema_examples.rb

+62-2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,26 @@ class #{prefix}TestGraphQLQuery < ::GraphQL::Schema::Object
2222
def user(id:)
2323
OpenStruct.new(id: id, name: 'Bits')
2424
end
25+
26+
field :graphql_error, ::GraphQL::Types::Int, description: 'Raises error'
27+
28+
def graphql_error
29+
raise 'GraphQL error'
30+
end
2531
end
2632
2733
class #{prefix}TestGraphQLSchema < ::GraphQL::Schema
2834
query(#{prefix}TestGraphQLQuery)
35+
36+
rescue_from(RuntimeError) do |err, obj, args, ctx, field|
37+
raise GraphQL::ExecutionError.new(err.message, extensions: {
38+
'int-1': 1,
39+
'str-1': '1',
40+
'array-1-2': [1,'2'],
41+
'': 'empty string',
42+
',': 'comma',
43+
})
44+
end
2945
end
3046
RUBY
3147
# rubocop:enable Style/DocumentDynamicEvalDefinition
@@ -76,10 +92,11 @@ def unload_test_schema(prefix: '')
7692
end
7793

7894
RSpec.shared_examples 'graphql instrumentation with unified naming convention trace' do |prefix: ''|
95+
let(:schema) { Object.const_get("#{prefix}TestGraphQLSchema") }
96+
let(:service) { defined?(super) ? super() : tracer.default_service }
97+
7998
describe 'query trace' do
8099
subject(:result) { schema.execute(query: 'query Users($var: ID!){ user(id: $var) { name } }', variables: { var: 1 }) }
81-
let(:schema) { Object.const_get("#{prefix}TestGraphQLSchema") }
82-
let(:service) { defined?(super) ? super() : tracer.default_service }
83100

84101
matrix = [
85102
['graphql.analyze', 'query Users($var: ID!){ user(id: $var) { name } }'],
@@ -134,4 +151,47 @@ def unload_test_schema(prefix: '')
134151
end
135152
end
136153
end
154+
155+
describe 'query with GraphQL errors' do
156+
subject(:result) { schema.execute(query: 'query Error{ err1: graphqlError err2: graphqlError }') }
157+
158+
let(:graphql_execute) { spans.find { |s| s.name == 'graphql.execute' } }
159+
160+
it 'creates query span for error' do
161+
expect(result.to_h['errors'][0]['message']).to eq('GraphQL error')
162+
expect(result.to_h['data']).to eq('err1' => nil, 'err2' => nil)
163+
164+
expect(graphql_execute.resource).to eq('Error')
165+
expect(graphql_execute.service).to eq(service)
166+
expect(graphql_execute.type).to eq('graphql')
167+
168+
expect(graphql_execute.get_tag('graphql.source')).to eq('query Error{ err1: graphqlError err2: graphqlError }')
169+
170+
expect(graphql_execute.get_tag('graphql.operation.type')).to eq('query')
171+
expect(graphql_execute.get_tag('graphql.operation.name')).to eq('Error')
172+
173+
expect(graphql_execute.events).to contain_exactly(
174+
a_span_event_with(
175+
name: 'dd.graphql.query.error',
176+
attributes: {
177+
'path' => ['err1'],
178+
'locations' => ['1:14'],
179+
'message' => 'GraphQL error',
180+
'type' => 'GraphQL::ExecutionError',
181+
'stacktrace' => include(__FILE__),
182+
}
183+
),
184+
a_span_event_with(
185+
name: 'dd.graphql.query.error',
186+
attributes: {
187+
'path' => ['err2'],
188+
'locations' => ['1:33'],
189+
'message' => 'GraphQL error',
190+
'type' => 'GraphQL::ExecutionError',
191+
'stacktrace' => include(__FILE__),
192+
}
193+
)
194+
)
195+
end
196+
end
137197
end

spec/support/span_helpers.rb

+10
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,14 @@ def description_of(actual)
128128
end
129129
end
130130
end
131+
132+
RSpec::Matchers.define :a_span_event_with do |expected|
133+
match do |actual|
134+
values_match? Datadog::Tracing::SpanEvent, actual
135+
136+
expected.all? do |key, value|
137+
values_match? value, actual.__send__(key)
138+
end
139+
end
140+
end
131141
end

vendor/rbs/graphql/0/graphql.rbs

+4
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,8 @@ module GraphQL
3838
class Multiplex
3939
end
4040
end
41+
42+
class Error
43+
def to_h: -> Hash[String, untyped]
44+
end
4145
end

0 commit comments

Comments
 (0)