104 lines
3.9 KiB
PHP
104 lines
3.9 KiB
PHP
<?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;
|
|
}
|
|
}
|