-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4775 from patch0/add-sentry-tracing
Add Sentry tracing
- Loading branch information
Showing
7 changed files
with
264 additions
and
13 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |