Skip to content

Commit a985a2b

Browse files
committed
feat: add support for OpenTelemetry messaging/queue system spans
Adds parsing logic for OTel spans with messaging.system attributes to properly categorize queue operations. Producer spans get 'queue.publish' operation and consumer spans get 'queue.process' operation. The span description is extracted from the span name (e.g., "Sidekiq::Worker" from "Sidekiq::Worker publish").
1 parent a3d87a6 commit a985a2b

File tree

2 files changed

+79
-2
lines changed

2 files changed

+79
-2
lines changed

sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ def parse_span_description(otel_span)
142142

143143
statement = otel_span.attributes[SEMANTIC_CONVENTIONS::DB_STATEMENT]
144144
description = statement if statement
145+
elsif (messaging_system = otel_span.attributes[SEMANTIC_CONVENTIONS::MESSAGING_SYSTEM])
146+
op = "queue.#{otel_span.kind == :producer ? "publish" : "process"}"
147+
description = description&.split(" ")&.first&.strip || messaging_system
145148
end
146149

147150
[op, description]

sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,30 @@
9797
tracer.start_span('connect', with_parent: root_parent_context, attributes: attributes, kind: :internal)
9898
end
9999

100+
let(:child_queue_span_producer) do
101+
attributes = {
102+
'messaging.system' => 'sidekiq',
103+
'messaging.destination' => 'default',
104+
'messaging.destination_kind' => 'queue',
105+
'messaging.operation' => 'publish',
106+
'messaging.sidekiq_job_class' => 'Sidekiq::Worker'
107+
}
108+
109+
tracer.start_span('Sidekiq::Worker publish', with_parent: root_parent_context, attributes: attributes, kind: :producer)
110+
end
111+
112+
let(:child_queue_span_consumer) do
113+
attributes = {
114+
'messaging.system' => 'sidekiq',
115+
'messaging.destination' => 'default',
116+
'messaging.destination_kind' => 'queue',
117+
'messaging.operation' => 'process',
118+
'messaging.sidekiq_job_class' => 'Sidekiq::Worker'
119+
}
120+
121+
tracer.start_span('Sidekiq::Worker process', with_parent: root_parent_context, attributes: attributes, kind: :consumer)
122+
end
123+
100124
before do
101125
perform_basic_setup
102126
perform_otel_setup
@@ -220,6 +244,8 @@
220244
subject.on_start(child_http_span, root_parent_context)
221245
subject.on_start(error_span, empty_context)
222246
subject.on_start(http_error_span, empty_context)
247+
subject.on_start(child_queue_span_producer, root_parent_context)
248+
subject.on_start(child_queue_span_consumer, root_parent_context)
223249
end
224250

225251
let(:finished_db_span) { child_db_span.finish }
@@ -228,6 +254,8 @@
228254
let(:finished_invalid_span) { invalid_span.finish }
229255
let(:finished_error_span) { error_span.finish }
230256
let(:finished_http_error_span) { http_error_span.finish }
257+
let(:finished_queue_span) { child_queue_span_producer.finish }
258+
let(:finished_queue_span_consumer) { child_queue_span_consumer.finish }
231259

232260
it 'noops when not initialized' do
233261
allow(Sentry).to receive(:initialized?).and_return(false)
@@ -290,7 +318,7 @@
290318
expect(sentry_span.data).to include({ 'otel.kind' => finished_db_span.kind })
291319
expect(sentry_span.timestamp).to eq(finished_db_span.end_timestamp / 1e9)
292320

293-
expect(subject.span_map.size).to eq(4)
321+
expect(subject.span_map.size).to eq(6)
294322
expect(subject.span_map.keys).not_to include(span_id)
295323
end
296324

@@ -312,7 +340,51 @@
312340
expect(sentry_span.timestamp).to eq(finished_http_span.end_timestamp / 1e9)
313341
expect(sentry_span.status).to eq('ok')
314342

315-
expect(subject.span_map.size).to eq(4)
343+
expect(subject.span_map.size).to eq(6)
344+
expect(subject.span_map.keys).not_to include(span_id)
345+
end
346+
347+
it 'finishes sentry child span on otel child queue producer span finish' do
348+
expect(subject.span_map).to receive(:delete).and_call_original
349+
350+
span_id = finished_queue_span.context.hex_span_id
351+
sentry_span = subject.span_map[span_id]
352+
expect(sentry_span).to be_a(Sentry::Span)
353+
354+
expect(sentry_span).to receive(:finish).and_call_original
355+
subject.on_finish(finished_queue_span)
356+
357+
expect(sentry_span.op).to eq('queue.publish')
358+
expect(sentry_span.origin).to eq('auto.otel')
359+
expect(sentry_span.description).to eq('Sidekiq::Worker')
360+
expect(sentry_span.data).to include(finished_queue_span.attributes)
361+
expect(sentry_span.data).to include({ 'otel.kind' => finished_queue_span.kind })
362+
expect(sentry_span.timestamp).to eq(finished_queue_span.end_timestamp / 1e9)
363+
expect(sentry_span.status).to eq('ok')
364+
365+
expect(subject.span_map.size).to eq(6)
366+
expect(subject.span_map.keys).not_to include(span_id)
367+
end
368+
369+
it 'finishes sentry child span on otel child queue consumer span finish' do
370+
expect(subject.span_map).to receive(:delete).and_call_original
371+
372+
span_id = finished_queue_span_consumer.context.hex_span_id
373+
sentry_span = subject.span_map[span_id]
374+
expect(sentry_span).to be_a(Sentry::Span)
375+
376+
expect(sentry_span).to receive(:finish).and_call_original
377+
subject.on_finish(finished_queue_span_consumer)
378+
379+
expect(sentry_span.op).to eq('queue.process')
380+
expect(sentry_span.origin).to eq('auto.otel')
381+
expect(sentry_span.description).to eq('Sidekiq::Worker')
382+
expect(sentry_span.data).to include(finished_queue_span_consumer.attributes)
383+
expect(sentry_span.data).to include({ 'otel.kind' => finished_queue_span_consumer.kind })
384+
expect(sentry_span.timestamp).to eq(finished_queue_span_consumer.end_timestamp / 1e9)
385+
expect(sentry_span.status).to eq('ok')
386+
387+
expect(subject.span_map.size).to eq(6)
316388
expect(subject.span_map.keys).not_to include(span_id)
317389
end
318390

@@ -321,6 +393,8 @@
321393
subject.on_finish(finished_http_span)
322394
subject.on_finish(finished_error_span)
323395
subject.on_finish(finished_http_error_span)
396+
subject.on_finish(finished_queue_span)
397+
subject.on_finish(finished_queue_span_consumer)
324398

325399
expect(subject.span_map).to receive(:delete).and_call_original
326400

0 commit comments

Comments
 (0)