- VmProvisionException/VmProvisionRefund xử lý lỗi cấp phát VM - QueueJobInspector + QueueMonitorController xem chi tiết job - Thêm service uuid cho ticket, thời lượng cho activity_logs - Migration chuẩn hoá RAM addon sang MB, trạng thái deleting cho snapshot Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
173 lines
5.9 KiB
PHP
173 lines
5.9 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Support\QueueJobInspector;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Laravel\Horizon\Contracts\JobRepository;
|
|
use Laravel\Horizon\Contracts\MetricsRepository;
|
|
use Laravel\Horizon\Contracts\WorkloadRepository;
|
|
use Laravel\Horizon\Jobs\RetryFailedJob;
|
|
|
|
class QueueMonitorController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly JobRepository $jobs,
|
|
private readonly MetricsRepository $metrics,
|
|
private readonly WorkloadRepository $workload,
|
|
) {}
|
|
|
|
/**
|
|
* GET /api/queue/jobs/recent
|
|
* Recent processed jobs tracked by Horizon.
|
|
*/
|
|
public function recent(): JsonResponse
|
|
{
|
|
$jobs = $this->jobs->getRecent();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'total' => $this->jobs->countRecent(),
|
|
'data' => $this->formatJobs($jobs, 'completed'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* GET /api/queue/jobs/pending
|
|
* Jobs currently waiting in the queue (per-queue workload + Horizon pending list).
|
|
*/
|
|
public function pending(): JsonResponse
|
|
{
|
|
$jobs = $this->jobs->getPending();
|
|
$queues = $this->workload->get();
|
|
|
|
$workload = collect($queues)->map(fn (array $q) => [
|
|
'queue' => $q['name'],
|
|
'length' => $q['length'],
|
|
'wait' => $q['wait'],
|
|
'processes' => $q['processes'],
|
|
])->values();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'total' => $this->jobs->countPending(),
|
|
'workload' => $workload,
|
|
'data' => $this->formatJobs($jobs, 'pending'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* GET /api/queue/jobs/failed
|
|
* Failed jobs tracked by Horizon.
|
|
*/
|
|
public function failed(): JsonResponse
|
|
{
|
|
$jobs = $this->jobs->getFailed();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'total' => $this->jobs->countFailed(),
|
|
'data' => $this->formatJobs($jobs, 'failed'),
|
|
]);
|
|
}
|
|
|
|
public function show(string $id): JsonResponse
|
|
{
|
|
$job = $this->jobs->getJobs([$id])->first();
|
|
abort_if(!$job, 404, "Không tìm thấy job #{$id}");
|
|
|
|
return response()->json(['success' => true, 'data' => QueueJobInspector::describe($job)]);
|
|
}
|
|
|
|
/**
|
|
* POST /api/queue/jobs/failed/{id}/retry
|
|
* Re-queue a failed job by its Horizon job ID.
|
|
*/
|
|
public function retry(string $id): JsonResponse
|
|
{
|
|
abort_if(!$this->jobs->findFailed($id), 404, "Can't find failed job #{$id}");
|
|
|
|
dispatch(new RetryFailedJob($id));
|
|
|
|
return response()->json(['success' => true, 'message' => 'Job đã được đưa lại vào hàng đợi']);
|
|
}
|
|
|
|
/**
|
|
* GET /api/queue/jobs/metrics
|
|
* Queue throughput, runtime, and per-queue/per-job breakdown.
|
|
*/
|
|
public function metrics(): JsonResponse
|
|
{
|
|
$measuredQueues = $this->metrics->measuredQueues();
|
|
$measuredJobs = $this->metrics->measuredJobs();
|
|
|
|
$queueMetrics = collect($measuredQueues)->map(fn (string $queue) => [
|
|
'queue' => $queue,
|
|
'throughput' => $this->metrics->throughputForQueue($queue),
|
|
'runtime_ms' => round($this->metrics->runtimeForQueue($queue), 2),
|
|
])->values();
|
|
|
|
$jobMetrics = collect($measuredJobs)->map(fn (string $job) => [
|
|
'job' => $job,
|
|
'throughput' => $this->metrics->throughputForJob($job),
|
|
'runtime_ms' => round($this->metrics->runtimeForJob($job), 2),
|
|
])->values();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'throughput' => $this->metrics->throughput(),
|
|
'jobs_processed_per_minute' => $this->metrics->jobsProcessedPerMinute(),
|
|
'counts' => [
|
|
'recent' => $this->jobs->countRecent(),
|
|
'pending' => $this->jobs->countPending(),
|
|
'failed' => $this->jobs->countFailed(),
|
|
'completed' => $this->jobs->countCompleted(),
|
|
'recently_failed' => $this->jobs->countRecentlyFailed(),
|
|
],
|
|
'queues' => $queueMetrics,
|
|
'jobs' => $jobMetrics,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Normalize a collection of Horizon job objects into a consistent array shape.
|
|
*
|
|
* @param \Illuminate\Support\Collection $jobs
|
|
* @param string $defaultStatus
|
|
* @return \Illuminate\Support\Collection
|
|
*/
|
|
private function formatJobs(\Illuminate\Support\Collection $jobs, string $defaultStatus): \Illuminate\Support\Collection
|
|
{
|
|
return $jobs->map(function (object $job) use ($defaultStatus) {
|
|
$payload = isset($job->payload) ? json_decode($job->payload, true) : [];
|
|
|
|
return [
|
|
'id' => $job->id ?? null,
|
|
'name' => $payload['displayName'] ?? $job->name ?? null,
|
|
'queue' => $job->queue ?? null,
|
|
'status' => $job->status ?? $defaultStatus,
|
|
'attempts' => $job->attempts ?? 0,
|
|
'started_at' => $this->formatTimestamp($job->reserved_at ?? null),
|
|
'completed_at' => $this->formatTimestamp($job->completed_at ?? null),
|
|
'failed_at' => $this->formatTimestamp($job->failed_at ?? null),
|
|
'exception' => $job->exception ?? null,
|
|
];
|
|
})->values();
|
|
}
|
|
|
|
/**
|
|
* Convert a Unix timestamp to ISO-8601 string, or return null.
|
|
*/
|
|
private function formatTimestamp(int|float|string|null $ts): ?string
|
|
{
|
|
if ($ts === null || $ts === 0) {
|
|
return null;
|
|
}
|
|
|
|
return \Carbon\Carbon::createFromTimestamp((int) $ts)->toIso8601String();
|
|
}
|
|
}
|