Skip to content

Commit 5cf39bf

Browse files
committed
WIP - add ActiveJobSubscriber
1 parent ed20911 commit 5cf39bf

File tree

2 files changed

+481
-0
lines changed

2 files changed

+481
-0
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# frozen_string_literal: true
2+
3+
require "sentry/rails/log_subscriber"
4+
5+
module Sentry
6+
module Rails
7+
module LogSubscribers
8+
# LogSubscriber for ActiveJob events that captures background job execution
9+
# and logs them using Sentry's structured logging system.
10+
#
11+
# This subscriber captures various ActiveJob events including job execution,
12+
# enqueueing, retries, and failures with relevant job information.
13+
#
14+
# @example Usage
15+
# # Enable structured logging for ActiveJob
16+
# Sentry.init do |config|
17+
# config.enable_logs = true
18+
# config.rails.structured_logging = true
19+
# config.rails.structured_logging.attach_to = [:active_job]
20+
# end
21+
class ActiveJobSubscriber < Sentry::Rails::LogSubscriber
22+
# Handle perform.active_job events
23+
#
24+
# @param event [ActiveSupport::Notifications::Event] The job performance event
25+
def perform(event)
26+
return if excluded_event?(event)
27+
28+
job = event.payload[:job]
29+
duration = duration_ms(event)
30+
31+
attributes = {
32+
job_class: job.class.name,
33+
job_id: job.job_id,
34+
queue_name: job.queue_name,
35+
duration_ms: duration,
36+
executions: job.executions,
37+
priority: job.priority
38+
}
39+
40+
attributes[:adapter] = job.class.queue_adapter.class.name if job.class.respond_to?(:queue_adapter)
41+
42+
if job.scheduled_at
43+
attributes[:scheduled_at] = job.scheduled_at.iso8601
44+
attributes[:delay_ms] = ((Time.current - job.scheduled_at) * 1000).round(2)
45+
end
46+
47+
if Sentry.configuration.send_default_pii && job.arguments.present?
48+
filtered_args = filter_sensitive_arguments(job.arguments)
49+
attributes[:arguments] = filtered_args unless filtered_args.empty?
50+
end
51+
52+
message = "Job performed: #{job.class.name}"
53+
54+
log_structured_event(
55+
message: message,
56+
level: :info,
57+
attributes: attributes
58+
)
59+
end
60+
61+
# Handle enqueue.active_job events
62+
#
63+
# @param event [ActiveSupport::Notifications::Event] The job enqueue event
64+
def enqueue(event)
65+
return if excluded_event?(event)
66+
67+
job = event.payload[:job]
68+
69+
attributes = {
70+
job_class: job.class.name,
71+
job_id: job.job_id,
72+
queue_name: job.queue_name,
73+
priority: job.priority
74+
}
75+
76+
attributes[:adapter] = job.class.queue_adapter.class.name if job.class.respond_to?(:queue_adapter)
77+
78+
if job.scheduled_at
79+
attributes[:scheduled_at] = job.scheduled_at.iso8601
80+
attributes[:delay_seconds] = (job.scheduled_at - Time.current).round(2)
81+
end
82+
83+
message = "Job enqueued: #{job.class.name}"
84+
85+
log_structured_event(
86+
message: message,
87+
level: :info,
88+
attributes: attributes
89+
)
90+
end
91+
92+
def retry_stopped(event)
93+
return if excluded_event?(event)
94+
95+
job = event.payload[:job]
96+
error = event.payload[:error]
97+
98+
attributes = {
99+
job_class: job.class.name,
100+
job_id: job.job_id,
101+
queue_name: job.queue_name,
102+
executions: job.executions,
103+
error_class: error.class.name,
104+
error_message: error.message
105+
}
106+
107+
message = "Job retry stopped: #{job.class.name}"
108+
109+
log_structured_event(
110+
message: message,
111+
level: :error,
112+
attributes: attributes
113+
)
114+
end
115+
116+
def discard(event)
117+
return if excluded_event?(event)
118+
119+
job = event.payload[:job]
120+
error = event.payload[:error]
121+
122+
attributes = {
123+
job_class: job.class.name,
124+
job_id: job.job_id,
125+
queue_name: job.queue_name,
126+
executions: job.executions
127+
}
128+
129+
attributes[:error_class] = error.class.name if error
130+
attributes[:error_message] = error.message if error
131+
132+
message = "Job discarded: #{job.class.name}"
133+
134+
log_structured_event(
135+
message: message,
136+
level: :warn,
137+
attributes: attributes
138+
)
139+
end
140+
141+
private
142+
143+
def filter_sensitive_arguments(arguments)
144+
return [] unless arguments.is_a?(Array)
145+
146+
arguments.map do |arg|
147+
case arg
148+
when Hash
149+
filter_sensitive_hash(arg)
150+
when String
151+
arg.length > 100 ? "[FILTERED: #{arg.length} chars]" : arg
152+
else
153+
arg
154+
end
155+
end
156+
end
157+
158+
def filter_sensitive_hash(hash)
159+
sensitive_keys = %w[
160+
password token secret api_key
161+
email personal_data user_data
162+
credit_card ssn social_security_number
163+
]
164+
165+
hash.reject do |key, _value|
166+
key_str = key.to_s.downcase
167+
sensitive_keys.any? { |sensitive| key_str.include?(sensitive) }
168+
end
169+
end
170+
end
171+
end
172+
end
173+
end

0 commit comments

Comments
 (0)