Skip to content

Commit fea435f

Browse files
authored
Merge pull request #4077 from toneymathews/configure-platform-tracing-for-per-request-tracing
Add query execution context based tracing for API requests
2 parents 12a80ad + bbf7bb5 commit fea435f

File tree

5 files changed

+313
-4
lines changed

5 files changed

+313
-4
lines changed

lib/graphql/tracing.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require "graphql/tracing/scout_tracing"
99
require "graphql/tracing/statsd_tracing"
1010
require "graphql/tracing/prometheus_tracing"
11+
require "graphql/tracing/opentelemetry_tracing"
1112

1213
if defined?(PrometheusExporter::Server)
1314
require "graphql/tracing/prometheus_tracing/graphql_collector"
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# frozen_string_literal: true
2+
3+
module GraphQL
4+
module Tracing
5+
class OpenTelemetryTracing < PlatformTracing
6+
self.platform_keys = {
7+
'lex' => 'graphql.lex',
8+
'parse' => 'graphql.parse',
9+
'validate' => 'graphql.validate',
10+
'analyze_query' => 'graphql.analyze_query',
11+
'analyze_multiplex' => 'graphql.analyze_multiplex',
12+
'execute_query' => 'graphql.execute_query',
13+
'execute_query_lazy' => 'graphql.execute_query_lazy',
14+
'execute_multiplex' => 'graphql.execute_multiplex'
15+
}
16+
17+
def platform_trace(platform_key, key, data)
18+
return yield if platform_key.nil?
19+
20+
tracer.in_span(platform_key, attributes: attributes_for(key, data)) do |span, _context|
21+
yield.tap do |response|
22+
errors = response[:errors]&.compact&.map { |e| e.to_h }&.to_json if key == 'validate'
23+
unless errors.nil?
24+
span.add_event(
25+
'graphql.validation.error',
26+
attributes: {
27+
'message' => errors
28+
}
29+
)
30+
end
31+
end
32+
end
33+
end
34+
35+
def platform_field_key(type, field)
36+
"#{type.graphql_name}.#{field.graphql_name}"
37+
end
38+
39+
def platform_authorized_key(type)
40+
"#{type.graphql_name}.authorized"
41+
end
42+
43+
def platform_resolve_type_key(type)
44+
"#{type.graphql_name}.resolve_type"
45+
end
46+
47+
private
48+
49+
def tracer
50+
OpenTelemetry::Instrumentation::GraphQL::Instrumentation.instance.tracer
51+
end
52+
53+
def config
54+
OpenTelemetry::Instrumentation::GraphQL::Instrumentation.instance.config
55+
end
56+
57+
def platform_key_enabled?(ctx, key)
58+
return false unless config[key]
59+
60+
ns = ctx.namespace(:opentelemetry)
61+
return true if ns.empty? # restores original behavior so that keys are returned if tracing is not set in context.
62+
return false unless ns.key?(key) && ns[key]
63+
64+
return true
65+
end
66+
67+
def attributes_for(key, data)
68+
attributes = {}
69+
case key
70+
when 'execute_query'
71+
attributes['selected_operation_name'] = data[:query].selected_operation_name if data[:query].selected_operation_name
72+
attributes['selected_operation_type'] = data[:query].selected_operation.operation_type
73+
attributes['query_string'] = data[:query].query_string
74+
end
75+
attributes
76+
end
77+
78+
def cached_platform_key(ctx, key, trace_phase)
79+
cache = ctx.namespace(self.class)[:platform_key_cache] ||= {}
80+
81+
cache.fetch(key) do
82+
cache[key] = if trace_phase == :field
83+
return unless platform_key_enabled?(ctx, :enable_platform_field)
84+
85+
yield
86+
elsif trace_phase == :authorized
87+
return unless platform_key_enabled?(ctx, :enable_platform_authorized)
88+
89+
yield
90+
elsif trace_phase == :resolve_type
91+
return unless platform_key_enabled?(ctx, :enable_platform_resolve_type)
92+
93+
yield
94+
else
95+
raise "Unknown trace phase"
96+
end
97+
end
98+
end
99+
end
100+
end
101+
end

lib/graphql/tracing/platform_tracing.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def trace(key, data)
4545

4646
platform_key = if trace_field
4747
context = data.fetch(:query).context
48-
cached_platform_key(context, field) { platform_field_key(data[:owner], field) }
48+
cached_platform_key(context, field, :field) { platform_field_key(data[:owner], field) }
4949
else
5050
nil
5151
end
@@ -61,14 +61,14 @@ def trace(key, data)
6161
when "authorized", "authorized_lazy"
6262
type = data.fetch(:type)
6363
context = data.fetch(:context)
64-
platform_key = cached_platform_key(context, type) { platform_authorized_key(type) }
64+
platform_key = cached_platform_key(context, type, :authorized) { platform_authorized_key(type) }
6565
platform_trace(platform_key, key, data) do
6666
yield
6767
end
6868
when "resolve_type", "resolve_type_lazy"
6969
type = data.fetch(:type)
7070
context = data.fetch(:context)
71-
platform_key = cached_platform_key(context, type) { platform_resolve_type_key(type) }
71+
platform_key = cached_platform_key(context, type, :resolve_type) { platform_resolve_type_key(type) }
7272
platform_trace(platform_key, key, data) do
7373
yield
7474
end
@@ -116,7 +116,7 @@ def fallback_transaction_name(context)
116116
# If the key isn't present, the given block is called and the result is cached for `key`.
117117
#
118118
# @return [String]
119-
def cached_platform_key(ctx, key)
119+
def cached_platform_key(ctx, key, trace_phase)
120120
cache = ctx.namespace(self.class)[:platform_key_cache] ||= {}
121121
cache.fetch(key) { cache[key] = yield }
122122
end
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# frozen_string_literal: true
2+
require "spec_helper"
3+
4+
describe ::GraphQL::Tracing::OpenTelemetryTracing do
5+
module OpenTelemetryTest
6+
class Thing < GraphQL::Schema::Object
7+
implements GraphQL::Types::Relay::Node
8+
end
9+
10+
class Query < GraphQL::Schema::Object
11+
include GraphQL::Types::Relay::HasNodeField
12+
13+
field :int, Integer, null: false
14+
15+
def int
16+
1
17+
end
18+
end
19+
20+
class SchemaWithPerRequestTracing < GraphQL::Schema
21+
query(Query)
22+
use(GraphQL::Tracing::OpenTelemetryTracing)
23+
orphan_types(Thing)
24+
25+
def self.object_from_id(_id, _ctx)
26+
:thing
27+
end
28+
29+
def self.resolve_type(_type, _obj, _ctx)
30+
Thing
31+
end
32+
end
33+
end
34+
35+
before do
36+
OpenTelemetry::Instrumentation::GraphQL::Instrumentation.clear_all
37+
end
38+
39+
it "captures all keys when tracing is enabled in config and in query execution context" do
40+
query = GraphQL::Query.new(OpenTelemetryTest::SchemaWithPerRequestTracing, '{ node(id: "1") { __typename } }')
41+
query.context.namespace(:opentelemetry)[:enable_platform_field] = true
42+
query.context.namespace(:opentelemetry)[:enable_platform_authorized] = true
43+
query.context.namespace(:opentelemetry)[:enable_platform_resolve_type] = true
44+
45+
query.result
46+
47+
assert_span("Query.authorized")
48+
assert_span("Query.node")
49+
assert_span("Node.resolve_type")
50+
assert_span("Thing.authorized")
51+
assert_span("DynamicFields.authorized")
52+
end
53+
54+
it "does not capture the keys when tracing is not enabled in config but is enabled in query execution context" do
55+
OpenTelemetry::Instrumentation::GraphQL::Instrumentation.instance.stub(:config, {
56+
schemas: [],
57+
enable_platform_field: false,
58+
enable_platform_authorized: false,
59+
enable_platform_resolve_type: false
60+
}) do
61+
62+
query = GraphQL::Query.new(OpenTelemetryTest::SchemaWithPerRequestTracing, '{ node(id: "1") { __typename } }')
63+
query.context.namespace(:opentelemetry)[:enable_platform_field] = true
64+
query.context.namespace(:opentelemetry)[:enable_platform_authorized] = true
65+
query.context.namespace(:opentelemetry)[:enable_platform_resolve_type] = true
66+
67+
query.result
68+
69+
refute_span("Query.authorized")
70+
refute_span("Query.node")
71+
refute_span("Node.resolve_type")
72+
refute_span("Thing.authorized")
73+
refute_span("DynamicFields.authorized")
74+
end
75+
end
76+
77+
it "does not capture any key when tracing is not enabled in config and tracing is not set in context" do
78+
OpenTelemetry::Instrumentation::GraphQL::Instrumentation.instance.stub(:config, {
79+
schemas: [],
80+
enable_platform_field: false,
81+
enable_platform_authorized: false,
82+
enable_platform_resolve_type: false
83+
}) do
84+
85+
query = GraphQL::Query.new(OpenTelemetryTest::SchemaWithPerRequestTracing, '{ node(id: "1") { __typename } }')
86+
87+
query.result
88+
89+
refute_span("Query.authorized")
90+
refute_span("Query.node")
91+
refute_span("Node.resolve_type")
92+
refute_span("Thing.authorized")
93+
refute_span("DynamicFields.authorized")
94+
end
95+
end
96+
97+
it "does not capture any key when tracing is not enabled in config and context" do
98+
OpenTelemetry::Instrumentation::GraphQL::Instrumentation.instance.stub(:config, {
99+
schemas: [],
100+
enable_platform_field: false,
101+
enable_platform_authorized: false,
102+
enable_platform_resolve_type: false
103+
}) do
104+
105+
query = GraphQL::Query.new(OpenTelemetryTest::SchemaWithPerRequestTracing, '{ node(id: "1") { __typename } }')
106+
query.context.namespace(:opentelemetry)[:enable_platform_field] = false
107+
query.context.namespace(:opentelemetry)[:enable_platform_authorized] = false
108+
query.context.namespace(:opentelemetry)[:enable_platform_resolve_type] = false
109+
110+
query.result
111+
112+
refute_span("Query.authorized")
113+
refute_span("Query.node")
114+
refute_span("Node.resolve_type")
115+
refute_span("Thing.authorized")
116+
refute_span("DynamicFields.authorized")
117+
end
118+
end
119+
120+
it "captures all keys when tracing is enabled in config but is not set in context" do
121+
query = GraphQL::Query.new(OpenTelemetryTest::SchemaWithPerRequestTracing, '{ node(id: "1") { __typename } }')
122+
123+
query.result
124+
125+
assert_span("Query.authorized")
126+
assert_span("Query.node")
127+
assert_span("Node.resolve_type")
128+
assert_span("Thing.authorized")
129+
assert_span("DynamicFields.authorized")
130+
end
131+
132+
it "does not capture any key when tracing is enabled in config but is not enabled in context" do
133+
query = GraphQL::Query.new(OpenTelemetryTest::SchemaWithPerRequestTracing, '{ node(id: "1") { __typename } }')
134+
query.context.namespace(:opentelemetry)[:enable_platform_field] = false
135+
query.context.namespace(:opentelemetry)[:enable_platform_authorized] = false
136+
query.context.namespace(:opentelemetry)[:enable_platform_resolve_type] = false
137+
138+
query.result
139+
140+
refute_span("Query.authorized")
141+
refute_span("Query.node")
142+
refute_span("Node.resolve_type")
143+
refute_span("Thing.authorized")
144+
refute_span("DynamicFields.authorized")
145+
end
146+
147+
private
148+
149+
def assert_span(span)
150+
assert OpenTelemetry::Instrumentation::GraphQL::Instrumentation::EVENTS.include?(span)
151+
end
152+
153+
def refute_span(span)
154+
refute OpenTelemetry::Instrumentation::GraphQL::Instrumentation::EVENTS.include?(span)
155+
end
156+
end

spec/support/opentelemetry.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
# A stub for the Opentelemetry agent, so we can make assertions about how it is used
3+
if defined?(OpenTelemetry)
4+
raise "Expected Opentelemetry to be undefined, so that we could define a stub for it."
5+
end
6+
7+
module OpenTelemetry
8+
module Instrumentation
9+
module GraphQL
10+
class Instrumentation
11+
EVENTS = []
12+
class << self
13+
def instance
14+
@instance ||= new
15+
end
16+
17+
def clear_all
18+
EVENTS.clear
19+
end
20+
end
21+
22+
def tracer
23+
@tracer ||= DummyTracer.new
24+
end
25+
26+
def config
27+
@config ||= {
28+
schemas: [],
29+
enable_platform_field: true,
30+
enable_platform_authorized: true,
31+
enable_platform_resolve_type: true
32+
}
33+
end
34+
end
35+
36+
class DummyTracer
37+
class TestSpan
38+
def add_event(name, attributes:)
39+
self
40+
end
41+
end
42+
43+
def in_span(name, attributes: nil, links: nil, start_timestamp: nil, kind: nil)
44+
OpenTelemetry::Instrumentation::GraphQL::Instrumentation::EVENTS << name
45+
46+
yield(TestSpan.new, {})
47+
end
48+
end
49+
end
50+
end
51+
end

0 commit comments

Comments
 (0)