Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sentry tracing #4775

Merged
merged 8 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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