Skip to content

Commit

Permalink
GraphQL: Report multiple query errors
Browse files Browse the repository at this point in the history
  • Loading branch information
marcotc committed Jan 18, 2025
1 parent 3e81a5a commit 418a657
Show file tree
Hide file tree
Showing 15 changed files with 184 additions and 31 deletions.
4 changes: 3 additions & 1 deletion .github/forced-tests-list.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{

"DEFAULT": [
"tests/test_graphql.py"
]
}
2 changes: 1 addition & 1 deletion .github/workflows/system-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
env:
REGISTRY: ghcr.io
REPO: ghcr.io/datadog/dd-trace-rb
SYSTEM_TESTS_REF: main # This must always be set to `main` on dd-trace-rb's master branch
SYSTEM_TESTS_REF: marcotc/graphql-error-events # This must always be set to `main` on dd-trace-rb's master branch

jobs:
build-harness:
Expand Down
1 change: 0 additions & 1 deletion Steepfile
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ target :datadog do
ignore 'lib/datadog/core/environment/socket.rb'
ignore 'lib/datadog/core/environment/variable_helpers.rb'
ignore 'lib/datadog/core/environment/vm_cache.rb'
ignore 'lib/datadog/core/error.rb'
ignore 'lib/datadog/core/metrics/client.rb'
ignore 'lib/datadog/core/metrics/helpers.rb'
ignore 'lib/datadog/core/metrics/metric.rb'
Expand Down
2 changes: 1 addition & 1 deletion docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ The `instrument :graphql` method accepts the following parameters. Additional op
| ------------------------ | -------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| `enabled` | `DD_TRACE_GRAPHQL_ENABLED` | `Bool` | Whether the integration should create spans. | `true` |
| `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. | `[]` |
| `with_unified_tracer` | | `Bool` | (Recommended) Enable to instrument with `UnifiedTrace` tracer for `graphql` >= v2.2, **enabling support for API Catalog**. `with_deprecated_tracer` has priority over this. Default is `false`, using `GraphQL::Tracing::DataDogTrace` instead | `false` |
| `with_unified_tracer` | `DD_TRACE_GRAPHQL_WITH_UNIFIED_TRACER` | `Bool` | (Recommended) Enable to instrument with `UnifiedTrace` tracer for `graphql` >= v2.2, **enabling support for API Catalog**. `with_deprecated_tracer` has priority over this. Default is `false`, using `GraphQL::Tracing::DataDogTrace` instead | `false` |
| `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` |
| `service_name` | | `String` | Service name used for graphql instrumentation | `'ruby-graphql'` |

Expand Down
6 changes: 4 additions & 2 deletions lib/datadog/core/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ class << self
def build_from(value)
case value
when Error then value
# steep:ignore:start
when Array then new(*value)
# steep:ignore:end
when Exception then new(value.class, value.message, full_backtrace(value))
when ContainsMessage then new(value.class, value.message)
when String then new(nil, value)
when ContainsMessage then new(value.class, value.message)
else BlankError
end
end
Expand Down Expand Up @@ -75,7 +77,7 @@ def backtrace_for(ex, backtrace)
if trace[1]
# Ident stack trace for caller lines, to separate
# them from the main error lines.
trace[1..-1].each do |line|
trace[1..-1]&.each do |line|
backtrace << "\n\tfrom "
backtrace << line
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class Settings < Contrib::Configuration::Settings
end

option :with_unified_tracer do |o|
o.env Ext::ENV_WITH_UNIFIED_TRACER
o.type :bool
o.default false
end
Expand Down
4 changes: 4 additions & 0 deletions lib/datadog/tracing/contrib/graphql/ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ module Ext
# @!visibility private
ENV_ANALYTICS_ENABLED = 'DD_TRACE_GRAPHQL_ANALYTICS_ENABLED'
ENV_ANALYTICS_SAMPLE_RATE = 'DD_TRACE_GRAPHQL_ANALYTICS_SAMPLE_RATE'
ENV_WITH_UNIFIED_TRACER = 'DD_TRACE_GRAPHQL_WITH_UNIFIED_TRACER'
SERVICE_NAME = 'graphql'
TAG_COMPONENT = 'graphql'

# Span event name for query-level errors
EVENT_QUERY_ERROR = 'dd.graphql.query.error'
end
end
end
Expand Down
87 changes: 76 additions & 11 deletions lib/datadog/tracing/contrib/graphql/unified_trace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,26 @@ def execute_multiplex(*args, multiplex:, **kwargs)
end

def execute_query(*args, query:, **kwargs)
trace(proc { super }, 'execute', query.selected_operation_name, query: query) do |span|
span.set_tag('graphql.source', query.query_string)
span.set_tag('graphql.operation.type', query.selected_operation.operation_type)
span.set_tag('graphql.operation.name', query.selected_operation_name) if query.selected_operation_name
query.variables.instance_variable_get(:@storage).each do |key, value|
span.set_tag("graphql.variables.#{key}", value)
end
end
trace(
proc { super },
'execute',
query.selected_operation_name,
lambda { |span|
span.set_tag('graphql.source', query.query_string)
span.set_tag('graphql.operation.type', query.selected_operation.operation_type)
if query.selected_operation_name
span.set_tag(
'graphql.operation.name',
query.selected_operation_name
)
end
query.variables.instance_variable_get(:@storage).each do |key, value|
span.set_tag("graphql.variables.#{key}", value)
end
},
->(span) { add_query_error_events(span, query.context.errors) },
query: query,
)
end

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

private

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

Tracing.trace(
Expand All @@ -144,11 +165,17 @@ def trace(callable, trace_key, resource, **kwargs)
Contrib::Analytics.set_sample_rate(span, config[:analytics_sample_rate])
end

yield(span) if block_given?
if before_callable = before || before_block
before_callable.call(span)
end

prepare_span(trace_key, kwargs, span) if @has_prepare_span

callable.call
ret = callable.call

after.call(span) if after

ret
end
end

Expand All @@ -163,6 +190,44 @@ def multiplex_resource(multiplex)
operations
end
end

# Create a Span Event for each error that occurs at query level.
#
# These are represented in the Datadog App as special GraphQL errors,
# given their event name `dd.graphql.query.error`.
def add_query_error_events(span, errors)
errors.each do |error|
e = Core::Error.build_from(error)
err = error.to_h

span.span_events << Datadog::Tracing::SpanEvent.new(
Ext::EVENT_QUERY_ERROR,
attributes: {
message: err['message'],
type: e.type,
stacktrace: e.backtrace,
locations: serialize_error_locations(err['locations']),
path: err['path'],
}
)
end
end

# Serialize error's `locations` array as an array of Strings, given
# Span Events do not support hashes nested inside arrays.
#
# Here's an example in which `locations`:
# [
# {"line" => 3, "column" => 10},
# {"line" => 7, "column" => 8},
# ]
# is serialized as:
# ["3:10", "7:8"]
def serialize_error_locations(locations)
locations.map do |location|
"#{location['line']}:#{location['column']}"
end
end
end
end
end
Expand Down
24 changes: 14 additions & 10 deletions sig/datadog/core/error.rbs
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
module Datadog
module Core
class Error
attr_reader type: untyped
attr_reader type: String

attr_reader message: untyped
attr_reader message: String

attr_reader backtrace: untyped
attr_reader backtrace: String

def self.build_from: (untyped value) -> untyped
interface _ContainsMessage
def message: () -> String
def class: () -> Class
end

def self.build_from: ((Error | Array[untyped] | ::Exception | _ContainsMessage | ::String) value) -> Error

private
def self.full_backtrace: (untyped ex) -> untyped
def self.backtrace_for: (untyped ex, untyped backtrace) -> (nil | untyped)
def self.full_backtrace: (Exception ex) -> String
def self.backtrace_for: (Exception ex, String backtrace) -> void

public

def initialize: (?untyped? `type`, ?untyped? message, ?untyped? backtrace) -> void

BlankError: untyped
def initialize: (?Object? `type`, ?Object? message, ?Object? backtrace) -> void

ContainsMessage: untyped
BlankError: Error
ContainsMessage: ^(Object) -> bool
end
end
end
2 changes: 2 additions & 0 deletions sig/datadog/tracing/contrib/graphql/ext.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ module Datadog

ENV_ANALYTICS_SAMPLE_RATE: "DD_TRACE_GRAPHQL_ANALYTICS_SAMPLE_RATE"

ENV_WITH_UNIFIED_TRACER: string
EVENT_QUERY_ERROR: String
SERVICE_NAME: "graphql"

TAG_COMPONENT: "graphql"
Expand Down
8 changes: 6 additions & 2 deletions sig/datadog/tracing/contrib/graphql/unified_trace.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,12 @@ module Datadog
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

type traceResult = lexerArray | GraphQL::Language::Nodes::Document | { remaining_timeout: Float?, error: Array[StandardError] } | Array[Object] | GraphQL::Schema::Object? | [GraphQL::Schema::Object, nil]

def trace: (Proc callable, String trace_key, String resource, **Hash[Symbol, traceKwargsValues ] kwargs) ?{ (Datadog::Tracing::SpanOperation) -> void } -> traceResult

def add_query_error_events: (SpanOperation span, Array[::GraphQL::Error] errors) -> void

def serialize_error_locations: (Array[{"line" => Integer, "column" => Integer}] locations)-> Array[String]

def trace: (Proc callable, String trace_key, String resource, ?Hash[Symbol, traceKwargsValues ] kwargs, ?before: ^(SpanOperation)-> void, ?after: ^(SpanOperation)-> void) ?{ (SpanOperation) -> void } -> traceResult

def multiplex_resource: (GraphQL::Execution::Multiplex multiplex) -> String?
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ def userByName(name:)
OpenStruct.new(id: 1, name: name)
end

field :unexpected_error, UserType, description: 'Raises error'

def unexpected_error
raise 'Unexpected error'
end

field :mutationUserByName, UserType, null: false, description: 'Find an user by name' do
argument :name, ::GraphQL::Types::String, required: true
end
Expand Down
54 changes: 52 additions & 2 deletions spec/datadog/tracing/contrib/graphql/test_schema_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,26 @@ class #{prefix}TestGraphQLQuery < ::GraphQL::Schema::Object
def user(id:)
OpenStruct.new(id: id, name: 'Bits')
end
field :graphql_error, ::GraphQL::Types::Int, description: 'Raises error'
def graphql_error
raise 'GraphQL error'
end
end
class #{prefix}TestGraphQLSchema < ::GraphQL::Schema
query(#{prefix}TestGraphQLQuery)
rescue_from(RuntimeError) do |err, obj, args, ctx, field|
raise GraphQL::ExecutionError.new(err.message, extensions: {
'int-1': 1,
'str-1': '1',
'array-1-2': [1,'2'],
'': 'empty string',
',': 'comma',
})
end
end
RUBY
# rubocop:enable Style/DocumentDynamicEvalDefinition
Expand Down Expand Up @@ -76,10 +92,11 @@ def unload_test_schema(prefix: '')
end

RSpec.shared_examples 'graphql instrumentation with unified naming convention trace' do |prefix: ''|
let(:schema) { Object.const_get("#{prefix}TestGraphQLSchema") }
let(:service) { defined?(super) ? super() : tracer.default_service }

describe 'query trace' do
subject(:result) { schema.execute(query: 'query Users($var: ID!){ user(id: $var) { name } }', variables: { var: 1 }) }
let(:schema) { Object.const_get("#{prefix}TestGraphQLSchema") }
let(:service) { defined?(super) ? super() : tracer.default_service }

matrix = [
['graphql.analyze', 'query Users($var: ID!){ user(id: $var) { name } }'],
Expand Down Expand Up @@ -134,4 +151,37 @@ def unload_test_schema(prefix: '')
end
end
end

describe 'query with a GraphQL error' do
subject(:result) { schema.execute(query: 'query Error{ graphqlError }', variables: { var: 1 }) }

let(:graphql_execute) { spans.find { |s| s.name == 'graphql.execute' } }

it 'creates query span for error' do
expect(result.to_h['errors'][0]['message']).to eq('GraphQL error')
expect(result.to_h['data']).to eq('graphqlError' => nil)

expect(graphql_execute.resource).to eq('Error')
expect(graphql_execute.service).to eq(service)
expect(graphql_execute.type).to eq('graphql')

expect(graphql_execute.get_tag('graphql.source')).to eq('query Error{ graphqlError }')

expect(graphql_execute.get_tag('graphql.operation.type')).to eq('query')
expect(graphql_execute.get_tag('graphql.operation.name')).to eq('Error')

expect(graphql_execute.events).to contain_exactly(
a_span_event_with(
name: 'dd.graphql.query.error',
attributes: {
'message' => 'GraphQL error',
'type' => 'GraphQL::ExecutionError',
'stacktrace' => include(__FILE__),
'locations' => ['1:14'],
'path' => ['graphqlError'],
}
)
)
end
end
end
10 changes: 10 additions & 0 deletions spec/support/span_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,14 @@ def description_of(actual)
end
end
end

RSpec::Matchers.define :a_span_event_with do |expected|
match do |actual|
values_match? Datadog::Tracing::SpanEvent, actual

expected.all? do |key, value|
values_match? value, actual.__send__(key)
end
end
end
end
4 changes: 4 additions & 0 deletions vendor/rbs/graphql/0/graphql.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ module GraphQL
class Multiplex
end
end

class Error
def to_h: -> Hash[String, untyped]
end
end

0 comments on commit 418a657

Please sign in to comment.