Skip to content

Commit d8fa1a6

Browse files
feat(active_job): add interrupt.active_job span event for Continuation
Record a span event on the current span when a continuable job is interrupted, with attributes for the interrupt reason, description, and completed steps.
1 parent be61b16 commit d8fa1a6

3 files changed

Lines changed: 139 additions & 1 deletion

File tree

instrumentation/active_job/lib/opentelemetry/instrumentation/active_job/handlers.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require_relative 'handlers/default'
99
require_relative 'handlers/enqueue'
1010
require_relative 'handlers/perform'
11+
require_relative 'handlers/interrupt'
1112
require_relative 'handlers/resume'
1213
require_relative 'handlers/step'
1314
require_relative 'handlers/step_skipped'
@@ -30,6 +31,12 @@ module Handlers
3031
# - perform
3132
# - retry_stopped
3233
# - discard
34+
# - step (Continuations, Rails 8.1+)
35+
#
36+
# The following events are recorded as span events:
37+
# - resume (Continuations, Rails 8.1+)
38+
# - step_skipped (Continuations, Rails 8.1+)
39+
# - interrupt (Continuations, Rails 8.1+)
3340
#
3441
# Ingress and Egress spans (perform, enqueue, enqueue_at) use Messaging semantic conventions for naming the span,
3542
# while internal spans keep their ActiveSupport event name.
@@ -52,6 +59,7 @@ def subscribe
5259
enqueue_handler = Handlers::Enqueue.new(parent_span_provider, mapper, config)
5360
perform_handler = Handlers::Perform.new(parent_span_provider, mapper, config)
5461
step_handler = Handlers::Step.new(parent_span_provider, mapper, config)
62+
interrupt_handler = Handlers::Interrupt.new(parent_span_provider)
5563
resume_handler = Handlers::Resume.new(parent_span_provider)
5664
step_skipped_handler = Handlers::StepSkipped.new(parent_span_provider)
5765

@@ -64,7 +72,8 @@ def subscribe
6472
'discard' => default_handler,
6573
'step' => step_handler,
6674
'resume' => resume_handler,
67-
'step_skipped' => step_skipped_handler
75+
'step_skipped' => step_skipped_handler,
76+
'interrupt' => interrupt_handler
6877
}
6978

7079
@subscriptions = handlers_by_pattern.map do |key, handler|
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
module OpenTelemetry
8+
module Instrumentation
9+
module ActiveJob
10+
module Handlers
11+
# Handles interrupt.active_job to record a span event when a continuable
12+
# job is interrupted for resumption
13+
class Interrupt
14+
def initialize(parent_span_provider)
15+
@parent_span_provider = parent_span_provider
16+
end
17+
18+
# @param _name [String] of the Event (unused)
19+
# @param _id [String] of the event (unused)
20+
# @param payload [Hash] containing job run information
21+
def start(_name, _id, payload)
22+
span = @parent_span_provider.current_span
23+
return unless span.recording?
24+
25+
attributes = {
26+
'messaging.active_job.continuation.reason' => payload[:reason].to_s,
27+
'messaging.active_job.continuation.description' => payload[:description]
28+
}
29+
30+
span.add_event('interrupt', attributes: attributes)
31+
end
32+
33+
def finish(_name, _id, _payload); end
34+
end
35+
end
36+
end
37+
end
38+
end
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
require 'test_helper'
8+
9+
require_relative '../../../../../lib/opentelemetry/instrumentation/active_job'
10+
11+
require 'active_job/continuation/test_helper' if defined?(ActiveJob::Continuable)
12+
13+
describe OpenTelemetry::Instrumentation::ActiveJob::Handlers::Interrupt do
14+
let(:instrumentation) { OpenTelemetry::Instrumentation::ActiveJob::Instrumentation.instance }
15+
let(:config) { { propagation_style: :link, span_naming: :queue } }
16+
let(:exporter) { EXPORTER }
17+
let(:spans) { exporter.finished_spans }
18+
let(:process_span) { spans.find { |s| s.name == 'default process' } }
19+
20+
before do
21+
skip 'Requires ActiveJob::Continuable (Rails 8.1+)' unless defined?(ActiveJob::Continuable)
22+
23+
singleton_class.include ActiveJob::Continuation::TestHelper
24+
25+
OpenTelemetry::Instrumentation::ActiveJob::Handlers.unsubscribe
26+
instrumentation.instance_variable_set(:@config, config)
27+
instrumentation.instance_variable_set(:@installed, false)
28+
29+
instrumentation.install(config)
30+
ActiveJob::Base.queue_adapter = :test
31+
32+
exporter.reset
33+
end
34+
35+
after do
36+
ActiveJob::Base.queue_adapter = :inline
37+
end
38+
39+
describe 'when a job is interrupted between steps' do
40+
it 'adds an interrupt event to the process span' do
41+
ContinuableJob.perform_later
42+
interrupt_job_after_step(ContinuableJob, :first_step) { perform_enqueued_jobs }
43+
44+
interrupt_events = process_span.events&.select { |e| e.name == 'interrupt' } || []
45+
46+
_(interrupt_events.length).must_equal 1
47+
end
48+
49+
it 'includes the interrupt reason' do
50+
ContinuableJob.perform_later
51+
interrupt_job_after_step(ContinuableJob, :first_step) { perform_enqueued_jobs }
52+
53+
interrupt_event = process_span.events.find { |e| e.name == 'interrupt' }
54+
55+
_(interrupt_event.attributes['messaging.active_job.continuation.reason']).must_equal 'stopping'
56+
end
57+
58+
it 'includes the description' do
59+
ContinuableJob.perform_later
60+
interrupt_job_after_step(ContinuableJob, :first_step) { perform_enqueued_jobs }
61+
62+
interrupt_event = process_span.events.find { |e| e.name == 'interrupt' }
63+
64+
_(interrupt_event.attributes['messaging.active_job.continuation.description']).wont_be_nil
65+
end
66+
end
67+
68+
describe 'when a job is interrupted during a step' do
69+
it 'adds an interrupt event to the process span, not the step span' do
70+
ContinuableWithCursorJob.perform_later
71+
interrupt_job_during_step(ContinuableWithCursorJob, :process_items, cursor: 2) { perform_enqueued_jobs }
72+
73+
step_span = spans.find { |s| s.name == 'default process_items' }
74+
step_interrupt_events = step_span&.events&.select { |e| e.name == 'interrupt' } || []
75+
process_interrupt_events = process_span.events&.select { |e| e.name == 'interrupt' } || []
76+
77+
_(step_interrupt_events).must_be(:empty?)
78+
_(process_interrupt_events.length).must_equal 1
79+
end
80+
end
81+
82+
describe 'when a job completes without interruption' do
83+
it 'does not add an interrupt event' do
84+
ContinuableJob.perform_later
85+
perform_enqueued_jobs
86+
87+
interrupt_events = process_span.events&.select { |e| e.name == 'interrupt' } || []
88+
_(interrupt_events).must_be(:empty?)
89+
end
90+
end
91+
end

0 commit comments

Comments
 (0)