Integrations & Developer Hooks
Integrations & Developer Hooks
This page is for package authors and platform teams wiring Queue Autoscale into dashboards, monitor packages, alerting and audit pipelines.
There are four integration surfaces:
- The facade / service API
- The cluster JSON snapshot
- The Laravel event stream
- The optional
cboxdk/laravel-telemetryprovider
Use the facade when your integration runs inside the same Laravel app. Use the JSON snapshot when another process wants a current-state document. Use events when you need an append-only operational trace. Use telemetry when you already run an OpenTelemetry pipeline.
Facade API
Cbox\LaravelQueueAutoscale\LaravelQueueAutoscale has exactly two public methods, both exposed
through the facade:
use Cbox\LaravelQueueAutoscale\Facades\LaravelQueueAutoscale;
$cluster = LaravelQueueAutoscale::cluster(); // array<string, mixed>
$metrics = LaravelQueueAutoscale::clusterMetrics(); // list<array{name, value, labels}>
There is no per-queue accessor, no decision history reader and no worker-pool API on the facade. Anything else you need comes from events or from resolving the internal classes yourself.
cluster()
Returns the cluster summary published by the current leader, read from ClusterStore.
It returns an empty array when cluster mode is disabled (QUEUE_AUTOSCALE_CLUSTER_ENABLED
defaults to false), and also before any leader has published a summary. Always guard:
$cluster = LaravelQueueAutoscale::cluster();
if ($cluster === []) {
// Cluster mode off, or no summary published yet.
}
The payload is intentionally an array rather than a typed DTO, and fields are added over time. Read
defensively with ?? defaults.
clusterMetrics()
Flattens the same summary into exporter-friendly rows:
$rows = [
['name' => 'queue_autoscale_cluster_managers', 'value' => 3, 'labels' => ['cluster' => 'my-app']],
['name' => 'queue_autoscale_cluster_host_workers', 'value' => 8, 'labels' => [
'cluster' => 'my-app',
'manager_id' => 'manager-1',
'host' => 'web-01',
'leader' => 'true',
]],
];
Every row is ['name' => string, 'value' => int|float, 'labels' => array<string, scalar|null>].
The series produced are:
| Scope | Metric names |
|---|---|
| Cluster | queue_autoscale_cluster_managers, queue_autoscale_cluster_workers, queue_autoscale_cluster_required_workers, queue_autoscale_cluster_worker_capacity, queue_autoscale_cluster_recommended_hosts |
| Per manager | queue_autoscale_cluster_host_workers, queue_autoscale_cluster_host_capacity, queue_autoscale_cluster_host_cpu_percent, queue_autoscale_cluster_host_memory_percent |
| Per workload | queue_autoscale_workload_workers_current, queue_autoscale_workload_workers_target, queue_autoscale_workload_pending_jobs, … |
clusterMetrics() is deliberately narrower than cluster(). New summary fields do not
automatically appear here — if you need full workload and lifecycle context, read cluster().
Cluster JSON Snapshot
The CLI exposes the same summary:
php artisan queue:autoscale:cluster --json
Without cluster mode enabled it prints a warning and exits 0 without JSON, and it does the same
when no summary has been published yet — so a collector should treat "no JSON on stdout" as "not
ready", not as an error.
Useful for local debugging, cron collectors, sidecar agents, and monitor packages that prefer to shell out rather than bind to the service.
Summary fields
cluster_idgenerated_at,generated_at_unix_msleader_id,leader_renewed_at,leader_renewed_at_unix_ms,leader_lease_ttl_seconds,leader_expires_atmanager_counttotal_workersrequired_workerstotal_worker_capacityutilization_percentscale_signalmanagersworkloadsscaling_decisions
Manager fields
Each managers[] entry:
manager_id,host,is_leaderlast_seen_at(unix milliseconds),last_seen_humantotal_workers,max_workers,available_worker_capacity,capacity_limitercpu_percent,cpu_cores,cpu_usable_cores,cpu_reserved_coresmemory_percent,memory_total_mb,memory_used_mb,memory_free_mbqueue_count,group_countpackage_versionqueue_workers,group_workers
The three cpu_*_cores fields are floats and can be fractional in cgroup-constrained environments.
Workload fields
Each workloads[] entry:
type(queueorgroup),connection,name,drivercurrent_workers,demand,target_workersworker_min,worker_max,sla_target_secondspending,oldest_job_age,oldest_job_age_statusthroughput_per_minute,active_workers,utilization_percentmember_queuesaction—'scale_up' | 'scale_down' | 'hold'
demand is the raw per-workload requirement before fair-share allocation; target_workers is what
the leader finally published, after both fair-share allocation and anti-flapping damping — so it can
sit above the allocation while a scale-down is being damped, and below the demand when the cluster is
capacity-bound. A persistent gap between demand and target_workers means the cluster is
capacity-bound.
Laravel Events
All events live in Cbox\LaravelQueueAutoscale\Events.
Workload / scaling
| Event | Constructor properties |
|---|---|
ScalingDecisionMade |
decision |
SlaBreachPredicted |
decision |
WorkersScaled |
connection, queue, from, to, action, reason |
SlaBreached |
connection, queue, oldestJobAge, slaTarget, pending, activeWorkers |
SlaRecovered |
connection, queue, currentJobAge, slaTarget, pending, activeWorkers |
ScalingDecisionMade and SlaBreachPredicted carry the ScalingDecision only — no metrics
object and no configuration — and they carry the decision after policies have modified it.
Failure fuse
| Event | Constructor properties |
|---|---|
FuseTripped |
connection, queue, failureRate, samples, failures, thresholdPercent, heldAtWorkers |
FuseProbing |
connection, queue, probeWorkers, cooldownSeconds |
FuseRecovered |
connection, queue, failureRate, samples |
Cluster / lifecycle
| Event | Constructor properties |
|---|---|
AutoscaleManagerStarted |
managerId, host, clusterEnabled, clusterId, intervalSeconds, startedAt, packageVersion |
AutoscaleManagerStopped |
managerId, host, clusterEnabled, clusterId, startedAt, stoppedAt, reason, workerCount, packageVersion |
ClusterLeaderChanged |
clusterId, previousLeaderId, currentLeaderId, observedByManagerId, changedAt |
ClusterManagerPresenceChanged |
clusterId, managerIds, addedManagerIds, removedManagerIds, leaderId, observedByManagerId, observedAt |
ClusterSummaryPublished |
clusterId, leaderId, summary, publishedAt |
ClusterScalingSignalUpdated |
see src/Events/ClusterScalingSignalUpdated.php |
There is no worker-level health event. src/Workers/the manager's inline liveness check.php exists but dispatches
nothing — worker deaths are visible only through the log channel and the next cycle's decision.
Registering listeners
use Cbox\LaravelQueueAutoscale\Events\ClusterLeaderChanged;
use Cbox\LaravelQueueAutoscale\Events\ScalingDecisionMade;
use Cbox\LaravelQueueAutoscale\Events\WorkersScaled;
use Illuminate\Support\Facades\Event;
Event::listen(ScalingDecisionMade::class, function (ScalingDecisionMade $event): void {
// $event->decision->targetWorkers, ->reason, ->capacity?->limitingFactor
});
Event::listen(WorkersScaled::class, function (WorkersScaled $event): void {
// $event->from, $event->to, $event->action
});
Event::listen(ClusterLeaderChanged::class, function (ClusterLeaderChanged $event): void {
// $event->previousLeaderId, $event->currentLeaderId
});
Events are dispatched from the manager's evaluation loop, which is a long-running CLI process. Listeners run synchronously inside the tick — keep them fast, or queue the work.
Telemetry (cboxdk/laravel-telemetry)
The integration lives in src/Telemetry/ and is wired by the service provider only when all of
these hold: Cbox\Telemetry\TelemetryManager exists, queue-autoscale.telemetry.enabled is true
(env QUEUE_AUTOSCALE_TELEMETRY_ENABLED, default true), and TelemetryManager is bound in the
container. Otherwise every part of it is a no-op, so the config can stay on by default.
'telemetry' => [
'enabled' => env('QUEUE_AUTOSCALE_TELEMETRY_ENABLED', true),
'cache_ttl' => env('QUEUE_AUTOSCALE_TELEMETRY_CACHE_TTL', 10),
'gauges' => [
'cluster' => true,
],
'events' => true,
],
cboxdk/laravel-telemetry is a suggest/dev dependency, not a requirement, and it needs Laravel 12
or newer.
Observable cluster gauges
QueueAutoscaleTelemetryProvider registers itself under the provider name cbox.queue-autoscale
and, when telemetry.gauges.cluster is true, exposes gauges evaluated at scrape time from the
cluster summary:
| Gauge | Source |
|---|---|
queue_autoscale.cluster.managers |
manager_count |
queue_autoscale.cluster.workers |
total_workers |
queue_autoscale.cluster.required_workers |
required_workers |
queue_autoscale.cluster.worker_capacity |
total_worker_capacity |
queue_autoscale.cluster.utilization |
utilization_percent |
queue_autoscale.cluster.recommended_hosts |
scale_signal.recommended_hosts |
queue_autoscale.cluster.host_workers |
per-manager total_workers |
queue_autoscale.cluster.host_capacity |
per-manager max_workers |
Queue depth, job durations and worker counts are deliberately not re-exported here — those are
owned by laravel-queue-metrics and telemetry's own queue instrumentation.
Pushed event metrics
TelemetryEventSubscriber is registered as a container singleton and subscribed to the event
dispatcher. It pushes gauges and counters from inside the manager daemon (push rather than
observable, because nothing else could evaluate a scrape callback for the daemon's in-memory state),
flushing at most once per second for per-tick decisions and immediately for rare signals.
It subscribes to ScalingDecisionMade, WorkersScaled, SlaBreached, SlaRecovered,
AutoscaleManagerStarted, AutoscaleManagerStopped, ClusterLeaderChanged, FuseTripped,
FuseProbing and FuseRecovered, and emits series including queue_autoscale.workers.target,
queue_autoscale.sla.target, queue_autoscale.sla.predicted_pickup,
queue_autoscale.capacity.max_workers, queue_autoscale.scaling.actions,
queue_autoscale.sla.breaches, queue_autoscale.cluster.leader_changes,
queue_autoscale.fuse.trips and queue_autoscale.fuse.state.
queue_autoscale.fuse.state encodes the fuse as a single series — 0 closed, 1 half-open
(probing), 2 open (holding at workers.min) — so a dashboard reads one series instead of
reconciling several booleans that can disagree mid-transition.
Set telemetry.events to false to keep the cluster gauges but stop the pushed event metrics.
Choosing between snapshot and events
Use cluster() / --json for current state: topology, leader, per-manager capacity and memory,
current workload targets, dashboard cards and tables.
Use events for history: transitions, alerting, audit logs, and asynchronous fan-out into Slack, notifications, analytics or a monitor package's database.
Most monitor packages want both — snapshot for now, events for what happened.
Design guidance for monitor packages
- Treat
cluster_id+generated_at_unix_msas the snapshot identity. - Treat lifecycle events as append-only history rows.
- Store
manager_id,cluster_id,hostand timestamps on every record. - Read summary arrays defensively; prefer
??defaults over hard assumptions. - Use
package_versionon each manager entry to detect mixed-version clusters during a rollout. - Handle
cluster() === []as a first-class state, not an error.
Not currently exposed
- A typed DTO contract for the summary — it is an array, and that is deliberate.
- Host load average.
- Remaining cooldown time per queue.
- Distinct heartbeat-stale / manager-expired events; presence changes are reported only through
ClusterManagerPresenceChanged. - Any worker process health event.
See Also
- Event Handling - Listener patterns for the event stream
- Cluster Scaling - How the summary is produced
- Monitoring - Operational monitoring
- Export Cluster Metrics - A worked exporter recipe