Observability
JSON worker output
Emit structured, one-object-per-line worker output for your log pipeline.
Each worker writes a line per job to its standard output. The default is the human-readable table you see in the terminal — fine at a terminal, awkward in a log pipeline that would rather parse a field than a column. Skyline can switch that output to structured, one-object-per-line JSON.
Enabling
// config/horizon.php
'worker_output' => env('HORIZON_WORKER_OUTPUT', 'cli'),
# .env
HORIZON_WORKER_OUTPUT=json
The value is trimmed and lower-cased before it is compared, so a stray JSON or a trailing space still
opts in rather than silently falling back. Any value other than json means CLI output.
Because the setting is read by the worker process itself, a manually invoked php artisan horizon:work
honours it too — you do not have to thread a flag through the supervisor.
JSON output is built on the framework's structured worker output, which exists on Laravel 11 and later. On earlier versions the setting is never consulted and output stays in CLI format. No error is raised.
Format
Each job produces a starting line when a worker picks it up and a terminal line when it finishes, on one
line each:
{"level":"info","id":"8813","uuid":"9f2c…","connection":"redis","queue":"default","job":"App\\Jobs\\ProcessPodcast","status":"starting","attempts":1,"timestamp":"2026-07-10T09:14:02.114302+00:00"}
{"level":"info","id":"8813","uuid":"9f2c…","connection":"redis","queue":"default","job":"App\\Jobs\\ProcessPodcast","status":"success","result":"deleted","attempts":1,"timestamp":"2026-07-10T09:14:04.882015+00:00","duration":2.767713}
| Field | Meaning |
|---|---|
level | info for starting and success; warning otherwise. |
id | The queue's job id — the same id used throughout the dashboard and in lifecycle logging. |
uuid | The job's uuid. |
connection | Queue connection name. |
queue | Queue name. |
job | Resolved job class name. |
status | starting, success, released_after_exception, or failed. |
result | deleted, released or failed — the job's disposition after the attempt. |
attempts | Attempt number for this run. |
exception | Exception class, on a failure. |
message | Exception message, on a failure. |
duration | Seconds the attempt took. Absent on the starting line. |
timestamp | ISO-8601 timestamp with microseconds. |
Empty fields are omitted rather than emitted as null, so a successful job carries no exception key at all.
Worker output or lifecycle logging?
They overlap, and they are not the same tool.
- JSON worker output is what the worker process prints to stdout. It is the natural fit when your containers ship stdout to a log aggregator, and it covers the job's execution — picked up, finished, how long it took.
- Job lifecycle logging goes through Laravel's logging stack to a channel you choose, and covers events a worker never sees at all: a job being queued, a delayed job migrating, a dispatch discarded because a unique lock was held.
Both tag lines with the same job id, so if you enable both you can join across them. A common setup is
JSON worker output for execution telemetry, plus a lifecycle channel at warning for the silent drops.