GoodJob

GoodJob runs ActiveJob jobs, so its jobs are logged by the ActiveJob integration (enqueue, schedule, start, finish and error lines). This integration covers GoodJob itself. When LogStruct is on, GoodJob.logger is a LogStruct logger, so GoodJob's own messages are JSON lines. Each error GoodJob passes to its thread error handler is one structured error line, and then your app's on_thread_error handler runs as before. LogStruct also turns itself on in the good_job start process.

What LogStruct changes

  • The logger. GoodJob.logger is replaced with a LogStruct logger named GoodJob (written as prog).
  • Thread errors. GoodJob passes the errors from its background threads (job tasks, the poller, the notifier, the cron manager, the probe server and the systemd watchdog) to GoodJob._on_thread_error. LogStruct logs each one, then calls your app's handler, so an error tracker that handler reports to still gets it. It works whether the app sets the handler in config.good_job.on_thread_error (which GoodJob applies after LogStruct's setup) or with GoodJob.on_thread_error = at any time. The app's handler runs even if LogStruct's line fails.
  • GoodJob's own error lines. GoodJob's log subscriber writes these errors as text with the whole backtrace in the message. LogStruct turns those lines off for the errors that reach the thread error handler, so each error is logged once.
  • good_job start. LogStruct counts GoodJob's executable as a server process, like Puma and Sidekiq, so LogStruct is on there in its enabled environments (test and production by default). It stops the executable adding a plain text STDOUT logger beside LogStruct's (GoodJob::CLI.log_to_stdout = false), so each line is written once, as JSON. In async mode GoodJob runs inside your web server's process, where LogStruct is on because it's a server.

Lines

GoodJob's messages

GoodJob's own messages are job log lines, with GoodJob's tags in tags:

{
  "src": "job",
  "evt": "log",
  "lvl": "info",
  "ts": "2026-10-17T03:23:11.213+00:00",
  "msg": "GoodJob 3.23.0 started scheduler with queues=* max_threads=5.",
  "pid": 72705,
  "tid": "4sg",
  "prog": "GoodJob",
  "tags": [
    "GoodJob",
    "1234"
  ]
}

A line GoodJob's logger writes while a job runs also has the job's job_id, job_class, queue_name, executions, scheduled_at and priority.

Thread errors

Each thread error is a job error line at error level:

{
  "src": "job",
  "evt": "error",
  "lvl": "error",
  "ts": "2026-10-17T03:23:11.212+00:00",
  "error_class": "ActiveRecord::ConnectionTimeoutError",
  "error_message": "could not obtain a connection from the pool within 5.000 seconds (waited 5.001 seconds); all pooled connections were in use",
  "pid": 72705,
  "tid": "4sg",
  "backtrace": [
    "lib/good_job/notifier.rb:250:in 'GoodJob::Notifier#with_connection'",
    "lib/good_job/notifier.rb:198:in 'block in GoodJob::Notifier#create_listen_task'"
  ],
  "prog": "GoodJob"
}

GoodJob also passes every failed job's exception to the thread error handler. The ActiveJob integration has already logged it, with the job's ID and class, so LogStruct doesn't log it again here; your handler still gets it. Exceptions that aren't StandardErrors (GoodJob's handled_exceptions setting can let them through) are logged and passed on too.

Notifier connection errors

GoodJob's notifier passes database connection errors to the thread error handler only once they repeat. Until then GoodJob logs each one itself, and LogStruct keeps that line, without the backtrace:

{
  "src": "job",
  "evt": "log",
  "lvl": "error",
  "ts": "2026-10-17T03:23:11.213+00:00",
  "msg": "Notifier errored: ActiveRecord::ConnectionNotEstablished: connection to server failed",
  "pid": 72705,
  "tid": "4sg",
  "prog": "GoodJob",
  "tags": [
    "GoodJob"
  ]
}

Fields

job log lines from GoodJob.logger:

FieldValue
msgGoodJob's message.
pidThe process ID.
tidThe thread's object ID, in base 36.
tagsGoodJob's tags, such as the process ID.
job_idDuring a job: the job's ActiveJob ID.
job_classDuring a job: the job's class.
queue_nameDuring a job: the job's queue.
executionsDuring a job: how many times the job has run.
scheduled_atDuring a job: when the job was scheduled to run.
priorityDuring a job: the job's priority.

job error lines (thread errors):

FieldValue
error_classThe exception's class.
error_messageThe exception's message (these lines have no msg).
backtraceThe backtrace, up to max_backtrace_lines (default 20).
pidThe process ID.
tidThe thread's object ID, in base 36.

GoodJob versions

  • Thread errors are logged on GoodJob 3.0 and later, which pass every thread error through GoodJob._on_thread_error.
  • The job fields come from GoodJob 4's GoodJob::CurrentThread.job or GoodJob 3's GoodJob::CurrentThread.execution.
  • LogStruct's tests use a stand-in for GoodJob's API, not the gem. The lines on this page were written with GoodJob 3.23.

Settings

config.integrations.enable_goodjob (default true). The job lines come from the ActiveJob integration (config.integrations.enable_activejob). See Configuration for the other settings.

LogStruct.configure do |config|
  # On by default. false keeps GoodJob's own logger and error lines.
  config.integrations.enable_goodjob = false
end

Not logged

  • GoodJob's own text lines for job task errors, systemd watchdog errors and notifier errors other than connection errors. Each of those errors is in LogStruct's thread error line instead (or, for a failed job, in the ActiveJob integration's error line).
  • Loggers your app added to GoodJob::LogSubscriber.loggers no longer get job errors; they're in the structured lines.
  • There are no GoodJob-specific enqueue, start or finish lines: those are the ActiveJob integration's lines.
Values in every line are filtered and scrubbed as described in Filtering Sensitive Data.