Skyline

Observability

Job lifecycle logging

Log every job transition — including the silent unique-lock discards.

A queued job passes through half a dozen states, and Laravel logs almost none of them. Some transitions are not even observable — a ShouldBeUnique dispatch that is discarded because the lock is held emits no event at all. Skyline surfaces the whole lifecycle to your logs, with every line tagged by job id so a single job's history is one grep away.

Enabling

Point log_channel at the channel that should receive the lines:

// config/horizon.php
'log_channel' => env('HORIZON_LOG_CHANNEL'),
# .env
HORIZON_LOG_CHANNEL=queue

Leave it unset to use the application's default channel. If the configured channel name does not resolve, Skyline falls back to the default channel rather than throwing out of a queue listener — a logging misconfiguration should never take down job processing.

Log level is the volume dial

Every event is emitted at a level chosen to match its operational severity, so you tune coverage purely by setting the level on the channel — no separate on/off switches.

  • error — terminal failures only.
  • warning — failures, timeouts, and the silent drops: unique-lock discards and WithoutOverlapping drops.
  • info — the above, plus releases, migrations and retries.
  • debug — everything, including a pending / reserved / completed line for every successful job.
Mind the volume at debug

At debug, a healthy queue writes three lines per job. On a queue doing 120,000 jobs a day that is 360,000 lines a day. Use debug to trace a problem, not as a steady state.

The events

Event Level When
job.pending debug Job was queued. Notes the delay, when delayed.
job.reserved debug A worker picked the job up and began processing.
job.completed debug Job finished successfully.
job.migrated info A delayed job became available and moved onto the ready queue.
job.released info
warning if the reason is an exception
Job went back onto the queue, with the reason and the delay before it runs again.
job.retried info A failed job was retried, recording the new job's id.
job.timed_out warning Job exceeded its timeout, noting whether it will be retried or was marked failed.
job.overlap_dropped warning Job was dropped without releasing because a WithoutOverlapping lock was held and the middleware is set to dontRelease().
unique_lock.discarded warning A dispatch was skipped because a ShouldBeUnique lock was already held. The job was never queued.
job.failed error Job failed terminally and will not be retried.

Lines read like this, with the job id in the message so a plain text search finds it:

[job:8813] queued onto [default] (delayed 30s)
[job:8813] migrated to [default] and is ready to run.
[job:8813] reserved from [default] and started processing.
[job:8813] released back to [default] (reason=exception, delay=15s)
[job:8813] failed on [default] and will not be retried (RuntimeException).

Structured context

Every line also carries a structured context array, which is what you actually query once the lines are in a log pipeline. All events include event, job_id, job, connection and queue. Beyond that:

Event Additional context
job.released reason, delay
job.timed_out will_retry
job.failed exception, message
job.retried retry_id
unique_lock.discarded unique_id, unique_lock_key, job_hash
job.overlap_dropped overlap_lock_key

Why a job was released

"Released back to the queue" is the single most ambiguous state in a Laravel queue: it can mean the job threw, that it hit an overlap lock, or that it called $job->release() itself. Skyline attributes it, so job.released carries a reason of exception, without_overlapping, or released (a manual release).

To distinguish overlap drops from manual releases

Import Skyline's drop-in middleware instead of the framework's — it is otherwise identical:

use Laravel\Horizon\Middleware\WithoutOverlapping;

Without it, a job released by an overlap lock is reported with the generic released reason, and job.overlap_dropped is never emitted.

Unique-lock discard caveat

Discard logging works by instrumenting the lock the framework takes at dispatch, and it covers the application's default cache store. A job that overrides uniqueVia() to lock on a different store is not covered — there is no framework event for the silent drop, so there is nothing to hook. The same applies if you have replaced the default cache repository with a custom subclass.

For the closely related problem of a lock that is never released, see Unique job locks.

Queue control, not just queue monitoring.

Skyline is a drop-in replacement for Laravel Horizon that lets you act on what you see — pause a queue, jump a job to the front, drain a backlog.

Sign up for early access