Skip to content

Commit 1479664

Browse files
authored
Merge pull request #4775 from patch0/add-sentry-tracing
Add Sentry tracing
2 parents 0f32887 + e9c0314 commit 1479664

File tree

7 files changed

+264
-13
lines changed

7 files changed

+264
-13
lines changed

guides/queries/sentry_example.png

174 KB
Loading

guides/queries/tracing.md

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ By default, GraphQL-Ruby makes a new trace instance when it runs a query. You ca
3939
You can attach a trace module to run only in some circumstances by using `mode:`. For example, to add detailed tracing for only some requests:
4040

4141
```ruby
42-
trace_with DetailedTracing, mode: :detailed_metrics
42+
trace_with DetailedTrace, mode: :detailed_metrics
4343
```
4444

4545
Then, to opt into that trace, use `context: { trace_mode: :detailed_metrics, ... }` when executing queries.
@@ -75,7 +75,7 @@ tracing as follows:
7575
require 'appoptics_apm'
7676

7777
class MySchema < GraphQL::Schema
78-
use(GraphQL::Tracing::AppOpticsTracing)
78+
trace_with GraphQL::Tracing::AppOpticsTrace
7979
end
8080
```
8181
<div class="monitoring-img-group">
@@ -88,7 +88,7 @@ To add [AppSignal](https://appsignal.com/) instrumentation:
8888

8989
```ruby
9090
class MySchema < GraphQL::Schema
91-
use(GraphQL::Tracing::AppsignalTracing)
91+
trace_with GraphQL::Tracing::AppsignalTrace
9292
end
9393
```
9494

@@ -102,9 +102,9 @@ To add [New Relic](https://newrelic.com/) instrumentation:
102102

103103
```ruby
104104
class MySchema < GraphQL::Schema
105-
use(GraphQL::Tracing::NewRelicTracing)
105+
trace_with GraphQL::Tracing::NewRelicTrace
106106
# Optional, use the operation name to set the new relic transaction name:
107-
# use(GraphQL::Tracing::NewRelicTracing, set_transaction_name: true)
107+
# trace_with GraphQL::Tracing::NewRelicTrace, set_transaction_name: true
108108
end
109109
```
110110

@@ -119,7 +119,7 @@ To add [Scout APM](https://scoutapp.com/) instrumentation:
119119

120120
```ruby
121121
class MySchema < GraphQL::Schema
122-
use(GraphQL::Tracing::ScoutTracing)
122+
trace_with GraphQL::Tracing::ScoutTrace
123123
end
124124
```
125125

@@ -148,7 +148,7 @@ To add [Datadog](https://www.datadoghq.com) instrumentation:
148148

149149
```ruby
150150
class MySchema < GraphQL::Schema
151-
use(GraphQL::Tracing::DataDogTracing, options)
151+
trace_with GraphQL::Tracing::DataDogTrace, options
152152
end
153153
```
154154

@@ -169,7 +169,7 @@ To add [Prometheus](https://prometheus.io) instrumentation:
169169
require 'prometheus_exporter/client'
170170

171171
class MySchema < GraphQL::Schema
172-
use(GraphQL::Tracing::PrometheusTracing)
172+
trace_with GraphQL::Tracing::PrometheusTrace
173173
end
174174
```
175175

@@ -181,7 +181,7 @@ The PrometheusExporter server must be run with a custom type collector that exte
181181
if defined?(PrometheusExporter::Server)
182182
require 'graphql/tracing'
183183

184-
class GraphQLCollector < GraphQL::Tracing::PrometheusTracing::GraphQLCollector
184+
class GraphQLCollector < GraphQL::Tracing::PrometheusTrace::GraphQLCollector
185185
end
186186
end
187187
```
@@ -190,16 +190,31 @@ end
190190
bundle exec prometheus_exporter -a lib/graphql_collector.rb
191191
```
192192

193+
## Sentry
194+
195+
To add [Sentry](https://sentry.io) instrumentation:
196+
197+
```ruby
198+
class MySchema < GraphQL::Schema
199+
trace_with GraphQL::Tracing::SentryTrace
200+
end
201+
```
202+
203+
<div class="monitoring-img-group">
204+
{{ "/queries/sentry_example.png" | link_to_img:"sentry monitoring" }}
205+
</div>
206+
207+
193208
## Statsd
194209

195-
You can add Statsd instrumentation by initializing a statsd client and passing it to {{ "GraphQL::Tracing::StatsdTracing" | api_doc }}:
210+
You can add Statsd instrumentation by initializing a statsd client and passing it to {{ "GraphQL::Tracing::StatsdTrace" | api_doc }}:
196211

197212
```ruby
198213
$statsd = Statsd.new 'localhost', 9125
199214
# ...
200215

201216
class MySchema < GraphQL::Schema
202-
use GraphQL::Tracing::StatsdTracing, statsd: $statsd
217+
use GraphQL::Tracing::StatsdTrace, statsd: $statsd
203218
end
204219
```
205220

lib/graphql/tracing.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
require "graphql/tracing/data_dog_trace"
2222
require "graphql/tracing/new_relic_trace"
2323
require "graphql/tracing/notifications_trace"
24+
require "graphql/tracing/sentry_trace"
2425
require "graphql/tracing/scout_trace"
2526
require "graphql/tracing/statsd_trace"
2627
require "graphql/tracing/prometheus_trace"
2728
if defined?(PrometheusExporter::Server)
28-
require "graphql/tracing/prometheus_tracing/graphql_collector"
29+
require "graphql/tracing/prometheus_trace/graphql_collector"
2930
end
3031

3132
module GraphQL

lib/graphql/tracing/prometheus_tracing/graphql_collector.rb renamed to lib/graphql/tracing/prometheus_trace/graphql_collector.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module GraphQL
44
module Tracing
5-
class PrometheusTracing < PlatformTracing
5+
module PrometheusTrace
66
class GraphQLCollector < ::PrometheusExporter::Server::TypeCollector
77
def initialize
88
@graphql_gauge = PrometheusExporter::Metric::Base.default_aggregation.new(
@@ -28,5 +28,7 @@ def metrics
2828
end
2929
end
3030
end
31+
# Backwards-compat:
32+
PrometheusTracing::GraphQLCollector = PrometheusTrace::GraphQLCollector
3133
end
3234
end
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# frozen_string_literal: true
2+
3+
module GraphQL
4+
module Tracing
5+
module SentryTrace
6+
include PlatformTrace
7+
8+
{
9+
"lex" => "graphql.lex",
10+
"parse" => "graphql.parse",
11+
"validate" => "graphql.validate",
12+
"analyze_query" => "graphql.analyze",
13+
"analyze_multiplex" => "graphql.analyze_multiplex",
14+
"execute_multiplex" => "graphql.execute_multiplex",
15+
"execute_query" => "graphql.execute",
16+
"execute_query_lazy" => "graphql.execute"
17+
}.each do |trace_method, platform_key|
18+
module_eval <<-RUBY, __FILE__, __LINE__
19+
def #{trace_method}(**data, &block)
20+
instrument_execution("#{platform_key}", "#{trace_method}", data, &block)
21+
end
22+
RUBY
23+
end
24+
25+
def platform_execute_field(platform_key, &block)
26+
instrument_execution(platform_key, "execute_field", &block)
27+
end
28+
29+
def platform_execute_field_lazy(platform_key, &block)
30+
instrument_execution(platform_key, "execute_field_lazy", &block)
31+
end
32+
33+
def platform_authorized(platform_key, &block)
34+
instrument_execution(platform_key, "authorized", &block)
35+
end
36+
37+
def platform_authorized_lazy(platform_key, &block)
38+
instrument_execution(platform_key, "authorized_lazy", &block)
39+
end
40+
41+
def platform_resolve_type(platform_key, &block)
42+
instrument_execution(platform_key, "resolve_type", &block)
43+
end
44+
45+
def platform_resolve_type_lazy(platform_key, &block)
46+
instrument_execution(platform_key, "resolve_type_lazy", &block)
47+
end
48+
49+
def platform_field_key(field)
50+
"graphql.field.#{field.path}"
51+
end
52+
53+
def platform_authorized_key(type)
54+
"graphql.authorized.#{type.graphql_name}"
55+
end
56+
57+
def platform_resolve_type_key(type)
58+
"graphql.resolve_type.#{type.graphql_name}"
59+
end
60+
61+
private
62+
63+
def instrument_execution(platform_key, trace_method, data=nil, &block)
64+
return yield unless Sentry.initialized?
65+
66+
Sentry.with_child_span(op: platform_key, start_timestamp: Sentry.utc_now.to_f) do |span|
67+
result = block.call
68+
span.finish
69+
70+
if trace_method == "execute_multiplex" && data.key?(:multiplex)
71+
operation_names = data[:multiplex].queries.map{|q| operation_name(q) }
72+
span.set_description(operation_names.join(", "))
73+
elsif trace_method == "execute_query" && data.key?(:query)
74+
span.set_description(operation_name(data[:query]))
75+
span.set_data('graphql.document', data[:query].query_string)
76+
span.set_data('graphql.operation.name', data[:query].selected_operation_name) if data[:query].selected_operation_name
77+
span.set_data('graphql.operation.type', data[:query].selected_operation.operation_type)
78+
end
79+
80+
result
81+
end
82+
end
83+
84+
def operation_name(query)
85+
selected_op = query.selected_operation
86+
if selected_op
87+
[selected_op.operation_type, selected_op.name].compact.join(' ')
88+
else
89+
'GraphQL Operation'
90+
end
91+
end
92+
end
93+
end
94+
end
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
describe GraphQL::Tracing::SentryTrace do
6+
class SentryTraceTestSchema < GraphQL::Schema
7+
class Thing < GraphQL::Schema::Object
8+
field :str, String
9+
def str; "blah"; end
10+
end
11+
12+
class Query < GraphQL::Schema::Object
13+
field :int, Integer, null: false
14+
15+
def int
16+
1
17+
end
18+
19+
field :thing, Thing
20+
def thing; :thing; end
21+
end
22+
23+
query(Query)
24+
25+
trace_with GraphQL::Tracing::SentryTrace
26+
end
27+
28+
before do
29+
Sentry.clear_all
30+
end
31+
32+
describe "When Sentry is not configured" do
33+
it "does not initialize any spans" do
34+
Sentry.stub(:initialized?, false) do
35+
SentryTraceTestSchema.execute("{ int thing { str } }")
36+
assert_equal [], Sentry::SPAN_DATA
37+
assert_equal [], Sentry::SPAN_DESCRIPTIONS
38+
assert_equal [], Sentry::SPAN_OPS
39+
end
40+
end
41+
end
42+
43+
it "sets the expected spans" do
44+
SentryTraceTestSchema.execute("{ int thing { str } }")
45+
expected_span_ops = [
46+
"graphql.execute_multiplex",
47+
"graphql.analyze_multiplex",
48+
(USING_C_PARSER ? "graphql.lex" : nil),
49+
"graphql.parse",
50+
"graphql.validate",
51+
"graphql.analyze",
52+
"graphql.execute",
53+
"graphql.authorized.Query",
54+
"graphql.field.Query.thing",
55+
"graphql.authorized.Thing",
56+
"graphql.execute"
57+
].compact
58+
59+
assert_equal expected_span_ops, Sentry::SPAN_OPS
60+
end
61+
62+
it "sets span descriptions for an anonymous query" do
63+
SentryTraceTestSchema.execute("{ int }")
64+
65+
assert_equal ["query", "query"], Sentry::SPAN_DESCRIPTIONS
66+
end
67+
68+
it "sets span data for an anonymous query" do
69+
SentryTraceTestSchema.execute("{ int }")
70+
expected_span_data = [
71+
["graphql.document", "{ int }"],
72+
["graphql.operation.type", "query"]
73+
].compact
74+
75+
assert_equal expected_span_data.sort, Sentry::SPAN_DATA.sort
76+
end
77+
78+
it "sets span descriptions for a named query" do
79+
SentryTraceTestSchema.execute("query Ab { int }")
80+
81+
assert_equal ["query Ab", "query Ab"], Sentry::SPAN_DESCRIPTIONS
82+
end
83+
84+
it "sets span data for a named query" do
85+
SentryTraceTestSchema.execute("query Ab { int }")
86+
expected_span_data = [
87+
["graphql.document", "query Ab { int }"],
88+
["graphql.operation.name", "Ab"],
89+
["graphql.operation.type", "query"]
90+
].compact
91+
92+
assert_equal expected_span_data.sort, Sentry::SPAN_DATA.sort
93+
end
94+
end

spec/support/sentry.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
3+
# A stub for the Sentry agent, so we can make assertions about how it is used
4+
if defined?(Sentry)
5+
raise "Expected Sentry to be undefined, so that we could define a stub for it."
6+
end
7+
8+
module Sentry
9+
SPAN_OPS = []
10+
SPAN_DATA = []
11+
SPAN_DESCRIPTIONS = []
12+
13+
def self.initialized?
14+
true
15+
end
16+
17+
def self.utc_now
18+
Time.now.utc
19+
end
20+
21+
def self.with_child_span(**args, &block)
22+
SPAN_OPS << args[:op]
23+
yield DummySpan.new
24+
end
25+
26+
def self.clear_all
27+
SPAN_DATA.clear
28+
SPAN_DESCRIPTIONS.clear
29+
SPAN_OPS.clear
30+
end
31+
32+
class DummySpan
33+
def set_data(key, value)
34+
Sentry::SPAN_DATA << [key, value]
35+
end
36+
37+
def set_description(description)
38+
Sentry::SPAN_DESCRIPTIONS << description
39+
end
40+
41+
def finish
42+
# no-op
43+
end
44+
end
45+
end

0 commit comments

Comments
 (0)