feat(errors, watchdog): Fehler-Stream mit Ignore-Regeln, Metrik-Verlauf, Abhängigkeits-Alarme
Fehler-Schnittstelle - Neuer schlanker Eingang POST /api/errors/v1/report für den globalen Exception-Handler einer Anwendung. Titel und Dringlichkeit leitet der Server ab; gespeichert wird in derselben Tabelle wie der Bugtracker. Ein zweiter Speicher wäre nur ein zweiter Ort, an dem man suchen müsste. - error_level (fatal/error/warning) trennt die technische Art des Ereignisses von der geschäftlichen Dringlichkeit. Ein Duplicate-Entry ist technisch ein error, geschäftlich belanglos — beides zu vermischen war der Grund, warum solche Meldungen als Bug im Dashboard landeten. Ignore-Regeln gegen bekanntes Rauschen - bugtracker_ignore_rules mit contains/regex/exception_class, Pflichtfeld für die Begründung und optionaler Alarmschwelle. - Ein Treffer bedeutet nicht "wegwerfen": Der Fehler wird weiterhin erfasst und hochgezählt, bleibt aber aus der Übersicht heraus und löst keine Benachrichtigung aus. Der Zähler ist der eigentliche Zweck — dass ein bekannter Fehler auftritt, ist normal; dass er plötzlich hundertmal so oft auftritt, ist ein Signal. Dafür das rollende Stundenfenster und error.rate_exceeded. - Neue Regeln lassen sich rückwirkend auf bestehende Einträge anwenden. Gruppierung überarbeitet - Der Schlüssel nahm bisher 300 Zeichen Stacktrace auf. Derselbe Fehler zersplitterte dadurch, sobald ein Aufrufer den Stack einmal mitschickte und einmal nicht. Jetzt zählt der Ursprungsort: bevorzugt die Dateiangabe, sonst der erste Rahmen des Stacktrace. - Die Normalisierung ersetzte nur Zahlen ab vier Stellen, wodurch 'AA-1' und 'BB-2' getrennt blieben. Werte in Anführungszeichen, die Ziffern enthalten, gelten jetzt als veränderlich — der Schlüsselname bleibt erhalten, sodass verschiedene Unique-Keys unterscheidbar sind. Mit 9 Testfällen belegt. Metrik-Verlauf - watchdog_metrics speichert numerische Heartbeat-Werte mit Zeitstempel. Zuvor wurde metrics_json bei jedem Heartbeat überschrieben; damit ließ sich "die Platte läuft seit drei Tagen voll" nicht erkennen, nur "sie ist voll". - GET /api/watchdog/v1/metrics liefert den verdichteten Verlauf und die Abweichung vom eigenen Sieben-Tage-Durchschnitt. Dieser relative Ansatz braucht keine projektspezifischen Schwellwerte. - Aufbewahrung 14 Tage, Bereinigung stündlich durch den Evaluator. Health-Checks per Push statt Abruf - Der Heartbeat nimmt ein checks-Objekt entgegen, das die Anwendung selbst ermittelt. Das Deploymentcenter interpretiert die Namen nicht, es liest nur ok und message — was "gesund" bedeutet, entscheidet jede Anwendung selbst. Schlägt eine Prüfung fehl, wird ein als ok gemeldeter Heartbeat auf warning herabgestuft. - Bewusst ausgehend: auf den Zielmaschinen müssen keine Ports geöffnet werden. Abhängigkeitsbewusste Alarmierung - Fällt ein Monitor aus, dessen Parent selbst unten ist, wird der Alarm unterdrückt. Der Zustand bleibt sichtbar. Vorher erzeugte ein ausgefallener Hypervisor mit zwölf VMs dreizehn Meldungen für ein Problem. - Mehrere Ebenen und fehlerhafte Hierarchien (Zyklen, gelöschte Parents) sind abgesichert; mit 10 Testfällen belegt. WebUI - Neue Ansicht "Fehler-Stream" mit Filtern nach Projekt, Fehlerklasse, Umgebung, Zeitraum und Sichtbarkeit sowie Volltextsuche und Pagination. Stummgeschaltete Einträge sind standardmäßig ausgeblendet. - Verwaltung der Ignore-Regeln inklusive Trefferzähler. - Die Detailansicht zeigt Fehlerklasse, Stummschaltungsgrund und die Häufung im laufenden Stundenfenster. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a74c6fd990
commit
60e34b29f6
@@ -32,6 +32,7 @@ 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;
|
||||
|
||||
@@ -56,18 +57,27 @@ switch ($action) {
|
||||
|
||||
authorizeSource($db, $source);
|
||||
|
||||
$instance = Http::str('instance') ?? 'default';
|
||||
$metrics = Http::input('metrics');
|
||||
|
||||
$monitor = $monitorRepo->upsertHeartbeat(
|
||||
$source,
|
||||
Http::str('instance') ?? 'default',
|
||||
$instance,
|
||||
Http::str('type') ?? 'heartbeat',
|
||||
Http::int('interval', 0) ?: Http::int('expected_interval_sec', 60),
|
||||
Http::input('metrics'),
|
||||
$metrics,
|
||||
strtolower(Http::str('status') ?? 'ok'),
|
||||
Http::str('message') ?? Http::str('reason'),
|
||||
Http::str('group') ?? Http::str('group_key'),
|
||||
Http::str('os')
|
||||
Http::str('os'),
|
||||
// Gesundheitszustand, den die Anwendung selbst ermittelt hat.
|
||||
Http::input('checks')
|
||||
);
|
||||
|
||||
// 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'];
|
||||
@@ -87,13 +97,15 @@ switch ($action) {
|
||||
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'],
|
||||
'state_changed' => (bool)($monitor['_state_changed'] ?? false),
|
||||
'source' => $monitor['source'],
|
||||
'instance' => $monitor['instance'],
|
||||
'state' => $monitor['state'],
|
||||
'last_status' => $monitor['last_status'],
|
||||
'last_seen_utc' => $monitor['last_seen_utc'],
|
||||
'state_changed' => (bool)($monitor['_state_changed'] ?? false),
|
||||
'failing_checks' => $monitor['_failing_checks'] ?? [],
|
||||
],
|
||||
'metrics_recorded' => $recordedMetrics,
|
||||
]);
|
||||
|
||||
case 'event':
|
||||
@@ -135,6 +147,34 @@ switch ($action) {
|
||||
);
|
||||
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=<name> 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.
|
||||
@@ -149,7 +189,7 @@ switch ($action) {
|
||||
|
||||
default:
|
||||
Http::fail(404, 'unknown_action', 'Endpunkt nicht gefunden.', null, [
|
||||
'available' => ['ping', 'event', 'status', 'events', 'evaluate'],
|
||||
'available' => ['ping', 'event', 'status', 'events', 'metrics', 'evaluate'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -213,6 +253,7 @@ function resolveWatchdogAction(): string
|
||||
'events' => 'events',
|
||||
'status' => 'status',
|
||||
'evaluate' => 'evaluate',
|
||||
'metrics' => 'metrics',
|
||||
default => 'status',
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user