195 lines
6.1 KiB
PHP
195 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Console\Concerns\InteractsWithCacheLocks;
|
|
use App\Models\EventMediaAsset;
|
|
use App\Models\MediaStorageTarget;
|
|
use App\Services\Storage\StorageHealthService;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Contracts\Cache\Lock;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class MonitorStorageCommand extends Command
|
|
{
|
|
use InteractsWithCacheLocks;
|
|
|
|
protected $signature = 'storage:monitor {--force : Execute even if another run is still in progress}';
|
|
|
|
protected $description = 'Collect storage capacity statistics and cache them for dashboards & alerts.';
|
|
|
|
public function __construct(private readonly StorageHealthService $storageHealth)
|
|
{
|
|
parent::__construct();
|
|
}
|
|
|
|
public function handle(): int
|
|
{
|
|
$lockSeconds = (int) config('storage-monitor.monitor.lock_seconds', 300);
|
|
$lock = $this->acquireCommandLock('storage:monitor', $lockSeconds, (bool) $this->option('force'));
|
|
|
|
if ($lock === false) {
|
|
$this->warn('storage:monitor is already running on another worker.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
try {
|
|
$targets = MediaStorageTarget::active()->get();
|
|
|
|
if ($targets->isEmpty()) {
|
|
$this->info('No media storage targets available.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$assetStats = $this->buildAssetStatistics();
|
|
$thresholds = $this->capacityThresholds();
|
|
$alerts = [];
|
|
$snapshotTargets = [];
|
|
|
|
foreach ($targets as $target) {
|
|
$capacity = $this->storageHealth->getCapacity($target);
|
|
$assets = $assetStats[$target->id] ?? [
|
|
'total' => 0,
|
|
'bytes' => 0,
|
|
'by_status' => [],
|
|
];
|
|
|
|
$severity = $this->determineCapacitySeverity($capacity, $thresholds);
|
|
if ($severity !== 'ok') {
|
|
$alerts[] = [
|
|
'target' => $target->key,
|
|
'type' => 'capacity',
|
|
'severity' => $severity,
|
|
'percentage' => $capacity['percentage'] ?? null,
|
|
'status' => $capacity['status'] ?? null,
|
|
];
|
|
}
|
|
|
|
$failedCount = $assets['by_status']['failed']['count'] ?? 0;
|
|
if ($failedCount > 0) {
|
|
$alerts[] = [
|
|
'target' => $target->key,
|
|
'type' => 'failed_assets',
|
|
'severity' => 'warning',
|
|
'failed' => $failedCount,
|
|
];
|
|
}
|
|
|
|
$snapshotTargets[] = [
|
|
'id' => $target->id,
|
|
'key' => $target->key,
|
|
'name' => $target->name,
|
|
'is_hot' => (bool) $target->is_hot,
|
|
'capacity' => $capacity,
|
|
'assets' => $assets,
|
|
];
|
|
}
|
|
|
|
$snapshot = [
|
|
'generated_at' => now()->toIso8601String(),
|
|
'targets' => $snapshotTargets,
|
|
'alerts' => $alerts,
|
|
];
|
|
|
|
$ttlMinutes = max(1, (int) config('storage-monitor.monitor.cache_minutes', 15));
|
|
Cache::put('storage:monitor:last', $snapshot, now()->addMinutes($ttlMinutes));
|
|
|
|
Log::channel('storage-jobs')->info('Storage monitor snapshot generated', [
|
|
'targets' => count($snapshotTargets),
|
|
'alerts' => count($alerts),
|
|
]);
|
|
|
|
$this->info(sprintf(
|
|
'Storage monitor finished: %d targets, %d alerts.',
|
|
count($snapshotTargets),
|
|
count($alerts)
|
|
));
|
|
|
|
return self::SUCCESS;
|
|
} finally {
|
|
if ($lock instanceof Lock) {
|
|
$lock->release();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array>
|
|
*/
|
|
private function buildAssetStatistics(): array
|
|
{
|
|
return EventMediaAsset::query()
|
|
->selectRaw('media_storage_target_id, status, COUNT(*) as total_count, COALESCE(SUM(size_bytes), 0) as total_bytes')
|
|
->groupBy('media_storage_target_id', 'status')
|
|
->get()
|
|
->groupBy('media_storage_target_id')
|
|
->map(function ($rows) {
|
|
$byStatus = [];
|
|
$totalCount = 0;
|
|
$totalBytes = 0;
|
|
|
|
foreach ($rows as $row) {
|
|
$count = (int) ($row->total_count ?? 0);
|
|
$bytes = (int) ($row->total_bytes ?? 0);
|
|
|
|
$totalCount += $count;
|
|
$totalBytes += $bytes;
|
|
$byStatus[$row->status] = [
|
|
'count' => $count,
|
|
'bytes' => $bytes,
|
|
];
|
|
}
|
|
|
|
ksort($byStatus);
|
|
|
|
return [
|
|
'total' => $totalCount,
|
|
'bytes' => $totalBytes,
|
|
'by_status' => $byStatus,
|
|
];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function capacityThresholds(): array
|
|
{
|
|
$warning = (int) config('storage-monitor.capacity_thresholds.warning', 75);
|
|
$critical = (int) config('storage-monitor.capacity_thresholds.critical', 90);
|
|
|
|
if ($warning > $critical) {
|
|
[$warning, $critical] = [$critical, $warning];
|
|
}
|
|
|
|
return [
|
|
'warning' => $warning,
|
|
'critical' => $critical,
|
|
];
|
|
}
|
|
|
|
private function determineCapacitySeverity(array $capacity, array $thresholds): string
|
|
{
|
|
$status = $capacity['status'] ?? 'ok';
|
|
if ($status !== 'ok') {
|
|
return $status;
|
|
}
|
|
|
|
$percentage = $capacity['percentage'] ?? null;
|
|
if ($percentage === null) {
|
|
return 'unknown';
|
|
}
|
|
|
|
if ($percentage >= ($thresholds['critical'] ?? 95)) {
|
|
return 'critical';
|
|
}
|
|
|
|
if ($percentage >= ($thresholds['warning'] ?? 75)) {
|
|
return 'warning';
|
|
}
|
|
|
|
return 'ok';
|
|
}
|
|
}
|