Skip to content

Prometheus

Enable via QUEUE_INSIGHTS_PROMETHEUS_ENABLED=true. Mounts GET /metrics (path configurable) exposing queue-insights state in Prometheus 0.0.4 text format — or OpenMetrics 1.0.0 when the scraper sends Accept: application/openmetrics-text (Prometheus negotiates this automatically). Default-off; adoption is opt-in.

Auth is fail-closed: the package's default middleware refuses with 403 unless prometheus.token (preferred for shared infra) or prometheus.allow_ips (CIDR list) is configured. There is no silent open default.

bash
# .env
QUEUE_INSIGHTS_PROMETHEUS_ENABLED=true
QUEUE_INSIGHTS_PROMETHEUS_TOKEN=long-random-string
yaml
# prometheus.yml
scrape_configs:
  - job_name: laravel-queue-insights
    metrics_path: /metrics
    bearer_token: long-random-string
    static_configs:
      - targets: ['app.example.com']

Metric catalogue

MetricTypeLabelsNotes
queue_insights_queue_depthgaugeconnection, queueMirrors snapshot loop output. Pair with queue_insights_snapshot_alive.
queue_insights_inflight_jobsgaugeconnection, queueZCARD inflight-zset.
queue_insights_pending_jobsgaugeconnection, queueRunnable now (available_at <= now).
queue_insights_delayed_jobsgaugeconnection, queueNot yet runnable (available_at > now).
queue_insights_oldest_pending_age_secondsgaugeconnection, queue0 when empty.
queue_insights_oldest_inflight_age_secondsgaugeconnection, queue0 when empty.
queue_insights_jobs_processed_totalcounterclass, connectionTrue monotonic INCR — safe for rate() / increase().
queue_insights_jobs_failed_totalcounterclass, connectionSame.
queue_insights_job_duration_count_totalcounterclass, connectionMean = rate(sum) / rate(count) Prometheus-side.
queue_insights_job_duration_sum_seconds_totalcounterclass, connectionSeconds (HINCRBY sum_ms ÷ 1000).
queue_insights_job_duration_max_secondsgaugeclass, connectionLifetime max. Use max_over_time() for windowed maxima.
queue_insights_alert_activegaugerule, connection, queue, severity (+ class for class-scoped rules)Always 1 when present; absent series = no alert. Use OR on() vector(0) Grafana-side to render gaps as 0.
queue_insights_snapshot_alivegaugeconnection, queue1/0. Use this in alerts, not _age_seconds.
queue_insights_snapshot_age_secondsgaugeconnection, queueOmitted when the snapshot key is absent (so alerts can use absent(...) cleanly instead of clamping to 0).
queue_insights_snapshot_errors_totalcounterconnection, queueMonotonic INCR — paired with the existing 10-min snapshot:error:* boolean.
queue_insights_exporter_collect_duration_secondsgauge(none)Wall-clock seconds of the previous collect cycle.

Per-class metrics (*_processed_total, *_failed_total, duration aggregates) are opt-in by class to bound cardinality. Default class_filter.mode = allow_list with empty classes → no per-class metrics emitted. Three modes:

php
// config/queue-insights.php
'prometheus' => [
    'class_filter' => [
        // 'allow_list'        — only emit for the FQCNs in `classes` (DEFAULT)
        // 'allow_all'         — emit for every class on classes:{connection}
        // 'top_n_by_recency'  — top N most-recently-seen per connection (recency, NOT throughput)
        'mode' => 'allow_list',
        'classes' => [
            App\Jobs\GenerateReport::class,
            App\Jobs\SyncCustomer::class,
        ],
        'top_n' => 50,
    ],
],

A two-tier cache (per-request memoise + 5 s Redis cache, key prom:cache:rendered:{flavour}) bounds thunder-herd when multiple Prometheus replicas scrape concurrently. Set prometheus.cache_ttl_seconds = 0 to disable both layers for instant reads.

Each metric family has its own toggle under prometheus.metrics.* (default-on) — disable any family the host doesn't need to keep the scrape body lean.

Scheduler metrics

When scheduler.enabled = true AND each per-family toggle below is set, the exporter emits scheduler-side families. Default OFF — adoption is opt-in per family (mirrors the per-class queue metrics stance).

MetricTypeLabelsNotes
queue_insights_scheduled_task_runs_totalcountertask, statusStatus: success (= total_runs - total_failed), failed, skipped. Hung + missed are separate families below.
queue_insights_scheduled_task_runtime_sum_seconds_totalcountertaskLifetime runtime sum, seconds. Pair with queue_insights_scheduled_task_runs_total for mean: rate(sum) / rate(runs_total{status=~"success|failed"}). Sample omitted until the first finished run.
queue_insights_scheduled_task_last_run_timestampgaugetask, statusUnix ts (seconds) of last run per status. Page on time() - queue_insights_scheduled_task_last_run_timestamp{status="success"} > N. Sample omitted when no run of that status exists.
queue_insights_scheduled_task_hung_totalcountertaskDetections from HungTaskReconciler.
queue_insights_scheduled_task_missed_totalcountertaskDetections from MissedRunReconciler.
queue_insights_scheduled_task_in_flightgaugetask1 when the task is mid-run (Started without Finished/Failed). Sample omitted when not running.
queue_insights_scheduled_snapshot_age_secondsgauge(none)Seconds since the schedule snapshot was last rewritten on app boot. Omitted when never written (alerts use absent(...) cleanly).
queue_insights_scheduled_sweeper_age_secondsgauge(none)Seconds since MissedRunReconciler last completed a tick. Alert on > 2 × sweeper.sweep_seconds.

Toggle each family independently:

php
'prometheus' => [
    'metrics' => [
        // ...
        'scheduler_runs_total' => true,
        'scheduler_runtime_sum' => true,
        'scheduler_last_run_timestamp' => true,
        'scheduler_hung_total' => true,
        'scheduler_missed_total' => true,
        'scheduler_in_flight' => true,
        'scheduler_snapshot_age' => true,
        'scheduler_sweeper_age' => true,
    ],

    // Per-task cardinality control. Task rosters are typically <100,
    // so the default is `allow_all`. `top_n_by_recency` is intentionally
    // not supported — see internal/specs/cron-monitoring/07-platform-extensions.md §1.3.
    'task_filter' => [
        'mode' => 'allow_all',  // | allow_list
        'tasks' => [],          // taskKey list, used when mode = allow_list
    ],
],

runtime_max_seconds is intentionally NOT shipped in v1 — would need a Lua HSET-IF-GREATER write path. Operators who need lifetime max can compute max_over_time(queue_insights_scheduled_task_runtime_sum_seconds_total[N]) Prometheus-side as a coarse proxy, or run the per-task duration sparkline in the dashboard for exact values.

Push gateway (short-lived workers, CLI)

For processes that exit before any scrape can land, php artisan queue-insights:prometheus-push does a one-shot collect + PUT to a configured Pushgateway. Long-running workers should be scraped, not pushed — push-mode is for CLI scripts and scheduled tasks where pull-mode can't reach the process.

bash
# .env
QUEUE_INSIGHTS_PUSHGATEWAY_URL=https://pushgateway.example/metrics
QUEUE_INSIGHTS_PUSHGATEWAY_JOB=laravel-queue-insights
QUEUE_INSIGHTS_PUSHGATEWAY_INSTANCE=worker-01   # required for clustered hosts

The command fails closed when pushgateway.instance is unset and --accept-shared-grouping is not passed: clustered hosts that share a job label without distinct instance values silently overwrite each other's pushed metrics. Pass --accept-shared-grouping once you've confirmed single-replica semantics, or set instance per-replica.

bash
php artisan queue-insights:prometheus-push                           # PUT metrics
php artisan queue-insights:prometheus-push --delete                  # DELETE the grouping
php artisan queue-insights:prometheus-push --accept-shared-grouping  # opt out of the instance guard

Exit codes mirror Symfony Console convention: 0 success, 1 Pushgateway HTTP failure, 2 config error (missing URL / unset instance without override).