diff --git a/CHANGELOG.md b/CHANGELOG.md index ef4099b0e..cd7900bc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ - Fixes [#2297](https://github.com/getsentry/sentry-ruby/issues/2297) - Don't mutate `enabled_environments` when using `Sentry::TestHelper` ([#2317](https://github.com/getsentry/sentry-ruby/pull/2317)) +### Internal + +- Add `origin` to spans and transactions to track integration sources for instrumentation ([#2319](https://github.com/getsentry/sentry-ruby/pull/2319)) + ## 5.17.3 ### Internal diff --git a/sentry-delayed_job/lib/sentry/delayed_job/plugin.rb b/sentry-delayed_job/lib/sentry/delayed_job/plugin.rb index 29e1d9e4e..66093f340 100644 --- a/sentry-delayed_job/lib/sentry/delayed_job/plugin.rb +++ b/sentry-delayed_job/lib/sentry/delayed_job/plugin.rb @@ -8,7 +8,8 @@ class Plugin < ::Delayed::Plugin # need to symbolize strings as keyword arguments in Ruby 2.4~2.6 DELAYED_JOB_CONTEXT_KEY = :"Delayed-Job" ACTIVE_JOB_CONTEXT_KEY = :"Active-Job" - OP_NAME = "queue.delayed_job".freeze + OP_NAME = "queue.delayed_job" + SPAN_ORIGIN = "auto.queue.delayed_job" callbacks do |lifecycle| lifecycle.before(:enqueue) do |job, *args, &block| @@ -93,7 +94,13 @@ def self.report?(job) end def self.start_transaction(scope, env, contexts) - options = { name: scope.transaction_name, source: scope.transaction_source, op: OP_NAME } + options = { + name: scope.transaction_name, + source: scope.transaction_source, + op: OP_NAME, + origin: SPAN_ORIGIN + } + transaction = Sentry.continue_trace(env, **options) Sentry.start_transaction(transaction: transaction, custom_sampling_context: contexts, **options) end diff --git a/sentry-delayed_job/spec/sentry/delayed_job_spec.rb b/sentry-delayed_job/spec/sentry/delayed_job_spec.rb index 7c7440452..ac2d31fb1 100644 --- a/sentry-delayed_job/spec/sentry/delayed_job_spec.rb +++ b/sentry-delayed_job/spec/sentry/delayed_job_spec.rb @@ -383,6 +383,7 @@ def perform expect(transaction.contexts.dig(:trace, :span_id)).to be_a(String) expect(transaction.contexts.dig(:trace, :status)).to eq("ok") expect(transaction.contexts.dig(:trace, :op)).to eq("queue.delayed_job") + expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.queue.delayed_job") end it "records transaction with exception" do diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb index d35bb7025..db6d500f0 100644 --- a/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb +++ b/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb @@ -11,6 +11,7 @@ class SpanProcessor < ::OpenTelemetry::SDK::Trace::SpanProcessor SEMANTIC_CONVENTIONS = ::OpenTelemetry::SemanticConventions::Trace INTERNAL_SPAN_KINDS = %i[client internal] + SPAN_ORIGIN = "auto.otel" # The mapping from otel span ids to sentry spans # @return [Hash] @@ -34,7 +35,8 @@ def on_start(otel_span, parent_context) sentry_parent_span.start_child( span_id: trace_data.span_id, description: otel_span.name, - start_timestamp: otel_span.start_timestamp / 1e9 + start_timestamp: otel_span.start_timestamp / 1e9, + origin: SPAN_ORIGIN ) else options = { @@ -45,7 +47,8 @@ def on_start(otel_span, parent_context) parent_span_id: trace_data.parent_span_id, parent_sampled: trace_data.parent_sampled, baggage: trace_data.baggage, - start_timestamp: otel_span.start_timestamp / 1e9 + start_timestamp: otel_span.start_timestamp / 1e9, + origin: SPAN_ORIGIN } Sentry.start_transaction(**options) diff --git a/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb b/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb index e90179498..48d27596e 100644 --- a/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb +++ b/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb @@ -92,6 +92,7 @@ expect(event.contexts).to include(:trace) expect(event.contexts[:trace][:trace_id]).to eq(root_span.context.hex_trace_id) expect(event.contexts[:trace][:span_id]).to eq(root_span.context.hex_span_id) + expect(event.contexts[:trace][:origin]).to eq('auto.otel') end end end @@ -134,6 +135,7 @@ expect(transaction.span_id).to eq(span_id) expect(transaction.trace_id).to eq(trace_id) expect(transaction.start_timestamp).to eq(root_span.start_timestamp / 1e9) + expect(transaction.origin).to eq('auto.otel') expect(transaction.parent_span_id).to eq(nil) expect(transaction.parent_sampled).to eq(nil) @@ -168,6 +170,7 @@ expect(sentry_span.trace_id).to eq(trace_id) expect(sentry_span.description).to eq(child_db_span.name) expect(sentry_span.start_timestamp).to eq(child_db_span.start_timestamp / 1e9) + expect(sentry_span.origin).to eq('auto.otel') end end end @@ -215,6 +218,7 @@ subject.on_finish(finished_db_span) expect(sentry_span.op).to eq('db') + expect(sentry_span.origin).to eq('auto.otel') expect(sentry_span.description).to eq(finished_db_span.attributes['db.statement']) expect(sentry_span.data).to include(finished_db_span.attributes) expect(sentry_span.data).to include({ 'otel.kind' => finished_db_span.kind }) @@ -235,6 +239,7 @@ subject.on_finish(finished_http_span) expect(sentry_span.op).to eq('http.client') + expect(sentry_span.origin).to eq('auto.otel') expect(sentry_span.description).to eq('GET www.google.com/search') expect(sentry_span.data).to include(finished_http_span.attributes) expect(sentry_span.data).to include({ 'otel.kind' => finished_http_span.kind }) @@ -259,6 +264,7 @@ subject.on_finish(finished_root_span) expect(transaction.op).to eq('http.server') + expect(transaction.origin).to eq('auto.otel') expect(transaction.name).to eq(finished_root_span.name) expect(transaction.status).to eq('ok') expect(transaction.contexts[:otel]).to eq({ diff --git a/sentry-rails/lib/sentry/rails/action_cable.rb b/sentry-rails/lib/sentry/rails/action_cable.rb index 155377466..06833a68b 100644 --- a/sentry-rails/lib/sentry/rails/action_cable.rb +++ b/sentry-rails/lib/sentry/rails/action_cable.rb @@ -3,6 +3,7 @@ module Rails module ActionCableExtensions class ErrorHandler OP_NAME = "websocket.server".freeze + SPAN_ORIGIN = "auto.http.rails.actioncable" class << self def capture(connection, transaction_name:, extra_context: nil, &block) @@ -33,7 +34,13 @@ def capture(connection, transaction_name:, extra_context: nil, &block) end def start_transaction(env, scope) - options = { name: scope.transaction_name, source: scope.transaction_source, op: OP_NAME } + options = { + name: scope.transaction_name, + source: scope.transaction_source, + op: OP_NAME, + origin: SPAN_ORIGIN + } + transaction = Sentry.continue_trace(env, **options) Sentry.start_transaction(transaction: transaction, **options) end diff --git a/sentry-rails/lib/sentry/rails/active_job.rb b/sentry-rails/lib/sentry/rails/active_job.rb index cf0c7ca1a..afdf87cd7 100644 --- a/sentry-rails/lib/sentry/rails/active_job.rb +++ b/sentry-rails/lib/sentry/rails/active_job.rb @@ -17,6 +17,7 @@ def already_supported_by_sentry_integration? class SentryReporter OP_NAME = "queue.active_job".freeze + SPAN_ORIGIN = "auto.queue.active_job".freeze class << self def record(job, &block) @@ -27,7 +28,12 @@ def record(job, &block) if job.is_a?(::Sentry::SendEventJob) nil else - Sentry.start_transaction(name: scope.transaction_name, source: scope.transaction_source, op: OP_NAME) + Sentry.start_transaction( + name: scope.transaction_name, + source: scope.transaction_source, + op: OP_NAME, + origin: SPAN_ORIGIN + ) end scope.set_span(transaction) if transaction diff --git a/sentry-rails/lib/sentry/rails/capture_exceptions.rb b/sentry-rails/lib/sentry/rails/capture_exceptions.rb index 8d93784ed..d52456081 100644 --- a/sentry-rails/lib/sentry/rails/capture_exceptions.rb +++ b/sentry-rails/lib/sentry/rails/capture_exceptions.rb @@ -2,6 +2,7 @@ module Sentry module Rails class CaptureExceptions < Sentry::Rack::CaptureExceptions RAILS_7_1 = Gem::Version.new(::Rails.version) >= Gem::Version.new("7.1.0.alpha") + SPAN_ORIGIN = 'auto.http.rails'.freeze def initialize(_) super @@ -32,7 +33,12 @@ def capture_exception(exception, env) end def start_transaction(env, scope) - options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op } + options = { + name: scope.transaction_name, + source: scope.transaction_source, + op: transaction_op, + origin: SPAN_ORIGIN + } if @assets_regexp && scope.transaction_name.match?(@assets_regexp) options.merge!(sampled: false) diff --git a/sentry-rails/lib/sentry/rails/controller_transaction.rb b/sentry-rails/lib/sentry/rails/controller_transaction.rb index 72f31e6c1..b85506a3d 100644 --- a/sentry-rails/lib/sentry/rails/controller_transaction.rb +++ b/sentry-rails/lib/sentry/rails/controller_transaction.rb @@ -1,6 +1,8 @@ module Sentry module Rails module ControllerTransaction + SPAN_ORIGIN = 'auto.view.rails'.freeze + def self.included(base) base.prepend_around_action(:sentry_around_action) end @@ -11,7 +13,7 @@ def sentry_around_action if Sentry.initialized? transaction_name = "#{self.class}##{action_name}" Sentry.get_current_scope.set_transaction_name(transaction_name, source: :view) - Sentry.with_child_span(op: "view.process_action.action_controller", description: transaction_name) do |child_span| + Sentry.with_child_span(op: "view.process_action.action_controller", description: transaction_name, origin: SPAN_ORIGIN) do |child_span| if child_span begin result = yield diff --git a/sentry-rails/lib/sentry/rails/tracing/action_controller_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/action_controller_subscriber.rb index 8c3ec5ddf..11e9eadf2 100644 --- a/sentry-rails/lib/sentry/rails/tracing/action_controller_subscriber.rb +++ b/sentry-rails/lib/sentry/rails/tracing/action_controller_subscriber.rb @@ -9,6 +9,7 @@ class ActionControllerSubscriber < AbstractSubscriber EVENT_NAMES = ["process_action.action_controller"].freeze OP_NAME = "view.process_action.action_controller".freeze + SPAN_ORIGIN = "auto.view.rails".freeze def self.subscribe! Sentry.logger.warn <<~MSG @@ -22,6 +23,7 @@ def self.subscribe! record_on_current_span( op: OP_NAME, + origin: SPAN_ORIGIN, start_timestamp: payload[START_TIMESTAMP_NAME], description: "#{controller}##{action}", duration: duration diff --git a/sentry-rails/lib/sentry/rails/tracing/action_view_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/action_view_subscriber.rb index 588af1374..baed2c7e5 100644 --- a/sentry-rails/lib/sentry/rails/tracing/action_view_subscriber.rb +++ b/sentry-rails/lib/sentry/rails/tracing/action_view_subscriber.rb @@ -6,10 +6,17 @@ module Tracing class ActionViewSubscriber < AbstractSubscriber EVENT_NAMES = ["render_template.action_view"].freeze SPAN_PREFIX = "template.".freeze + SPAN_ORIGIN = "auto.template.rails".freeze def self.subscribe! subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload| - record_on_current_span(op: SPAN_PREFIX + event_name, start_timestamp: payload[START_TIMESTAMP_NAME], description: payload[:identifier], duration: duration) + record_on_current_span( + op: SPAN_PREFIX + event_name, + origin: SPAN_ORIGIN, + start_timestamp: payload[START_TIMESTAMP_NAME], + description: payload[:identifier], + duration: duration + ) end end end diff --git a/sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb index 113ebba76..813aa9589 100644 --- a/sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb +++ b/sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb @@ -6,6 +6,7 @@ module Tracing class ActiveRecordSubscriber < AbstractSubscriber EVENT_NAMES = ["sql.active_record"].freeze SPAN_PREFIX = "db.".freeze + SPAN_ORIGIN = "auto.db.rails".freeze EXCLUDED_EVENTS = ["SCHEMA", "TRANSACTION"].freeze SUPPORT_SOURCE_LOCATION = ActiveSupport::BacktraceCleaner.method_defined?(:clean_frame) @@ -28,7 +29,13 @@ def subscribe! subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload| next if EXCLUDED_EVENTS.include? payload[:name] - record_on_current_span(op: SPAN_PREFIX + event_name, start_timestamp: payload[START_TIMESTAMP_NAME], description: payload[:sql], duration: duration) do |span| + record_on_current_span( + op: SPAN_PREFIX + event_name, + origin: SPAN_ORIGIN, + start_timestamp: payload[START_TIMESTAMP_NAME], + description: payload[:sql], + duration: duration + ) do |span| span.set_tag(:cached, true) if payload.fetch(:cached, false) # cached key is only set for hits in the QueryCache, from Rails 5.1 connection = payload[:connection] diff --git a/sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb index 4a8f8a306..5d62bad22 100644 --- a/sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb +++ b/sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb @@ -19,9 +19,17 @@ class ActiveStorageSubscriber < AbstractSubscriber analyze.active_storage ].freeze + SPAN_ORIGIN = "auto.file.rails".freeze + def self.subscribe! subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload| - record_on_current_span(op: "file.#{event_name}".freeze, start_timestamp: payload[START_TIMESTAMP_NAME], description: payload[:service], duration: duration) do |span| + record_on_current_span( + op: "file.#{event_name}".freeze, + origin: SPAN_ORIGIN, + start_timestamp: payload[START_TIMESTAMP_NAME], + description: payload[:service], + duration: duration + ) do |span| payload.each do |key, value| span.set_data(key, value) unless key == START_TIMESTAMP_NAME end diff --git a/sentry-rails/spec/sentry/rails/action_cable_spec.rb b/sentry-rails/spec/sentry/rails/action_cable_spec.rb index 40db25c74..4b353adff 100644 --- a/sentry-rails/spec/sentry/rails/action_cable_spec.rb +++ b/sentry-rails/spec/sentry/rails/action_cable_spec.rb @@ -204,7 +204,8 @@ def disconnect expect(transaction["contexts"]).to include( "trace" => hash_including( "op" => "websocket.server", - "status" => "internal_error" + "status" => "internal_error", + "origin" => "auto.http.rails.actioncable" ) ) end @@ -230,7 +231,8 @@ def disconnect expect(subscription_transaction["contexts"]).to include( "trace" => hash_including( "op" => "websocket.server", - "status" => "ok" + "status" => "ok", + "origin" => "auto.http.rails.actioncable" ) ) @@ -259,7 +261,8 @@ def disconnect expect(action_transaction["contexts"]).to include( "trace" => hash_including( "op" => "websocket.server", - "status" => "internal_error" + "status" => "internal_error", + "origin" => "auto.http.rails.actioncable" ) ) end @@ -281,7 +284,8 @@ def disconnect expect(subscription_transaction["contexts"]).to include( "trace" => hash_including( "op" => "websocket.server", - "status" => "ok" + "status" => "ok", + "origin" => "auto.http.rails.actioncable" ) ) @@ -308,7 +312,8 @@ def disconnect expect(transaction["contexts"]).to include( "trace" => hash_including( "op" => "websocket.server", - "status" => "internal_error" + "status" => "internal_error", + "origin" => "auto.http.rails.actioncable" ) ) end diff --git a/sentry-rails/spec/sentry/rails/activejob_spec.rb b/sentry-rails/spec/sentry/rails/activejob_spec.rb index f4d1550be..f5892d944 100644 --- a/sentry-rails/spec/sentry/rails/activejob_spec.rb +++ b/sentry-rails/spec/sentry/rails/activejob_spec.rb @@ -182,6 +182,7 @@ def post.to_global_id expect(transaction.contexts.dig(:trace, :span_id)).to be_present expect(transaction.contexts.dig(:trace, :status)).to eq("ok") expect(transaction.contexts.dig(:trace, :op)).to eq("queue.active_job") + expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.queue.active_job") expect(transaction.spans.count).to eq(1) expect(transaction.spans.first[:op]).to eq("db.sql.active_record") @@ -199,6 +200,7 @@ def post.to_global_id expect(transaction.contexts.dig(:trace, :trace_id)).to be_present expect(transaction.contexts.dig(:trace, :span_id)).to be_present expect(transaction.contexts.dig(:trace, :status)).to eq("internal_error") + expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.queue.active_job") event = transport.events.last expect(event.transaction).to eq("FailedWithExtraJob") diff --git a/sentry-rails/spec/sentry/rails/tracing/action_controller_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/action_controller_subscriber_spec.rb index 58852df52..f147e1b8e 100644 --- a/sentry-rails/spec/sentry/rails/tracing/action_controller_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing/action_controller_subscriber_spec.rb @@ -36,6 +36,7 @@ span = transaction[:spans][0] expect(span[:op]).to eq("view.process_action.action_controller") + expect(span[:origin]).to eq("auto.view.rails") expect(span[:description]).to eq("HelloController#world") expect(span[:trace_id]).to eq(transaction.dig(:contexts, :trace, :trace_id)) expect(span[:data].keys).to match_array(["http.response.status_code", :format, :method, :path, :params]) diff --git a/sentry-rails/spec/sentry/rails/tracing/action_view_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/action_view_subscriber_spec.rb index f6d3ff31f..aa2473312 100644 --- a/sentry-rails/spec/sentry/rails/tracing/action_view_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing/action_view_subscriber_spec.rb @@ -25,6 +25,7 @@ # ignore the first span, which is for controller action span = transaction[:spans][1] expect(span[:op]).to eq("template.render_template.action_view") + expect(span[:origin]).to eq("auto.template.rails") expect(span[:description]).to match(/test_template\.html\.erb/) expect(span[:trace_id]).to eq(transaction.dig(:contexts, :trace, :trace_id)) end diff --git a/sentry-rails/spec/sentry/rails/tracing/active_record_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/active_record_subscriber_spec.rb index ecdb2ab4b..aa29b82d2 100644 --- a/sentry-rails/spec/sentry/rails/tracing/active_record_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing/active_record_subscriber_spec.rb @@ -34,6 +34,7 @@ span = transaction[:spans][0] expect(span[:op]).to eq("db.sql.active_record") + expect(span[:origin]).to eq("auto.db.rails") expect(span[:description]).to eq("SELECT \"posts\".* FROM \"posts\"") expect(span[:tags].key?(:cached)).to eq(false) expect(span[:trace_id]).to eq(transaction.dig(:contexts, :trace, :trace_id)) @@ -134,6 +135,7 @@ def foo cached_query_span = transaction[:spans][1] expect(cached_query_span[:op]).to eq("db.sql.active_record") + expect(cached_query_span[:origin]).to eq("auto.db.rails") expect(cached_query_span[:description]).to eq("SELECT \"posts\".* FROM \"posts\"") expect(cached_query_span[:tags]).to include({ cached: true }) diff --git a/sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb index bfacfe433..5d34db1e3 100644 --- a/sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb @@ -29,10 +29,13 @@ if Rails.version.to_f > 6.1 expect(analysis_transaction[:spans].count).to eq(2) expect(analysis_transaction[:spans][0][:op]).to eq("file.service_streaming_download.active_storage") + expect(analysis_transaction[:spans][0][:origin]).to eq("auto.file.rails") expect(analysis_transaction[:spans][1][:op]).to eq("file.analyze.active_storage") + expect(analysis_transaction[:spans][1][:origin]).to eq("auto.file.rails") else expect(analysis_transaction[:spans].count).to eq(1) expect(analysis_transaction[:spans][0][:op]).to eq("file.service_streaming_download.active_storage") + expect(analysis_transaction[:spans][0][:origin]).to eq("auto.file.rails") end request_transaction = transport.events.last.to_hash @@ -41,6 +44,7 @@ span = request_transaction[:spans][1] expect(span[:op]).to eq("file.service_upload.active_storage") + expect(span[:origin]).to eq("auto.file.rails") expect(span[:description]).to eq("Disk") expect(span.dig(:data, :key)).to eq(p.cover.key) expect(span[:trace_id]).to eq(request_transaction.dig(:contexts, :trace, :trace_id)) diff --git a/sentry-rails/spec/sentry/rails/tracing_spec.rb b/sentry-rails/spec/sentry/rails/tracing_spec.rb index 719ab1794..287bd8acc 100644 --- a/sentry-rails/spec/sentry/rails/tracing_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing_spec.rb @@ -32,11 +32,13 @@ expect(transaction[:type]).to eq("transaction") expect(transaction.dig(:contexts, :trace, :op)).to eq("http.server") + expect(transaction.dig(:contexts, :trace, :origin)).to eq("auto.http.rails") parent_span_id = transaction.dig(:contexts, :trace, :span_id) expect(transaction[:spans].count).to eq(2) first_span = transaction[:spans][0] expect(first_span[:op]).to eq("view.process_action.action_controller") + expect(first_span[:origin]).to eq("auto.view.rails") expect(first_span[:description]).to eq("PostsController#index") expect(first_span[:parent_span_id]).to eq(parent_span_id) expect(first_span[:status]).to eq("internal_error") @@ -44,6 +46,7 @@ second_span = transaction[:spans][1] expect(second_span[:op]).to eq("db.sql.active_record") + expect(second_span[:origin]).to eq("auto.db.rails") expect(second_span[:description]).to eq("SELECT \"posts\".* FROM \"posts\"") expect(second_span[:parent_span_id]).to eq(first_span[:span_id]) @@ -63,19 +66,21 @@ expect(transaction[:type]).to eq("transaction") expect(transaction.dig(:contexts, :trace, :op)).to eq("http.server") + expect(transaction.dig(:contexts, :trace, :origin)).to eq("auto.http.rails") parent_span_id = transaction.dig(:contexts, :trace, :span_id) expect(transaction[:spans].count).to eq(3) first_span = transaction[:spans][0] expect(first_span[:data].keys).to match_array(["http.response.status_code", :format, :method, :path, :params]) expect(first_span[:op]).to eq("view.process_action.action_controller") + expect(first_span[:origin]).to eq("auto.view.rails") expect(first_span[:description]).to eq("PostsController#show") expect(first_span[:parent_span_id]).to eq(parent_span_id) expect(first_span[:status]).to eq("ok") - second_span = transaction[:spans][1] expect(second_span[:op]).to eq("db.sql.active_record") + expect(second_span[:origin]).to eq("auto.db.rails") expect(second_span[:description].squeeze("\s")).to eq( 'SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT ?' ) @@ -86,6 +91,7 @@ third_span = transaction[:spans][2] expect(third_span[:op]).to eq("template.render_template.action_view") + expect(third_span[:origin]).to eq("auto.template.rails") expect(third_span[:description].squeeze("\s")).to eq("text template") expect(third_span[:parent_span_id]).to eq(first_span[:span_id]) end @@ -239,6 +245,7 @@ expect(transaction.timestamp).not_to be_nil expect(transaction.contexts.dig(:trace, :status)).to eq("ok") expect(transaction.contexts.dig(:trace, :op)).to eq("http.server") + expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.http.rails") expect(transaction.spans.count).to eq(3) # should inherit information from the external_transaction diff --git a/sentry-resque/lib/sentry/resque.rb b/sentry-resque/lib/sentry/resque.rb index f204e5236..84907d84f 100644 --- a/sentry-resque/lib/sentry/resque.rb +++ b/sentry-resque/lib/sentry/resque.rb @@ -15,6 +15,8 @@ def perform end class SentryReporter + SPAN_ORIGIN = "auto.queue.resque" + class << self def record(queue, worker, payload, &block) Sentry.with_scope do |scope| @@ -25,7 +27,13 @@ def record(queue, worker, payload, &block) name = contexts.dig(:"Active-Job", :job_class) || contexts.dig(:"Resque", :job_class) scope.set_transaction_name(name, source: :task) - transaction = Sentry.start_transaction(name: scope.transaction_name, source: scope.transaction_source, op: "queue.resque") + transaction = Sentry.start_transaction( + name: scope.transaction_name, + source: scope.transaction_source, + op: "queue.resque", + origin: SPAN_ORIGIN + ) + scope.set_span(transaction) if transaction yield diff --git a/sentry-resque/spec/sentry/tracing_spec.rb b/sentry-resque/spec/sentry/tracing_spec.rb index 01605b950..e77b84f4e 100644 --- a/sentry-resque/spec/sentry/tracing_spec.rb +++ b/sentry-resque/spec/sentry/tracing_spec.rb @@ -42,6 +42,7 @@ def self.perform(msg) expect(tracing_event[:type]).to eq("transaction") expect(tracing_event.dig(:contexts, :trace, :status)).to eq("ok") expect(tracing_event.dig(:contexts, :trace, :op)).to eq("queue.resque") + expect(tracing_event.dig(:contexts, :trace, :origin)).to eq("auto.queue.resque") end it "records tracing events with exceptions" do @@ -59,6 +60,7 @@ def self.perform(msg) expect(tracing_event[:type]).to eq("transaction") expect(tracing_event.dig(:contexts, :trace, :status)).to eq("internal_error") expect(tracing_event.dig(:contexts, :trace, :op)).to eq("queue.resque") + expect(tracing_event.dig(:contexts, :trace, :origin)).to eq("auto.queue.resque") end context "with instrumenter :otel" do diff --git a/sentry-ruby/lib/sentry/metrics.rb b/sentry-ruby/lib/sentry/metrics.rb index e67fc769f..99bd9f7f1 100644 --- a/sentry-ruby/lib/sentry/metrics.rb +++ b/sentry-ruby/lib/sentry/metrics.rb @@ -15,6 +15,7 @@ module Metrics FRACTIONAL_UNITS = %w[ratio percent] OP_NAME = 'metric.timing' + SPAN_ORIGIN = 'auto.metric.timing' class << self def increment(key, value = 1.0, unit: 'none', tags: {}, timestamp: nil) @@ -37,7 +38,7 @@ def timing(key, unit: 'second', tags: {}, timestamp: nil, &block) return unless block_given? return yield unless DURATION_UNITS.include?(unit) - result, value = Sentry.with_child_span(op: OP_NAME, description: key) do |span| + result, value = Sentry.with_child_span(op: OP_NAME, description: key, origin: SPAN_ORIGIN) do |span| tags.each { |k, v| span.set_tag(k, v.is_a?(Array) ? v.join(', ') : v.to_s) } if span start = Timing.send(unit.to_sym) diff --git a/sentry-ruby/lib/sentry/net/http.rb b/sentry-ruby/lib/sentry/net/http.rb index 9480ec081..205c79c55 100644 --- a/sentry-ruby/lib/sentry/net/http.rb +++ b/sentry-ruby/lib/sentry/net/http.rb @@ -8,6 +8,7 @@ module Sentry module Net module HTTP OP_NAME = "http.client" + SPAN_ORIGIN = "auto.http.net_http" BREADCRUMB_CATEGORY = "net.http" # To explain how the entire thing works, we need to know how the original Net::HTTP#request works @@ -30,7 +31,7 @@ def request(req, body = nil, &block) return super unless started? && Sentry.initialized? return super if from_sentry_sdk? - Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |sentry_span| + Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |sentry_span| request_info = extract_request_info(req) if propagate_trace?(request_info[:url], Sentry.configuration) diff --git a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb index 99a84a0fe..653188e10 100644 --- a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb +++ b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb @@ -5,6 +5,7 @@ module Rack class CaptureExceptions ERROR_EVENT_ID_KEY = "sentry.error_event_id" MECHANISM_TYPE = "rack" + SPAN_ORIGIN = "auto.http.rack" def initialize(app) @app = app @@ -63,7 +64,13 @@ def capture_exception(exception, env) end def start_transaction(env, scope) - options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op } + options = { + name: scope.transaction_name, + source: scope.transaction_source, + op: transaction_op, + origin: SPAN_ORIGIN + } + transaction = Sentry.continue_trace(env, **options) Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options) end diff --git a/sentry-ruby/lib/sentry/redis.rb b/sentry-ruby/lib/sentry/redis.rb index 3c3832321..050b85346 100644 --- a/sentry-ruby/lib/sentry/redis.rb +++ b/sentry-ruby/lib/sentry/redis.rb @@ -4,6 +4,7 @@ module Sentry # @api private class Redis OP_NAME = "db.redis" + SPAN_ORIGIN = "auto.db.redis" LOGGER_NAME = :redis_logger def initialize(commands, host, port, db) @@ -13,7 +14,7 @@ def initialize(commands, host, port, db) def instrument return yield unless Sentry.initialized? - Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |span| + Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |span| yield.tap do record_breadcrumb diff --git a/sentry-ruby/lib/sentry/span.rb b/sentry-ruby/lib/sentry/span.rb index d3b0a336d..061b7b1e0 100644 --- a/sentry-ruby/lib/sentry/span.rb +++ b/sentry-ruby/lib/sentry/span.rb @@ -60,6 +60,8 @@ module DataConventions 504 => "deadline_exceeded" } + DEFAULT_SPAN_ORIGIN = "manual" + # An uuid that can be used to identify a trace. # @return [String] attr_reader :trace_id @@ -93,6 +95,9 @@ module DataConventions # Span data # @return [Hash] attr_reader :data + # Span origin that tracks what kind of instrumentation created a span + # @return [String] + attr_reader :origin # The SpanRecorder the current span belongs to. # SpanRecorder holds all spans under the same Transaction object (including the Transaction itself). @@ -114,7 +119,8 @@ def initialize( parent_span_id: nil, sampled: nil, start_timestamp: nil, - timestamp: nil + timestamp: nil, + origin: nil ) @trace_id = trace_id || SecureRandom.uuid.delete("-") @span_id = span_id || SecureRandom.uuid.delete("-").slice(0, 16) @@ -128,6 +134,7 @@ def initialize( @status = status @data = {} @tags = {} + @origin = origin || DEFAULT_SPAN_ORIGIN end # Finishes the span by adding a timestamp. @@ -165,7 +172,8 @@ def to_hash op: @op, status: @status, tags: @tags, - data: @data + data: @data, + origin: @origin } summary = metrics_summary @@ -183,7 +191,8 @@ def get_trace_context parent_span_id: @parent_span_id, description: @description, op: @op, - status: @status + status: @status, + origin: @origin } end @@ -280,6 +289,12 @@ def set_tag(key, value) @tags[key] = value end + # Sets the origin of the span. + # @param origin [String] + def set_origin(origin) + @origin = origin + end + # Collects gauge metrics on the span for metric summaries. def metrics_local_aggregator @metrics_local_aggregator ||= Sentry::Metrics::LocalAggregator.new diff --git a/sentry-ruby/spec/sentry/metrics_spec.rb b/sentry-ruby/spec/sentry/metrics_spec.rb index 42d9ad754..335c31e5a 100644 --- a/sentry-ruby/spec/sentry/metrics_spec.rb +++ b/sentry-ruby/spec/sentry/metrics_spec.rb @@ -122,7 +122,7 @@ end it 'starts a span' do - expect(Sentry).to receive(:with_child_span).with(op: Sentry::Metrics::OP_NAME, description: 'foo').and_call_original + expect(Sentry).to receive(:with_child_span).with(op: Sentry::Metrics::OP_NAME, description: 'foo', origin: Sentry::Metrics::SPAN_ORIGIN).and_call_original described_class.timing('foo') { sleep(0.1) } end diff --git a/sentry-ruby/spec/sentry/net/http_spec.rb b/sentry-ruby/spec/sentry/net/http_spec.rb index 4718b5e1f..064c4f36c 100644 --- a/sentry-ruby/spec/sentry/net/http_spec.rb +++ b/sentry-ruby/spec/sentry/net/http_spec.rb @@ -62,6 +62,7 @@ request_span = transaction.span_recorder.spans.last expect(request_span.op).to eq("http.client") + expect(request_span.origin).to eq("auto.http.net_http") expect(request_span.start_timestamp).not_to be_nil expect(request_span.timestamp).not_to be_nil expect(request_span.start_timestamp).not_to eq(request_span.timestamp) @@ -93,6 +94,7 @@ request_span = transaction.span_recorder.spans.last expect(request_span.op).to eq("http.client") + expect(request_span.origin).to eq("auto.http.net_http") expect(request_span.start_timestamp).not_to be_nil expect(request_span.timestamp).not_to be_nil expect(request_span.start_timestamp).not_to eq(request_span.timestamp) @@ -320,6 +322,7 @@ def verify_spans(transaction) request_span = transaction.span_recorder.spans[1] expect(request_span.op).to eq("http.client") + expect(request_span.origin).to eq("auto.http.net_http") expect(request_span.start_timestamp).not_to be_nil expect(request_span.timestamp).not_to be_nil expect(request_span.start_timestamp).not_to eq(request_span.timestamp) @@ -332,6 +335,7 @@ def verify_spans(transaction) request_span = transaction.span_recorder.spans[2] expect(request_span.op).to eq("http.client") + expect(request_span.origin).to eq("auto.http.net_http") expect(request_span.start_timestamp).not_to be_nil expect(request_span.timestamp).not_to be_nil expect(request_span.start_timestamp).not_to eq(request_span.timestamp) diff --git a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb index c5751bba0..471a5d30a 100644 --- a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb +++ b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb @@ -264,6 +264,7 @@ def verify_transaction_attributes(transaction) expect(transaction.timestamp).not_to be_nil expect(transaction.contexts.dig(:trace, :status)).to eq("ok") expect(transaction.contexts.dig(:trace, :op)).to eq("http.server") + expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.http.rack") expect(transaction.spans.count).to eq(0) end diff --git a/sentry-ruby/spec/sentry/redis_spec.rb b/sentry-ruby/spec/sentry/redis_spec.rb index ef2c4ce79..2722e0498 100644 --- a/sentry-ruby/spec/sentry/redis_spec.rb +++ b/sentry-ruby/spec/sentry/redis_spec.rb @@ -23,6 +23,7 @@ expect(result).to eq("OK") request_span = transaction.span_recorder.spans.last expect(request_span.op).to eq("db.redis") + expect(request_span.origin).to eq("auto.db.redis") expect(request_span.start_timestamp).not_to be_nil expect(request_span.timestamp).not_to be_nil expect(request_span.start_timestamp).not_to eq(request_span.timestamp) diff --git a/sentry-ruby/spec/sentry/span_spec.rb b/sentry-ruby/spec/sentry/span_spec.rb index 3e61c8058..0bc187eec 100644 --- a/sentry-ruby/spec/sentry/span_spec.rb +++ b/sentry-ruby/spec/sentry/span_spec.rb @@ -33,6 +33,7 @@ expect(context[:status]).to eq("ok") expect(context[:trace_id].length).to eq(32) expect(context[:span_id].length).to eq(16) + expect(context[:origin]).to eq('manual') end end @@ -69,6 +70,7 @@ expect(hash[:tags]).to eq({ "foo" => "bar" }) expect(hash[:trace_id].length).to eq(32) expect(hash[:span_id].length).to eq(16) + expect(hash[:origin]).to eq('manual') end it 'has metric summary if present' do @@ -296,4 +298,12 @@ expect(subject.tags).to eq({ foo: "bar" }) end end + + describe "#set_origin" do + it "sets origin" do + subject.set_origin('auto.http') + + expect(subject.origin).to eq('auto.http') + end + end end diff --git a/sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb b/sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb index b6cde1ea5..748ca0155 100644 --- a/sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb +++ b/sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb @@ -1,9 +1,12 @@ +# frozen_string_literal: true + require 'sentry/sidekiq/context_filter' module Sentry module Sidekiq class SentryContextServerMiddleware - OP_NAME = "queue.sidekiq".freeze + OP_NAME = "queue.sidekiq" + SPAN_ORIGIN = "auto.queue.sidekiq" def call(_worker, job, queue) return yield unless Sentry.initialized? @@ -40,7 +43,13 @@ def build_tags(tags) end def start_transaction(scope, env) - options = { name: scope.transaction_name, source: scope.transaction_source, op: OP_NAME } + options = { + name: scope.transaction_name, + source: scope.transaction_source, + op: OP_NAME, + origin: SPAN_ORIGIN + } + transaction = Sentry.continue_trace(env, **options) Sentry.start_transaction(transaction: transaction, **options) end diff --git a/sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb b/sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb index f7c1a21a3..8d28577e7 100644 --- a/sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb +++ b/sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb @@ -57,6 +57,12 @@ expect(event.tags.keys).to include(:"sidekiq.marvel", :"sidekiq.dc") end + it "has the correct origin" do + execute_worker(processor, TagsWorker) + transaction = transport.events.last + expect(transaction.contexts.dig(:trace, :origin)).to eq('auto.queue.sidekiq') + end + context "with trace_propagation_headers" do let(:parent_transaction) { Sentry.start_transaction(op: "sidekiq") }