fox/app/Services/SystemStatsService.php

129 lines
3.8 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Redis;
/**
* Sammelt System-Stats aus /proc, Disk, Ollama API, Redis Queue.
* Ein einziger Producer (Artisan-Command) ruft das auf und broadcastet
* per Reverb an alle Browser.
*/
class SystemStatsService
{
/**
* @return array{cpu:int, memory: array, disk: array, load: array, ollama: array, queue: int}
*/
public function collect(): array
{
return [
'cpu' => $this->cpuUsage(),
'memory' => $this->memoryUsage(),
'disk' => $this->diskUsage(),
'load' => $this->loadAverage(),
'ollama' => $this->ollamaStatus(),
'openai' => $this->openaiStatus(),
'queue' => $this->queueLength(),
];
}
protected function cpuUsage(): int
{
$sample = function (): array {
$line = explode("\n", (string) @file_get_contents('/proc/stat'))[0] ?? '';
$parts = preg_split('/\s+/', trim($line));
array_shift($parts);
$parts = array_map('intval', $parts);
return [
'idle' => ($parts[3] ?? 0) + ($parts[4] ?? 0),
'total' => array_sum($parts),
];
};
$a = $sample();
usleep(100_000);
$b = $sample();
$totalDiff = $b['total'] - $a['total'];
$idleDiff = $b['idle'] - $a['idle'];
return $totalDiff > 0 ? (int) round((1 - $idleDiff / $totalDiff) * 100) : 0;
}
protected function memoryUsage(): array
{
$info = (string) @file_get_contents('/proc/meminfo');
preg_match('/MemTotal:\s+(\d+)/', $info, $totalMatch);
preg_match('/MemAvailable:\s+(\d+)/', $info, $availMatch);
$total = (int) ($totalMatch[1] ?? 0);
$available = (int) ($availMatch[1] ?? 0);
$used = max(0, $total - $available);
return [
'percent' => $total > 0 ? (int) round(($used / $total) * 100) : 0,
'used_mb' => (int) round($used / 1024),
'total_mb' => (int) round($total / 1024),
];
}
protected function diskUsage(): array
{
$total = (int) @disk_total_space('/var/www');
$free = (int) @disk_free_space('/var/www');
$used = max(0, $total - $free);
return [
'percent' => $total > 0 ? (int) round(($used / $total) * 100) : 0,
'used_gb' => round($used / 1024 / 1024 / 1024, 1),
'total_gb' => round($total / 1024 / 1024 / 1024, 1),
];
}
protected function loadAverage(): array
{
$line = (string) @file_get_contents('/proc/loadavg');
$parts = preg_split('/\s+/', trim($line));
return [
'1m' => (float) ($parts[0] ?? 0),
'5m' => (float) ($parts[1] ?? 0),
'15m' => (float) ($parts[2] ?? 0),
];
}
protected function ollamaStatus(): array
{
try {
$r = Http::timeout(2)->get(rtrim((string) config('services.ollama.url'), '/').'/api/tags');
if ($r->successful()) {
return ['online' => true, 'models' => count($r->json('models', []))];
}
} catch (\Throwable) {
// ignore
}
return ['online' => false, 'models' => 0];
}
/**
* OpenAI ist "online" wenn ein Key gesetzt ist - kein Live-Ping (kostet Geld).
* Der echte Health-Check passiert beim ersten realen Request, im LLM-Fallback.
*/
protected function openaiStatus(): array
{
return ['online' => ! empty(config('services.openai.key'))];
}
protected function queueLength(): int
{
try {
return (int) Redis::connection()->llen(config('cache.prefix', '').'queues:default');
} catch (\Throwable) {
return 0;
}
}
}