Skip to content

Commit 7eda574

Browse files
authored
feat: add support for OpenTelemetry messaging/queue system spans (#2685)
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 c11a499 commit 7eda574

3 files changed

Lines changed: 81 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
DB.extension(:sentry)
3232
```
3333

34+
- Add support for OpenTelemetry messaging/queue system spans ([#2685](https://github.com/getsentry/sentry-ruby/pull/2685))
35+
3436
### Bug Fixes
3537

3638
- Handle empty frames case gracefully with local vars ([#2807](https://github.com/getsentry/sentry-ruby/pull/2807))

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

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

152152
statement = otel_span.attributes[SEMANTIC_CONVENTIONS::DB_STATEMENT]
153153
description = statement if statement
154+
elsif (messaging_system = otel_span.attributes[SEMANTIC_CONVENTIONS::MESSAGING_SYSTEM])
155+
op = "queue.#{otel_span.kind == :producer ? "publish" : "process"}"
156+
description = description&.split(" ")&.first&.strip || messaging_system
154157
end
155158

156159
[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
@@ -247,6 +271,8 @@
247271
subject.on_start(child_http_span, root_parent_context)
248272
subject.on_start(error_span, empty_context)
249273
subject.on_start(http_error_span, empty_context)
274+
subject.on_start(child_queue_span_producer, root_parent_context)
275+
subject.on_start(child_queue_span_consumer, root_parent_context)
250276
end
251277

252278
let(:finished_db_span) { child_db_span.finish }
@@ -255,6 +281,8 @@
255281
let(:finished_invalid_span) { invalid_span.finish }
256282
let(:finished_error_span) { error_span.finish }
257283
let(:finished_http_error_span) { http_error_span.finish }
284+
let(:finished_queue_span) { child_queue_span_producer.finish }
285+
let(:finished_queue_span_consumer) { child_queue_span_consumer.finish }
258286

259287
it 'noops when not initialized' do
260288
allow(Sentry).to receive(:initialized?).and_return(false)
@@ -317,7 +345,7 @@
317345
expect(sentry_span.data).to include({ 'otel.kind' => finished_db_span.kind })
318346
expect(sentry_span.timestamp).to eq(finished_db_span.end_timestamp / 1e9)
319347

320-
expect(subject.span_map.size).to eq(4)
348+
expect(subject.span_map.size).to eq(6)
321349
expect(subject.span_map.keys).not_to include(span_id)
322350
end
323351

@@ -339,7 +367,51 @@
339367
expect(sentry_span.timestamp).to eq(finished_http_span.end_timestamp / 1e9)
340368
expect(sentry_span.status).to eq('ok')
341369

342-
expect(subject.span_map.size).to eq(4)
370+
expect(subject.span_map.size).to eq(6)
371+
expect(subject.span_map.keys).not_to include(span_id)
372+
end
373+
374+
it 'finishes sentry child span on otel child queue producer span finish' do
375+
expect(subject.span_map).to receive(:delete).and_call_original
376+
377+
span_id = finished_queue_span.context.hex_span_id
378+
sentry_span = subject.span_map[span_id]
379+
expect(sentry_span).to be_a(Sentry::Span)
380+
381+
expect(sentry_span).to receive(:finish).and_call_original
382+
subject.on_finish(finished_queue_span)
383+
384+
expect(sentry_span.op).to eq('queue.publish')
385+
expect(sentry_span.origin).to eq('auto.otel')
386+
expect(sentry_span.description).to eq('Sidekiq::Worker')
387+
expect(sentry_span.data).to include(finished_queue_span.attributes)
388+
expect(sentry_span.data).to include({ 'otel.kind' => finished_queue_span.kind })
389+
expect(sentry_span.timestamp).to eq(finished_queue_span.end_timestamp / 1e9)
390+
expect(sentry_span.status).to eq('ok')
391+
392+
expect(subject.span_map.size).to eq(6)
393+
expect(subject.span_map.keys).not_to include(span_id)
394+
end
395+
396+
it 'finishes sentry child span on otel child queue consumer span finish' do
397+
expect(subject.span_map).to receive(:delete).and_call_original
398+
399+
span_id = finished_queue_span_consumer.context.hex_span_id
400+
sentry_span = subject.span_map[span_id]
401+
expect(sentry_span).to be_a(Sentry::Span)
402+
403+
expect(sentry_span).to receive(:finish).and_call_original
404+
subject.on_finish(finished_queue_span_consumer)
405+
406+
expect(sentry_span.op).to eq('queue.process')
407+
expect(sentry_span.origin).to eq('auto.otel')
408+
expect(sentry_span.description).to eq('Sidekiq::Worker')
409+
expect(sentry_span.data).to include(finished_queue_span_consumer.attributes)
410+
expect(sentry_span.data).to include({ 'otel.kind' => finished_queue_span_consumer.kind })
411+
expect(sentry_span.timestamp).to eq(finished_queue_span_consumer.end_timestamp / 1e9)
412+
expect(sentry_span.status).to eq('ok')
413+
414+
expect(subject.span_map.size).to eq(6)
343415
expect(subject.span_map.keys).not_to include(span_id)
344416
end
345417

@@ -348,6 +420,8 @@
348420
subject.on_finish(finished_http_span)
349421
subject.on_finish(finished_error_span)
350422
subject.on_finish(finished_http_error_span)
423+
subject.on_finish(finished_queue_span)
424+
subject.on_finish(finished_queue_span_consumer)
351425

352426
expect(subject.span_map).to receive(:delete).and_call_original
353427

0 commit comments

Comments
 (0)