laravel-workkit/src/Commands/Queue/QueueHealthCommand.php

104 lines
3.9 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace Blax\Workkit\Commands\Queue;
use Blax\Workkit\Services\QueueHealthService;
use Illuminate\Console\Command;
/**
* Report queue health and exit non-zero when a threshold is breached, so it
* drops straight into cron / a monitoring probe:
*
* * * * * * php artisan workkit:queue:health --json || alert
*
* For the database queue driver it shows per-queue depth, due/delayed/reserved
* counts and the oldest due job's age, and flags a STALLED queue (due work past
* the age limit with nothing reserved the worker is probably down). Failed
* jobs (total + last 24h) are reported on any driver.
*/
class QueueHealthCommand extends Command
{
protected $signature = 'workkit:queue:health
{--connection= : Queue connection (defaults to config(queue.default))}
{--max-age= : Stall threshold in minutes (default: workkit.queue.max_age_minutes)}
{--max-depth= : Per-queue backlog threshold (default: workkit.queue.max_depth)}
{--max-failed= : Max failed jobs in the window (default: workkit.queue.max_failed)}
{--window=24 : Failed-jobs lookback window in hours}
{--json : Output machine-readable JSON}';
protected $description = 'Report queue depth/age/failed-jobs health; exit non-zero on any threshold breach.';
public function handle(): int
{
$connection = $this->option('connection') ?: config('queue.default');
$window = max(1, (int) $this->option('window'));
$snapshot = QueueHealthService::inspect($connection, $window);
$thresholds = QueueHealthService::thresholds();
foreach (['max-age' => 'max_age_minutes', 'max-depth' => 'max_depth', 'max-failed' => 'max_failed'] as $opt => $key) {
$val = $this->option($opt);
if ($val !== null) {
$thresholds[$key] = (int) $val;
}
}
$result = QueueHealthService::evaluate($snapshot, $thresholds);
if ($this->option('json')) {
$this->line((string) json_encode(
['snapshot' => $snapshot, 'thresholds' => $thresholds] + $result,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
));
return $result['ok'] ? self::SUCCESS : self::FAILURE;
}
$this->line(sprintf('Queue connection: %s (driver: %s)', $snapshot['connection'], $snapshot['driver'] ?? 'unknown'));
if ($snapshot['supported']) {
if ($snapshot['queues'] === []) {
$this->line(' (no jobs queued)');
} else {
$rows = [];
foreach ($snapshot['queues'] as $name => $q) {
$rows[] = [
$name,
number_format($q['pending']),
number_format($q['due']),
number_format($q['delayed']),
number_format($q['reserved']),
$q['oldest_due_age'] === null ? '—' : intdiv($q['oldest_due_age'], 60).'m',
];
}
$this->table(['Queue', 'Pending', 'Due', 'Delayed', 'Reserved', 'Oldest due'], $rows);
}
} else {
$this->warn(sprintf('Per-queue introspection unsupported for the "%s" driver — showing failed jobs only.', $snapshot['driver'] ?? 'unknown'));
}
$this->line(sprintf(
'Failed jobs: %s total, %s in last %dh',
$snapshot['failed_total'] ?? 'n/a',
$snapshot['failed_recent'] ?? 'n/a',
$snapshot['failed_window_hours'],
));
if ($result['ok']) {
$this->info('Queue health: OK');
return self::SUCCESS;
}
$this->newLine();
$this->error('Queue health: '.count($result['breaches']).' issue(s)');
foreach ($result['breaches'] as $b) {
$this->line(' • '.$b['message']);
}
return self::FAILURE;
}
}