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
@@ -72,6 +72,10 @@ final class MonitorRepo
|
||||
/**
|
||||
* Nimmt einen Heartbeat entgegen und legt den Monitor bei Bedarf an.
|
||||
*/
|
||||
/**
|
||||
* @param mixed $metrics
|
||||
* @param mixed $checks Gesundheitszustand, den die Anwendung selbst ermittelt hat.
|
||||
*/
|
||||
public function upsertHeartbeat(
|
||||
string $source,
|
||||
string $instance,
|
||||
@@ -81,18 +85,35 @@ final class MonitorRepo
|
||||
string $status,
|
||||
?string $message,
|
||||
?string $groupKey = null,
|
||||
?string $os = null
|
||||
?string $os = null,
|
||||
$checks = null
|
||||
): array {
|
||||
$metricsJson = (is_array($metrics) || is_object($metrics))
|
||||
? json_encode($metrics, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
|
||||
: null;
|
||||
|
||||
// Die Anwendung meldet ihren Gesundheitszustand selbst mit. Das
|
||||
// Deploymentcenter interpretiert die Namen der Pruefungen nicht - es
|
||||
// liest nur ok und message. Damit muss auf der Zielmaschine kein Port
|
||||
// geoeffnet werden, und jede Anwendung entscheidet selbst, was bei ihr
|
||||
// "gesund" bedeutet.
|
||||
$failing = self::failingChecks($checks);
|
||||
$healthJson = (is_array($checks) || is_object($checks))
|
||||
? json_encode($checks, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
|
||||
: null;
|
||||
|
||||
$state = match ($status) {
|
||||
'ok' => 'up',
|
||||
'warning' => 'warning',
|
||||
default => 'down',
|
||||
};
|
||||
|
||||
// Eine fehlgeschlagene Pruefung stuft einen als "ok" gemeldeten
|
||||
// Heartbeat herab: der Prozess laeuft, tut aber nicht, was er soll.
|
||||
if ($failing !== [] && $state === 'up') {
|
||||
$state = 'warning';
|
||||
}
|
||||
|
||||
$previous = $this->getMonitor($source, $instance);
|
||||
$previousState = $previous !== null ? (string)$previous['state'] : null;
|
||||
|
||||
@@ -102,12 +123,12 @@ final class MonitorRepo
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO watchdog_monitors (
|
||||
source, instance, type, state, last_state_change_utc, expected_interval_sec,
|
||||
last_seen_utc, last_status, last_message, metrics_json, group_key, os,
|
||||
created_utc, updated_utc
|
||||
last_seen_utc, last_status, last_message, metrics_json, health_json,
|
||||
failing_checks, group_key, os, created_utc, updated_utc
|
||||
) VALUES (
|
||||
:source, :instance, :type, :state, UTC_TIMESTAMP(), :interval,
|
||||
UTC_TIMESTAMP(), :last_status, :message, :metrics, :group_key, :os,
|
||||
UTC_TIMESTAMP(), UTC_TIMESTAMP()
|
||||
UTC_TIMESTAMP(), :last_status, :message, :metrics, :health,
|
||||
:failing, :group_key, :os, UTC_TIMESTAMP(), UTC_TIMESTAMP()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
-- Reihenfolge ist relevant: MySQL wertet die Zuweisungen von
|
||||
@@ -121,6 +142,8 @@ final class MonitorRepo
|
||||
last_status = VALUES(last_status),
|
||||
last_message = VALUES(last_message),
|
||||
metrics_json = VALUES(metrics_json),
|
||||
health_json = COALESCE(VALUES(health_json), health_json),
|
||||
failing_checks = VALUES(failing_checks),
|
||||
group_key = COALESCE(VALUES(group_key), group_key),
|
||||
os = COALESCE(VALUES(os), os),
|
||||
updated_utc = VALUES(updated_utc)
|
||||
@@ -135,6 +158,8 @@ final class MonitorRepo
|
||||
':last_status' => in_array($status, ['ok', 'warning', 'error'], true) ? $status : 'error',
|
||||
':message' => $message,
|
||||
':metrics' => $metricsJson,
|
||||
':health' => $healthJson,
|
||||
':failing' => $failing !== [] ? mb_substr(implode(', ', $failing), 0, 255) : null,
|
||||
':group_key' => $groupKey,
|
||||
':os' => $os,
|
||||
]);
|
||||
@@ -146,10 +171,52 @@ final class MonitorRepo
|
||||
|
||||
$monitor['_previous_state'] = $previousState;
|
||||
$monitor['_state_changed'] = $previousState !== null && $previousState !== $state;
|
||||
$monitor['_failing_checks'] = $failing;
|
||||
|
||||
return $monitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt die Namen aller fehlgeschlagenen Pruefungen.
|
||||
*
|
||||
* Erwartetes Format, das die Anwendung mitschickt:
|
||||
* { "db": { "ok": true }, "feed": { "ok": false, "message": "..." } }
|
||||
*
|
||||
* Akzeptiert zur Bequemlichkeit auch { "db": true, "feed": false }.
|
||||
*
|
||||
* @param mixed $checks
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function failingChecks($checks): array
|
||||
{
|
||||
if (!is_array($checks) && !is_object($checks)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$failing = [];
|
||||
foreach ((array)$checks as $name => $check) {
|
||||
if (!is_string($name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_bool($check)) {
|
||||
if (!$check) {
|
||||
$failing[] = $name;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($check) || is_object($check)) {
|
||||
$data = (array)$check;
|
||||
if (array_key_exists('ok', $data) && !filter_var($data['ok'], FILTER_VALIDATE_BOOLEAN)) {
|
||||
$failing[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $failing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt einen Monitor manuell an (ohne Heartbeat).
|
||||
*
|
||||
@@ -357,6 +424,23 @@ final class MonitorRepo
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vermerkt, dass die Alarmierung dieses Monitors unterdrueckt wird, weil
|
||||
* ein uebergeordnetes System ausgefallen ist. Der Zustand bleibt sichtbar,
|
||||
* nur die Benachrichtigung entfaellt.
|
||||
*/
|
||||
public function setAlertSuppression(int $id, ?string $blockingParent): bool
|
||||
{
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE watchdog_monitors
|
||||
SET alert_suppressed_by = :parent
|
||||
WHERE id = :id
|
||||
');
|
||||
$stmt->execute([':parent' => $blockingParent, ':id' => $id]);
|
||||
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
public function setMaintenance(string $source, string $instance, ?string $untilUtc): bool
|
||||
{
|
||||
$stmt = $this->db->prepare('
|
||||
|
||||
Reference in New Issue
Block a user