Skip to content

Commit

Permalink
Merge pull request #4775 from patch0/add-sentry-tracing
Browse files Browse the repository at this point in the history
Add Sentry tracing
  • Loading branch information
rmosolgo authored Jan 12, 2024
2 parents 0f32887 + e9c0314 commit 1479664
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 13 deletions.
Binary file added guides/queries/sentry_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 26 additions & 11 deletions guides/queries/tracing.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ By default, GraphQL-Ruby makes a new trace instance when it runs a query. You ca
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:

```ruby
trace_with DetailedTracing, mode: :detailed_metrics
trace_with DetailedTrace, mode: :detailed_metrics
```

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

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

```ruby
class MySchema < GraphQL::Schema
use(GraphQL::Tracing::AppsignalTracing)
trace_with GraphQL::Tracing::AppsignalTrace
end
```

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

```ruby
class MySchema < GraphQL::Schema
use(GraphQL::Tracing::NewRelicTracing)
trace_with GraphQL::Tracing::NewRelicTrace
# Optional, use the operation name to set the new relic transaction name:
# use(GraphQL::Tracing::NewRelicTracing, set_transaction_name: true)
# trace_with GraphQL::Tracing::NewRelicTrace, set_transaction_name: true
end
```

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

```ruby
class MySchema < GraphQL::Schema
use(GraphQL::Tracing::ScoutTracing)
trace_with GraphQL::Tracing::ScoutTrace
end
```

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

```ruby
class MySchema < GraphQL::Schema
use(GraphQL::Tracing::DataDogTracing, options)
trace_with GraphQL::Tracing::DataDogTrace, options
end
```

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

class MySchema < GraphQL::Schema
use(GraphQL::Tracing::PrometheusTracing)
trace_with GraphQL::Tracing::PrometheusTrace
end
```

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

class GraphQLCollector < GraphQL::Tracing::PrometheusTracing::GraphQLCollector
class GraphQLCollector < GraphQL::Tracing::PrometheusTrace::GraphQLCollector
end
end
```
Expand All @@ -190,16 +190,31 @@ end
bundle exec prometheus_exporter -a lib/graphql_collector.rb
```

## Sentry

To add [Sentry](https://sentry.io) instrumentation:

```ruby
class MySchema < GraphQL::Schema
trace_with GraphQL::Tracing::SentryTrace
end
```

<div class="monitoring-img-group">
{{ "/queries/sentry_example.png" | link_to_img:"sentry monitoring" }}
</div>


## Statsd

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

```ruby
$statsd = Statsd.new 'localhost', 9125
# ...

class MySchema < GraphQL::Schema
use GraphQL::Tracing::StatsdTracing, statsd: $statsd
use GraphQL::Tracing::StatsdTrace, statsd: $statsd
end
```

Expand Down
3 changes: 2 additions & 1 deletion lib/graphql/tracing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
require "graphql/tracing/data_dog_trace"
require "graphql/tracing/new_relic_trace"
require "graphql/tracing/notifications_trace"
require "graphql/tracing/sentry_trace"
require "graphql/tracing/scout_trace"
require "graphql/tracing/statsd_trace"
require "graphql/tracing/prometheus_trace"
if defined?(PrometheusExporter::Server)
require "graphql/tracing/prometheus_tracing/graphql_collector"
require "graphql/tracing/prometheus_trace/graphql_collector"
end

module GraphQL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module GraphQL
module Tracing
class PrometheusTracing < PlatformTracing
module PrometheusTrace
class GraphQLCollector < ::PrometheusExporter::Server::TypeCollector
def initialize
@graphql_gauge = PrometheusExporter::Metric::Base.default_aggregation.new(
Expand All @@ -28,5 +28,7 @@ def metrics
end
end
end
# Backwards-compat:
PrometheusTracing::GraphQLCollector = PrometheusTrace::GraphQLCollector
end
end
94 changes: 94 additions & 0 deletions lib/graphql/tracing/sentry_trace.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# frozen_string_literal: true

module GraphQL
module Tracing
module SentryTrace
include PlatformTrace

{
"lex" => "graphql.lex",
"parse" => "graphql.parse",
"validate" => "graphql.validate",
"analyze_query" => "graphql.analyze",
"analyze_multiplex" => "graphql.analyze_multiplex",
"execute_multiplex" => "graphql.execute_multiplex",
"execute_query" => "graphql.execute",
"execute_query_lazy" => "graphql.execute"
}.each do |trace_method, platform_key|
module_eval <<-RUBY, __FILE__, __LINE__
def #{trace_method}(**data, &block)
instrument_execution("#{platform_key}", "#{trace_method}", data, &block)
end
RUBY
end

def platform_execute_field(platform_key, &block)
instrument_execution(platform_key, "execute_field", &block)
end

def platform_execute_field_lazy(platform_key, &block)
instrument_execution(platform_key, "execute_field_lazy", &block)
end

def platform_authorized(platform_key, &block)
instrument_execution(platform_key, "authorized", &block)
end

def platform_authorized_lazy(platform_key, &block)
instrument_execution(platform_key, "authorized_lazy", &block)
end

def platform_resolve_type(platform_key, &block)
instrument_execution(platform_key, "resolve_type", &block)
end

def platform_resolve_type_lazy(platform_key, &block)
instrument_execution(platform_key, "resolve_type_lazy", &block)
end

def platform_field_key(field)
"graphql.field.#{field.path}"
end

def platform_authorized_key(type)
"graphql.authorized.#{type.graphql_name}"
end

def platform_resolve_type_key(type)
"graphql.resolve_type.#{type.graphql_name}"
end

private

def instrument_execution(platform_key, trace_method, data=nil, &block)
return yield unless Sentry.initialized?

Sentry.with_child_span(op: platform_key, start_timestamp: Sentry.utc_now.to_f) do |span|
result = block.call
span.finish

if trace_method == "execute_multiplex" && data.key?(:multiplex)
operation_names = data[:multiplex].queries.map{|q| operation_name(q) }
span.set_description(operation_names.join(", "))
elsif trace_method == "execute_query" && data.key?(:query)
span.set_description(operation_name(data[:query]))
span.set_data('graphql.document', data[:query].query_string)
span.set_data('graphql.operation.name', data[:query].selected_operation_name) if data[:query].selected_operation_name
span.set_data('graphql.operation.type', data[:query].selected_operation.operation_type)
end

result
end
end

def operation_name(query)
selected_op = query.selected_operation
if selected_op
[selected_op.operation_type, selected_op.name].compact.join(' ')
else
'GraphQL Operation'
end
end
end
end
end
94 changes: 94 additions & 0 deletions spec/graphql/tracing/sentry_trace_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# frozen_string_literal: true

require "spec_helper"

describe GraphQL::Tracing::SentryTrace do
class SentryTraceTestSchema < GraphQL::Schema
class Thing < GraphQL::Schema::Object
field :str, String
def str; "blah"; end
end

class Query < GraphQL::Schema::Object
field :int, Integer, null: false

def int
1
end

field :thing, Thing
def thing; :thing; end
end

query(Query)

trace_with GraphQL::Tracing::SentryTrace
end

before do
Sentry.clear_all
end

describe "When Sentry is not configured" do
it "does not initialize any spans" do
Sentry.stub(:initialized?, false) do
SentryTraceTestSchema.execute("{ int thing { str } }")
assert_equal [], Sentry::SPAN_DATA
assert_equal [], Sentry::SPAN_DESCRIPTIONS
assert_equal [], Sentry::SPAN_OPS
end
end
end

it "sets the expected spans" do
SentryTraceTestSchema.execute("{ int thing { str } }")
expected_span_ops = [
"graphql.execute_multiplex",
"graphql.analyze_multiplex",
(USING_C_PARSER ? "graphql.lex" : nil),
"graphql.parse",
"graphql.validate",
"graphql.analyze",
"graphql.execute",
"graphql.authorized.Query",
"graphql.field.Query.thing",
"graphql.authorized.Thing",
"graphql.execute"
].compact

assert_equal expected_span_ops, Sentry::SPAN_OPS
end

it "sets span descriptions for an anonymous query" do
SentryTraceTestSchema.execute("{ int }")

assert_equal ["query", "query"], Sentry::SPAN_DESCRIPTIONS
end

it "sets span data for an anonymous query" do
SentryTraceTestSchema.execute("{ int }")
expected_span_data = [
["graphql.document", "{ int }"],
["graphql.operation.type", "query"]
].compact

assert_equal expected_span_data.sort, Sentry::SPAN_DATA.sort
end

it "sets span descriptions for a named query" do
SentryTraceTestSchema.execute("query Ab { int }")

assert_equal ["query Ab", "query Ab"], Sentry::SPAN_DESCRIPTIONS
end

it "sets span data for a named query" do
SentryTraceTestSchema.execute("query Ab { int }")
expected_span_data = [
["graphql.document", "query Ab { int }"],
["graphql.operation.name", "Ab"],
["graphql.operation.type", "query"]
].compact

assert_equal expected_span_data.sort, Sentry::SPAN_DATA.sort
end
end
45 changes: 45 additions & 0 deletions spec/support/sentry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

# A stub for the Sentry agent, so we can make assertions about how it is used
if defined?(Sentry)
raise "Expected Sentry to be undefined, so that we could define a stub for it."
end

module Sentry
SPAN_OPS = []
SPAN_DATA = []
SPAN_DESCRIPTIONS = []

def self.initialized?
true
end

def self.utc_now
Time.now.utc
end

def self.with_child_span(**args, &block)
SPAN_OPS << args[:op]
yield DummySpan.new
end

def self.clear_all
SPAN_DATA.clear
SPAN_DESCRIPTIONS.clear
SPAN_OPS.clear
end

class DummySpan
def set_data(key, value)
Sentry::SPAN_DATA << [key, value]
end

def set_description(description)
Sentry::SPAN_DESCRIPTIONS << description
end

def finish
# no-op
end
end
end

0 comments on commit 1479664

Please sign in to comment.