" \ * https://dc.example.com/api/watchdog/v1/evaluate > /dev/null * * Authentifizierung fuer Agenten: sowohl die zentralen Master-/Sub-Tokens * (dc_tokens, Scope watchdog:ping) als auch die aelteren Agent-Tokens aus * watchdog_agent_tokens werden akzeptiert. */ declare(strict_types=1); require_once __DIR__ . '/../../../../src/bootstrap.php'; use Deploymentcenter\Core\ApiAuth; use Deploymentcenter\Core\Db; use Deploymentcenter\Core\Http; use Deploymentcenter\Modules\Watchdog\Evaluator; use Deploymentcenter\Modules\Watchdog\EventLog; use Deploymentcenter\Modules\Watchdog\MetricStore; use Deploymentcenter\Modules\Watchdog\MonitorRepo; use Deploymentcenter\Modules\Watchdog\TokenManager as LegacyTokenManager; Http::beginJson(['GET', 'POST', 'OPTIONS'], true); $db = Db::init(); $monitorRepo = new MonitorRepo($db); $eventLog = new EventLog($db); $action = resolveWatchdogAction(); switch ($action) { case 'ping': requirePost(); $source = Http::str('source'); if ($source === null) { Http::fail(400, 'missing_source', 'Das Feld "source" wird benoetigt.'); } authorizeSource($db, $source); $instance = Http::str('instance') ?? 'default'; $metrics = Http::input('metrics'); $monitor = $monitorRepo->upsertHeartbeat( $source, $instance, Http::str('type') ?? 'heartbeat', Http::int('interval', 0) ?: Http::int('expected_interval_sec', 60), $metrics, strtolower(Http::str('status') ?? 'ok'), Http::str('message') ?? Http::str('reason'), Http::str('group') ?? Http::str('group_key'), Http::str('os'), // Gesundheitszustand, den die Anwendung selbst ermittelt hat. Http::input('checks'), // Welche Version laeuft hier? Optional - bestehende Agenten // schicken das Feld nicht und laufen unveraendert weiter. Http::str('version') ?? Http::str('app_version') ); // Numerische Werte in den Verlauf uebernehmen, damit sich Trends // erkennen lassen statt nur der letzte Moment. $recordedMetrics = (new MetricStore($db))->record($source, $instance, $metrics); // Zustandswechsel im Ereignisprotokoll festhalten. if (!empty($monitor['_state_changed'])) { $previous = (string)$monitor['_previous_state']; $current = (string)$monitor['state']; $eventLog->logEvent( $source, (string)$monitor['instance'], $current === 'up' ? 'recovered' : ($current === 'warning' ? 'warning_raised' : 'hard_error'), $previous, $current, $current === 'up' ? 'info' : ($current === 'warning' ? 'warning' : 'alarm'), 'Zustandswechsel durch Heartbeat.' ); } Http::ok([ 'message' => 'Heartbeat empfangen.', 'monitor' => [ 'source' => $monitor['source'], 'instance' => $monitor['instance'], 'state' => $monitor['state'], 'last_status' => $monitor['last_status'], 'last_seen_utc' => $monitor['last_seen_utc'], 'app_version' => $monitor['app_version'] ?? null, 'state_changed' => (bool)($monitor['_state_changed'] ?? false), 'failing_checks' => $monitor['_failing_checks'] ?? [], ], 'metrics_recorded' => $recordedMetrics, ]); case 'event': requirePost(); $source = Http::str('source'); if ($source === null) { Http::fail(400, 'missing_source', 'Das Feld "source" wird benoetigt.'); } authorizeSource($db, $source); $meta = Http::input('meta'); $eventId = $eventLog->logEvent( $source, Http::str('instance') ?? 'default', Http::str('kind') ?? 'started', Http::str('from_state'), Http::str('to_state'), Http::str('severity') ?? 'info', Http::str('message'), is_array($meta) ? $meta : null ); Http::ok(['event_id' => $eventId], 201); case 'status': ApiAuth::requireScope($db, 'watchdog:read'); $monitors = $monitorRepo->getAllMonitors(); Http::ok(['count' => count($monitors), 'monitors' => $monitors]); case 'events': ApiAuth::requireScope($db, 'watchdog:read'); $events = $eventLog->getRecentEvents( Http::int('limit', 50), Http::str('source'), Http::str('instance'), Http::str('severity') ); Http::ok(['count' => count($events), 'events' => $events]); case 'metrics': ApiAuth::requireScope($db, 'watchdog:read'); $source = Http::str('source'); if ($source === null) { Http::fail(400, 'missing_source', 'Der Parameter "source" wird benoetigt.'); } $store = new MetricStore($db); $instance = Http::str('instance') ?? 'default'; $metricKey = Http::str('metric'); if ($metricKey === null) { Http::ok([ 'source' => $source, 'metrics' => $store->keysFor($source, $instance), 'hint' => 'Mit &metric= den Verlauf abrufen.', ]); } Http::ok([ 'source' => $source, 'metric' => $metricKey, 'hours' => Http::int('hours', 24), 'history' => $store->history($source, $metricKey, Http::int('hours', 24), $instance, Http::int('bucket', 15)), 'deviation' => $store->deviation($source, $metricKey, $instance), ]); case 'evaluate': // Bewusst nur fuer Shared Key oder eine angemeldete Sitzung - // ein Agenten-Token soll den Zustand aller Monitore nicht umschreiben. ApiAuth::requireScope($db, 'watchdog:evaluate'); $reportRes = null; if (Http::str('force_report') === '1' || Http::str('report') === '1') { $reportRes = \Deploymentcenter\Modules\Notify\RocketChatNotifier::sendStatusReport($db, true); } $result = Evaluator::run($db); Http::ok($result + [ 'rocketchat_report' => $reportRes, 'message' => sprintf( '%d Monitor(e) geprueft, %d Zustandswechsel.', $result['checked'], $result['changed'] ) ]); default: Http::fail(404, 'unknown_action', 'Endpunkt nicht gefunden.', null, [ 'available' => ['ping', 'event', 'status', 'events', 'metrics', 'evaluate'], ]); } // ====================================================================== function requirePost(): void { if (Http::method() !== 'POST') { Http::fail(405, 'method_not_allowed', 'Diese Aktion erwartet POST.'); } } /** * Prueft die Berechtigung, fuer eine bestimmte Source zu melden. * * Akzeptiert zentrale Tokens (dc_tokens, Scope watchdog:ping) und die * aelteren, an eine Source gebundenen Agent-Tokens. */ function authorizeSource(PDO $db, string $source): void { // Zentrale Token-Hierarchie, Shared Key oder Session if (ApiAuth::resolve($db, 'watchdog:ping', null, true) !== null) { return; } // Alt-Tokens aus watchdog_agent_tokens $presented = Http::bearerToken(); if ($presented !== null) { $legacy = new LegacyTokenManager($db); if ($legacy->validateToken($presented, $source)) { return; } } Http::fail( 401, 'unauthorized', sprintf('Kein gueltiges Token fuer die Source "%s".', $source), null, ['required_scope' => 'watchdog:ping'] ); } function resolveWatchdogAction(): string { $explicit = Http::str('action'); if ($explicit !== null) { return strtolower($explicit); } $segments = array_values(array_filter( explode('/', trim(Http::path(), '/')), static fn(string $s): bool => $s !== '' )); $last = strtolower((string)end($segments)); return match ($last) { 'ping', 'heartbeat' => 'ping', 'event' => 'event', 'events' => 'events', 'status' => 'status', 'evaluate' => 'evaluate', 'metrics' => 'metrics', default => 'status', }; }