Skip to content

Commit 9446a30

Browse files
Add child_spans for Sidekiq Queue instrumentation (#2403)
* Add childspan for Sidekiq Queue instrumentation * Add basic specs * Remove unneeded exception alloc * Add CHANGELOG entry * Set root op instead of nested spans * Adjust feedback * Remove timecop_delay from options hash * Fix 1 day * Fix 1.day * Dont use .day helper
1 parent 27d7384 commit 9446a30

File tree

6 files changed

+81
-9
lines changed

6 files changed

+81
-9
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
### Features
44

55
- Add `include_sentry_event` matcher for RSpec [#2424](https://github.com/getsentry/sentry-ruby/pull/2424)
6-
- Add support for Sentry Cache instrumentation, when using Rails.cache [#2380](https://github.com/getsentry/sentry-ruby/pull/2380)
6+
- Add support for Sentry Cache instrumentation, when using Rails.cache ([#2380](https://github.com/getsentry/sentry-ruby/pull/2380))
7+
- Add support for Queue Instrumentation for Sidekiq. [#2403](https://github.com/getsentry/sentry-ruby/pull/2403)
78

89
Note: MemoryStore and FileStore require Rails 8.0+
910

sentry-sidekiq/Gemfile

+2
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@ end
2525

2626
gem "rails", "> 5.0.0"
2727

28+
gem "timecop"
29+
2830
eval_gemfile File.expand_path("../Gemfile", __dir__)

sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb

+30-5
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,24 @@
44

55
module Sentry
66
module Sidekiq
7+
module Helpers
8+
def set_span_data(span, id:, queue:, latency: nil, retry_count: nil)
9+
if span
10+
span.set_data("messaging.message.id", id)
11+
span.set_data("messaging.destination.name", queue)
12+
span.set_data("messaging.message.receive.latency", latency) if latency
13+
span.set_data("messaging.message.retry.count", retry_count) if retry_count
14+
end
15+
end
16+
end
17+
718
class SentryContextServerMiddleware
8-
OP_NAME = "queue.sidekiq"
19+
include Sentry::Sidekiq::Helpers
20+
21+
OP_NAME = "queue.process"
922
SPAN_ORIGIN = "auto.queue.sidekiq"
1023

11-
def call(_worker, job, queue)
24+
def call(worker, job, queue)
1225
return yield unless Sentry.initialized?
1326

1427
context_filter = Sentry::Sidekiq::ContextFilter.new(job)
@@ -23,7 +36,12 @@ def call(_worker, job, queue)
2336
scope.set_contexts(sidekiq: job.merge("queue" => queue))
2437
scope.set_transaction_name(context_filter.transaction_name, source: :task)
2538
transaction = start_transaction(scope, job["trace_propagation_headers"])
26-
scope.set_span(transaction) if transaction
39+
40+
if transaction
41+
scope.set_span(transaction)
42+
43+
set_span_data(transaction, id: job["jid"], queue: queue, latency: ((Time.now.to_f - job["enqueued_at"]) * 1000).to_i, retry_count: job["retry_count"] || 0)
44+
end
2745

2846
begin
2947
yield
@@ -63,13 +81,20 @@ def finish_transaction(transaction, status)
6381
end
6482

6583
class SentryContextClientMiddleware
66-
def call(_worker_class, job, _queue, _redis_pool)
84+
include Sentry::Sidekiq::Helpers
85+
86+
def call(worker_class, job, queue, _redis_pool)
6787
return yield unless Sentry.initialized?
6888

6989
user = Sentry.get_current_scope.user
7090
job["sentry_user"] = user unless user.empty?
7191
job["trace_propagation_headers"] ||= Sentry.get_trace_propagation_headers
72-
yield
92+
93+
Sentry.with_child_span(op: "queue.publish", description: worker_class.to_s) do |span|
94+
set_span_data(span, id: job["jid"], queue: queue)
95+
96+
yield
97+
end
7398
end
7499
end
75100
end

sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb

+39
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "spec_helper"
4+
require "timecop"
45

56
RSpec.shared_context "sidekiq", shared_context: :metadata do
67
let(:user) { { "id" => rand(10_000) } }
@@ -65,6 +66,30 @@
6566
expect(transaction.contexts.dig(:trace, :origin)).to eq('auto.queue.sidekiq')
6667
end
6768

69+
it "adds a queue.process spans" do
70+
Timecop.freeze do
71+
execute_worker(processor, HappyWorker)
72+
execute_worker(processor, HappyWorker, jid: '123456', timecop_delay: Time.now + 86400)
73+
74+
expect(transport.events.count).to eq(2)
75+
76+
transaction = transport.events[0]
77+
expect(transaction).not_to be_nil
78+
expect(transaction.spans.count).to eq(0)
79+
expect(transaction.contexts[:trace][:data]['messaging.message.id']).to eq('123123') # Default defined in #execute_worker
80+
expect(transaction.contexts[:trace][:data]['messaging.destination.name']).to eq('default')
81+
expect(transaction.contexts[:trace][:data]['messaging.message.retry.count']).to eq(0)
82+
expect(transaction.contexts[:trace][:data]['messaging.message.receive.latency']).to eq(0)
83+
84+
transaction = transport.events[1]
85+
expect(transaction).not_to be_nil
86+
expect(transaction.spans.count).to eq(0)
87+
expect(transaction.contexts[:trace][:data]['messaging.message.id']).to eq('123456') # Explicitly set above.
88+
expect(transaction.contexts[:trace][:data]['messaging.destination.name']).to eq('default')
89+
expect(transaction.contexts[:trace][:data]['messaging.message.receive.latency']).to eq(86400000)
90+
end
91+
end
92+
6893
context "with trace_propagation_headers" do
6994
let(:parent_transaction) { Sentry.start_transaction(op: "sidekiq") }
7095

@@ -73,6 +98,7 @@
7398
execute_worker(processor, HappyWorker, trace_propagation_headers: trace_propagation_headers)
7499

75100
expect(transport.events.count).to eq(1)
101+
76102
transaction = transport.events[0]
77103
expect(transaction).not_to be_nil
78104
expect(transaction.contexts.dig(:trace, :trace_id)).to eq(parent_transaction.trace_id)
@@ -156,5 +182,18 @@
156182
expect(second_headers["sentry-trace"]).to eq(transaction.to_sentry_trace)
157183
expect(second_headers["baggage"]).to eq(transaction.to_baggage)
158184
end
185+
186+
it "has a queue.publish span" do
187+
message_id = client.push('queue' => 'default', 'class' => HappyWorker, 'args' => [])
188+
189+
transaction.finish
190+
191+
expect(transport.events.count).to eq(1)
192+
event = transport.events.last
193+
expect(event.spans.count).to eq(1)
194+
expect(event.spans[0][:op]).to eq("queue.publish")
195+
expect(event.spans[0][:data]['messaging.message.id']).to eq(message_id)
196+
expect(event.spans[0][:data]['messaging.destination.name']).to eq('default')
197+
end
159198
end
160199
end

sentry-sidekiq/spec/sentry/sidekiq_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ def retry_last_failed_job
223223
expect(transaction.contexts.dig(:trace, :trace_id)).to be_a(String)
224224
expect(transaction.contexts.dig(:trace, :span_id)).to be_a(String)
225225
expect(transaction.contexts.dig(:trace, :status)).to eq("ok")
226-
expect(transaction.contexts.dig(:trace, :op)).to eq("queue.sidekiq")
226+
expect(transaction.contexts.dig(:trace, :op)).to eq("queue.process")
227227
end
228228

229229
it "records transaction with exception" do

sentry-sidekiq/spec/spec_helper.rb

+7-2
Original file line numberDiff line numberDiff line change
@@ -229,15 +229,20 @@ def sidekiq_config(opts)
229229

230230
def execute_worker(processor, klass, **options)
231231
klass_options = klass.sidekiq_options_hash || {}
232-
233232
# for Ruby < 2.6
234233
klass_options.each do |k, v|
235234
options[k.to_sym] = v
236235
end
237236

238-
msg = Sidekiq.dump_json(jid: "123123", class: klass, args: [], **options)
237+
jid = options.delete(:jid) || "123123"
238+
timecop_delay = options.delete(:timecop_delay)
239+
240+
msg = Sidekiq.dump_json(created_at: Time.now.to_f, enqueued_at: Time.now.to_f, jid: jid, class: klass, args: [], **options)
241+
Timecop.freeze(timecop_delay) if timecop_delay
239242
work = Sidekiq::BasicFetch::UnitOfWork.new('queue:default', msg)
240243
process_work(processor, work)
244+
ensure
245+
Timecop.return if timecop_delay
241246
end
242247

243248
def process_work(processor, work)

0 commit comments

Comments
 (0)