ruby
43 lines · 6 steps
Instrumenting every job in Rails
A base ActiveJob class that adds retry policy and structured, timed logging around every job's execution.
Explained by
highlit
1class ApplicationJob < ActiveJob::Base
2 retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
3 discard_on ActiveJob::DeserializationError
4
5 around_perform do |job, block|
6 metadata = {
7 job_id: job.job_id,
8 job_class: job.class.name,
9 queue: job.queue_name,
10 arguments: job.arguments.map { |arg| summarize(arg) },
11 executions: job.executions,
12 enqueued_at: job.enqueued_at
13 }
14
15 Rails.logger.tagged("job:#{job.class.name}", job.job_id) do
16 started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
17 Rails.logger.info(metadata.merge(event: "job.started").to_json)
18
19 begin
20 block.call
21 rescue => error
22 Rails.logger.error(metadata.merge(event: "job.failed", error: error.class.name, message: error.message).to_json)
23 raise
24 ensure
25 duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
26 Rails.logger.info(metadata.merge(event: "job.finished", duration_ms: (duration * 1000).round(2)).to_json)
27 end
28 end
29 end
30
31 private
32
33 def summarize(argument)
34 case argument
35 when ActiveRecord::Base
36 { gid: argument.to_gid_param }
37 when Hash
38 argument.transform_values { |value| summarize(value) }
39 else
40 argument
41 end
42 end
43end
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1An around_perform callback is the natural place to wrap every job with cross-cutting concerns like logging and timing.
- 2Emitting JSON events with a monotonic clock gives you reliable, queryable metrics on job duration and failures.
- 3Sanitizing arguments before logging keeps sensitive records and payloads out of your logs while preserving traceability.
Related explainers
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
ruby
class LogAggregator BUCKET_FORMAT = "%Y-%m-%dT%H:%M" def initialize(entries)
Bucketing log entries by the minute in Ruby
aggregation
hashing
enumerable
Intermediate
5 steps
ruby
class WeeklySignupsReport DEFAULT_WEEKS = 12 def initialize(weeks: DEFAULT_WEEKS, source: User.all)
Building a weekly signups report in Rails
service object
aggregation
group by
Intermediate
7 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
Intermediate
8 steps
ruby
class ApplicationController < ActionController::Base EXPERIMENTS = { checkout_button_color: %w[control blue green], onboarding_flow: %w[control streamlined]
How A/B test cohorts are assigned in Rails
a-b-testing
cookies
hashing
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/instrumenting-every-job-in-rails-explained-ruby-68f4/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.