109 lines
4.3 KiB
PHP
109 lines
4.3 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Events\MetricsTicked;
|
|
use App\Models\Server;
|
|
use App\Services\AlertEvaluator;
|
|
use App\Services\FleetService;
|
|
use App\Support\Ssh\CredentialVault;
|
|
use App\Support\Ssh\SshClient;
|
|
use Illuminate\Console\Command;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Replaces the dev mock emitter: polls REAL CPU/MEM/disk over SSH for every
|
|
* server that has a stored credential, persists the values onto the Server row,
|
|
* and broadcasts MetricsTicked so the dashboard chart moves with real data.
|
|
*
|
|
* One long-lived SSH connection is kept per server and reused across ticks (a
|
|
* single login, not one per interval — avoids fail2ban/auth-log churn). A dead
|
|
* connection is dropped and re-established on the next tick; one server failing
|
|
* never kills the loop.
|
|
*/
|
|
class PollMetrics extends Command
|
|
{
|
|
protected $signature = 'clusev:poll-metrics
|
|
{--interval=15 : Seconds between polls}
|
|
{--once : Poll a single round and exit}';
|
|
|
|
protected $description = 'Poll real metrics over SSH for credentialed servers, persist + broadcast';
|
|
|
|
public function handle(FleetService $fleet, CredentialVault $vault, AlertEvaluator $alerts): int
|
|
{
|
|
$interval = max(2, (int) $this->option('interval'));
|
|
|
|
/** @var array<int, SshClient> $clients */
|
|
$clients = [];
|
|
|
|
do {
|
|
// Only servers with a PRESENT, non-disabled credential. A credential disabled
|
|
// between ticks drops the server here, so its cached connection is no longer polled
|
|
// (the vault refuses a fresh connect, but a reused long-lived session would not
|
|
// re-check disabled_at). Prune any cached client whose server just left the set so
|
|
// the revoked server's open session is closed promptly, not left idle until exit.
|
|
$servers = Server::withActiveCredential()->get();
|
|
$activeIds = $servers->pluck('id')->all();
|
|
foreach (array_keys($clients) as $id) {
|
|
if (! in_array($id, $activeIds, true)) {
|
|
$clients[$id]->disconnect();
|
|
unset($clients[$id]);
|
|
}
|
|
}
|
|
|
|
if ($servers->isEmpty()) {
|
|
$this->warn('Keine Server mit aktivem Credential — nichts zu pollen.');
|
|
}
|
|
|
|
foreach ($servers as $server) {
|
|
try {
|
|
$clients[$server->id] ??= (new SshClient($vault, timeout: 12))->connect($server);
|
|
|
|
$m = $fleet->readMetrics($clients[$server->id]);
|
|
$fleet->applyMetrics($server, $m); // forceFills the fresh status onto $server
|
|
broadcast(new MetricsTicked($m['cpu'], $m['mem'], $server->name));
|
|
$this->evaluateAlerts($alerts, $server, $m, $server->status);
|
|
|
|
$this->line("ok {$server->name} cpu={$m['cpu']} mem={$m['mem']} disk={$m['disk']} load={$m['load']}");
|
|
} catch (Throwable $e) {
|
|
// drop the (possibly dead) connection so the next tick reconnects
|
|
if (isset($clients[$server->id])) {
|
|
$clients[$server->id]->disconnect();
|
|
unset($clients[$server->id]);
|
|
}
|
|
$server->forceFill(['status' => 'offline'])->save();
|
|
$this->evaluateAlerts($alerts, $server, [], 'offline'); // an offline rule fires here
|
|
$this->warn("offline {$server->name}: ".$e->getMessage());
|
|
}
|
|
}
|
|
|
|
if ($this->option('once')) {
|
|
break;
|
|
}
|
|
|
|
sleep($interval);
|
|
} while (true);
|
|
|
|
foreach ($clients as $client) {
|
|
$client->disconnect();
|
|
}
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Run the alert rules for a freshly-polled server. Best-effort: an alerting/notification error
|
|
* must NEVER kill the poll loop, so any throwable is caught and logged as a warning line.
|
|
*
|
|
* @param array<string, int|float|string> $metrics
|
|
*/
|
|
private function evaluateAlerts(AlertEvaluator $alerts, Server $server, array $metrics, string $status): void
|
|
{
|
|
try {
|
|
$alerts->evaluate($server, $metrics, $status);
|
|
} catch (Throwable $e) {
|
|
$this->warn("alert eval failed {$server->name}: ".$e->getMessage());
|
|
}
|
|
}
|
|
}
|