ActiveJob

When LogStruct is on, it detaches Rails' ActiveJob log subscriber and attaches its own, which writes a job line when a job is enqueued, scheduled, started, finished, retried, discarded or fails. It works with any queue backend (Sidekiq, GoodJob, SolidQueue, the async adapter, ...), since it listens to ActiveJob's own events. The lines go to ActiveJob::Base.logger and carry ActiveJob's tags.

Lines

enqueue

perform_later without a wait. This job was enqueued with InvoiceJob.perform_later(42, {"api_key" => key, "plan" => "pro"}):

{
  "src": "job",
  "evt": "enqueue",
  "lvl": "info",
  "ts": "2026-10-17T03:21:01.874+00:00",
  "job_id": "a6219a2b-5c94-4030-acd8-9d3f9809cfab",
  "job_class": "InvoiceJob",
  "queue_name": "billing",
  "arguments": [
    42,
    {
      "api_key": {
        "_filtered": {
          "_class": "String"
        }
      },
      "plan": "pro"
    }
  ],
  "executions": 0,
  "prog": "Rails",
  "tags": [
    "ActiveJob"
  ]
}

perform_all_later writes an enqueue or schedule line for each job it enqueued. A job the backend couldn't take (ActiveJob::EnqueueError, or another exception while enqueuing) writes an error line instead, with executions 0, and a job that a before_enqueue callback stopped writes no line. Rails tags the lines of perform_later with ActiveJob, but not those of perform_all_later.

Some adapters (Sidekiq's) enqueue the jobs together and say only how many they enqueued. When that's all of them, each job has its line. When it's fewer (a Sidekiq client middleware stopped one), there's one warn line with both counts instead, since the adapter doesn't say which jobs it enqueued. If the adapter raises (Redis is down), perform_all_later raises too, and there's one error line with the error and the number of jobs. With other adapters, the jobs enqueued before the error have their lines, and so do the jobs the adapter refused (ActiveJob::EnqueueError) and the job that raised.

schedule

perform_later with wait or wait_until, and the retries that retry_on schedules:

{
  "src": "job",
  "evt": "schedule",
  "lvl": "info",
  "ts": "2026-10-17T03:21:01.877+00:00",
  "job_id": "60fae424-39f5-4615-b61b-5cf019045965",
  "job_class": "InvoiceJob",
  "queue_name": "billing",
  "arguments": [
    42
  ],
  "executions": 0,
  "scheduled_at": "2026-10-18T09:00:00.000Z",
  "prog": "Rails",
  "tags": [
    "ActiveJob"
  ]
}

start

Before the job's perform runs. During the job, ActiveJob tags every line with the job's class and ID.

{
  "src": "job",
  "evt": "start",
  "lvl": "info",
  "ts": "2026-10-17T03:21:01.877+00:00",
  "job_id": "76614a80-6f74-4887-b681-ad39718b2633",
  "job_class": "InvoiceJob",
  "queue_name": "billing",
  "arguments": [
    42,
    {
      "plan": "pro"
    }
  ],
  "executions": 1,
  "started_at": "2026-10-17T03:21:01.877+00:00",
  "attempt": 1,
  "prog": "Rails",
  "tags": [
    "ActiveJob",
    "InvoiceJob",
    "76614a80-6f74-4887-b681-ad39718b2633"
  ]
}

finish

After perform returns, or when the job's own retry_on, discard_on or rescue_from handled its exception. A job that a before_perform callback stopped didn't run, and has no finish line.

{
  "src": "job",
  "evt": "finish",
  "lvl": "info",
  "ts": "2026-10-17T03:21:01.878+00:00",
  "job_id": "76614a80-6f74-4887-b681-ad39718b2633",
  "job_class": "InvoiceJob",
  "queue_name": "billing",
  "arguments": [
    42,
    {
      "plan": "pro"
    }
  ],
  "executions": 1,
  "duration_ms": 0.23600000143051147,
  "finished_at": "2026-10-17T03:21:01.878+00:00",
  "prog": "Rails",
  "tags": [
    "ActiveJob",
    "InvoiceJob",
    "76614a80-6f74-4887-b681-ad39718b2633"
  ]
}

error

The job raised an exception that it didn't handle, or the backend couldn't enqueue it (then executions is 0), at error level. Any Exception is logged, not only StandardErrors. Sidekiq's and GoodJob's integrations don't log the same exception again.

{
  "src": "job",
  "evt": "error",
  "lvl": "error",
  "ts": "2026-10-17T03:21:01.879+00:00",
  "job_id": "99d68b68-2726-4c1c-8dc3-39e5c3bdecf1",
  "job_class": "FailingJob",
  "queue_name": "default",
  "arguments": [
    7
  ],
  "executions": 1,
  "error_class": "ArgumentError",
  "msg": "Template not found",
  "backtrace": [
    "app/jobs/failing_job.rb:3:in 'FailingJob#perform'"
  ],
  "prog": "Rails",
  "tags": [
    "ActiveJob",
    "FailingJob",
    "99d68b68-2726-4c1c-8dc3-39e5c3bdecf1"
  ]
}

retry

retry_on caught the job's exception and enqueued the job again, after wait_ms. The retry itself has a schedule line just before this one, and the run that raised has a finish line after it.

{
  "src": "job",
  "evt": "retry",
  "lvl": "info",
  "ts": "2026-10-17T04:57:05.105+00:00",
  "job_id": "cadb49f1-8913-4dbf-a73d-00b2742f6dbf",
  "job_class": "SyncJob",
  "queue_name": "default",
  "arguments": [
    "orders"
  ],
  "executions": 1,
  "error_class": "Timeout::Error",
  "msg": "API timed out",
  "wait_ms": 3000.0,
  "prog": "Rails",
  "tags": [
    "ActiveJob",
    "SyncJob",
    "cadb49f1-8913-4dbf-a73d-00b2742f6dbf"
  ]
}

retry_stopped

The exception came back on the last of retry_on's attempts, at error level. The line has the same fields as a retry line, without wait_ms. The job then raises the exception, which is an error line, unless retry_on was given a block.

discard

discard_on caught the job's exception and dropped the job, at error level, as Rails logs it. A finish line follows.

{
  "src": "job",
  "evt": "discard",
  "lvl": "error",
  "ts": "2026-10-17T04:57:05.106+00:00",
  "job_id": "87dceb1e-e870-4424-8e83-501287c188ac",
  "job_class": "InvoiceJob",
  "queue_name": "billing",
  "arguments": [
    42
  ],
  "executions": 1,
  "error_class": "ActiveRecord::RecordNotFound",
  "msg": "Couldn't find Invoice with 'id'=42",
  "prog": "Rails",
  "tags": [
    "ActiveJob",
    "InvoiceJob",
    "87dceb1e-e870-4424-8e83-501287c188ac"
  ]
}

Fields

Every job line has:

FieldValue
job_idThe job's ActiveJob ID.
job_classThe job's class.
queue_nameThe job's queue.
executionsHow many times the job has run: 0 when it's enqueued, 1 on its first run.
provider_job_idThe backend's own ID for the job (a Sidekiq jid, a SolidQueue row ID), as a String, when the backend sets one.
argumentsThe job's arguments, filtered and scrubbed (see Arguments). Left out when the job's log_arguments is off.
tagsActiveJob's tags.

And per event:

FieldValue
scheduled_atschedule: when the job is due to run.
started_atstart: when the job started.
attemptstart: the same number as executions.
duration_msfinish: how long perform took, in milliseconds.
finished_atfinish: when the job finished, the same time as ts.
error_classerror, retry, retry_stopped, discard: the exception's class.
msgerror, retry, retry_stopped, discard: the exception's message.
backtraceerror: the backtrace, up to max_backtrace_lines (default 20).
wait_msretry: how long until the job runs again, in milliseconds.

Arguments

  • Arguments are logged when the job class's log_arguments is on, which is Rails' default. Turn it off in a job whose arguments you don't want in your logs (the example below).
  • Arguments are filtered and scrubbed like any other value: the keys of a Hash argument are matched against your filter keys and Rails' filter_parameters, and strings are scrubbed (email addresses, card numbers and the rest). A plain String argument has no key, so it's only scrubbed: a token passed as a String is logged unless one of the scrubbers matches it. See Filtering Sensitive Data.
  • A record, or any other GlobalID object, is written as its GlobalID (gid://my-app/User/5).
  • For ActionMailer::MailDeliveryJob (every deliver_later) and its subclasses, only the first three arguments are logged: the mailer, the mail and the delivery method, as in ["UserMailer", "reset_password", "deliver_now"]. The mail's own arguments are left out, because mailers are often passed tokens as plain Strings (Devise passes its confirmation, password reset and invitation tokens that way), which no key-based filter can find.
class ImportJob < ActiveJob::Base
  # Leave this job's arguments out of its job lines
  self.log_arguments = false

  def perform(file_url, api_token)
  end
end

Settings

config.integrations.enable_activejob (default true), and each job's log_arguments. With the integration off, Rails' own ActiveJob lines are written instead, as text in msg. They include the arguments of every job whose log_arguments is on, mail arguments too. See Configuration for the other settings.

LogStruct.configure do |config|
  # On by default. false keeps Rails' own ActiveJob log lines.
  config.integrations.enable_activejob = false
end

Not logged

  • A job that a before_enqueue callback stopped has no line, and one that a before_perform callback stopped has a start line only.