Files
portal-api/app/Http/Controllers/Api/QueueMonitorController.php
baodev0andClaude Sonnet 5.5 62596d9063 feat(vm): hoàn tiền khi cấp VM lỗi, theo dõi job queue, uuid dịch vụ cho ticket, chuẩn hoá RAM addon
- 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>
2026-10-30 11:54:05 +07:00

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();
}
}