Sammelcommit fuer den ueber mehrere Sitzungen gewachsenen, bisher nicht
eingecheckten Stand. Getrennte Commits waeren ehrlicher gewesen, sind es
nachtraeglich aber nicht mehr: BugRepo, changelog.json und die Doku tragen
Aenderungen aus allen Straengen gleichzeitig, und keine Zwischenstufe waere
lauffaehig.
ROADMAP IM DEPLOYMENTCENTER (2.7.0)
Planungsstand, Reihenfolge und Zustaendigkeit lagen in handgepflegten
Markdown-Dateien neben einem Bugtracker, der genau diese drei Dinge bereits
fuehrt. Zwei Pfleger fuer dasselbe Feld - Drift war eine Frage der Zeit.
Ab jetzt: Zustand im DC, Begruendung in der Datei. Kein Feld hat zwei Pfleger.
- Migration 016: epic, roadmap_rank, target_build, defer_trigger,
doc_anchor, Status "deferred", Tabelle roadmap_epics.
KEINE Roadmap-Tabelle - ein Eintrag ist ein bugtracker_item.
- Zugehoerigkeit ist ein Feld: Ein Item steht auf der Roadmap, wenn es ein
epic hat. Ein zweites Kennzeichen daneben koennte auseinanderlaufen.
- Kennung ist die Item-ID. Handvergebene Kuerzel ("A12") erzeugen die
schlimmste Fehlerart: Beide Seiten stimmig, Verschiedenes gemeint, weil
die Kennung nach einer Loeschung neu vergeben wurde.
- Zurueckstellen verlangt defer_trigger. Ohne benannten Anlass ist ein
Vorhaben nicht zurueckgestellt, sondern vergessen. Die Regel sitzt im
Repository, damit sie auf allen drei Schreibwegen gleich greift.
- Modules/Bugtracker/Roadmap.php, /api/bugtracker/v1/roadmap.php,
WebUI-Modul mit Filtern, cli/roadmap.php --write/--check,
dc_roadmap und dc_roadmap_set im MCP-Adapter.
- cli/roadmap.php --write ERZEUGT den Zustandsteil der Markdown-Datei;
--check prueft nur die Prosa. Was erzeugt wird, kann nicht driften.
- docs/ROADMAP_MIGRATION.md: Uebernahme fuer die anderen Projekte.
TESTINSTANZ UND TESTTOR
- dc-dev.mhdf.de: zweite Installation, eigene DB, eigene Schluessel,
Basic-Auth davor, Rocket.Chat aus.
- 124 Testfaelle in tests/e2e gegen API, Rechte, Migrationen, Sperre.
- scripts/deploy.py bringt vor jedem Upload erst den anstehenden Stand auf
die Testinstanz und laesst dann den Testlauf laufen. Erst bei
Rueckgabewert 0 geht etwas auf die Produktion (--ohne-test als Notausgang).
Andersherum prueft der Lauf aelteren Code als den, der gleich hochgeht.
- tests/ ist aus dem Produktiv-Deploy ausgeschlossen; testctl.php kann
nicht versehentlich auf dc.mhdf.de landen.
- Ein Lauf mit mehr uebersprungenen als geprueften Faellen endet mit
Rueckgabewert 2 statt 0. Nichts ist kaputt, geprueft wurde es aber auch
nicht - der Unterschied darf nicht am Rueckgabewert verschwinden.
SUPERVISOR, PROJEKTLANDSCHAFT, NACHPRUEFUNG (2.6.0/2.6.1)
- Migration 013: projektuebergreifend melden (dev_agent, evidence,
triage_external, affects).
- Migration 014: supervisor_findings, Katalog, fuenf serverseitige
Detektoren im Tick.
- Migration 015: Nachpruefung als Arbeitsschlange statt Benachrichtigung.
- DEV-Lizenzschluessel zur Selbstbedienung, dc-testkit fuer die Test-VM.
- MCP-Adapter: 15 Werkzeuge, ohne Fremdabhaengigkeiten, mit Abgrenzung
von Fremddaten gegen Prompt-Injektion.
BEHOBEN
- observe und status im Supervisor legten den Zustand des Befundes als
"status" in die Antwort. Http::ok() merged ['status' => 'success']
darunter, also gewann der Befundzustand - eine angenommene Meldung kam
als status="open" an. Heisst jetzt finding_status. War Item #59.
Migrationen 013-016 sind auf dem Produktivserver noch NICHT ausgefuehrt.
Ohne sie bricht das WebUI, weil die Roadmap-Ansicht bei jedem Seitenaufruf
gegen bugtracker_items.epic abfragt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
214 lines
9.1 KiB
PHP
214 lines
9.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Deploymentcenter\Modules\Supervisor;
|
|
|
|
/**
|
|
* Der Detektor-Katalog.
|
|
*
|
|
* Legt fest, wonach der Supervisor sucht - unter Namen, die in jedem Projekt
|
|
* dieselben sind. Das ist der eigentliche Zweck: Befunde aus verschiedenen
|
|
* Anwendungen muessen vergleichbar bleiben, sonst ist die Sammlung nur ein
|
|
* Haufen Text.
|
|
*
|
|
* Ein Name hat die Form "familie.sache" und wandert nie mehr. Aendert sich,
|
|
* wonach gesucht wird, bekommt es einen neuen Namen - ein umbenannter Detektor
|
|
* zerreisst jede Auswertung, die ueber den Zeitpunkt der Umbenennung
|
|
* hinausgeht.
|
|
*
|
|
* Zur Einordnung, warum das mehr ist als "Unit-Tests zur Laufzeit": Ein Test
|
|
* prueft bekannte Logik gegen bekannte Eingaben in einer kontrollierten
|
|
* Umgebung. Die Detektoren hier pruefen gegen unbekannte Eingaben in einer
|
|
* unbekannten Umgebung, ueber Zeitraeume, die ein Testlauf nicht herstellen
|
|
* kann, und - die Gruppe NOVEL/SILENCE/DEVIATION - ohne den gesuchten Fehler
|
|
* vorher zu kennen.
|
|
*/
|
|
final class Catalog
|
|
{
|
|
/**
|
|
* Woher kommt ein Befund?
|
|
*
|
|
* SERVER - hier ermittelt, aus Daten, die ohnehin hereinkommen
|
|
* (Fehlerberichte, Heartbeats, Kennzahlen). Laeuft von selbst.
|
|
*
|
|
* CLIENT - von aussen ueber POST /api/supervisor/v1/observe eingeliefert.
|
|
* Der Name ist historisch: Gedacht war urspruenglich ein
|
|
* Beobachter im ausgelieferten Programm. Den gibt es nicht mehr -
|
|
* er haette zu viele Baustellen aufgemacht, und Error-Reporting
|
|
* und Bugtracker decken den Zweck ohnehin ab. Wer heute
|
|
* einliefert, sind Pruefwerkzeuge und Testlaeufe.
|
|
*
|
|
* Der Wert "client" bleibt, weil er so in supervisor_findings.origin
|
|
* steht; eine Umbenennung waere eine Migration ohne Gegenwert.
|
|
*/
|
|
public const ORIGIN_CLIENT = 'client';
|
|
public const ORIGIN_SERVER = 'server';
|
|
|
|
/**
|
|
* Der Katalog.
|
|
*
|
|
* origin - wer den Befund erzeugt
|
|
* needs - was der Detektor braucht, damit er ueberhaupt etwas sagen kann
|
|
* finds - wonach er sucht, in einem Satz
|
|
*
|
|
* @var array<string,array{origin:string,finds:string,needs:string}>
|
|
*/
|
|
public const DETECTORS = [
|
|
|
|
// ------------------------------------------------------------------
|
|
// Findet Dinge, an die niemand gedacht hat
|
|
// ------------------------------------------------------------------
|
|
// Diese Gruppe braucht kein Vorwissen ueber den konkreten Fehler. Der
|
|
// Massstab kommt nicht aus einem Kopf, sondern aus der Wirklichkeit:
|
|
// aus dem eigenen Verlauf, aus der Vorversion, aus den anderen
|
|
// Installationen derselben Fassung.
|
|
|
|
'novel.error_signature' => [
|
|
'origin' => self::ORIGIN_SERVER,
|
|
'finds' => 'Fehlersignatur, die es in dieser Fassung noch nie gab',
|
|
'needs' => 'Fehler-Stream (bugtracker_items mit error_level)',
|
|
],
|
|
'novel.log_template' => [
|
|
'origin' => self::ORIGIN_SERVER,
|
|
'finds' => 'Logzeilen-Schablone, die noch nie auftrat',
|
|
'needs' => 'Logauszuege im Fehler-Stream',
|
|
],
|
|
'silence.periodic' => [
|
|
'origin' => self::ORIGIN_CLIENT,
|
|
'finds' => 'Bekanntes wiederkehrendes Ereignis bleibt aus - der still gestorbene Hintergrundjob',
|
|
'needs' => 'Ein Melder, der das Ereignis bestaetigt, solange es stattfindet',
|
|
],
|
|
'silence.heartbeat' => [
|
|
'origin' => self::ORIGIN_SERVER,
|
|
'finds' => 'Instanz meldet sich nicht mehr',
|
|
'needs' => 'Watchdog-Monitor',
|
|
],
|
|
'deviation.self' => [
|
|
'origin' => self::ORIGIN_SERVER,
|
|
'finds' => 'Abweichung vom eigenen Verlauf der letzten Tage',
|
|
'needs' => 'Kennzahlen im MetricStore, mindestens 30 Messpunkte aelter als zwei Stunden',
|
|
],
|
|
'deviation.fleet' => [
|
|
'origin' => self::ORIGIN_SERVER,
|
|
'finds' => 'Ausreisser gegenueber anderen Installationen derselben Fassung',
|
|
'needs' => 'mehrere Installationen mit derselben app_version',
|
|
],
|
|
'regression.build' => [
|
|
'origin' => self::ORIGIN_SERVER,
|
|
'finds' => 'Kennzahl verschiebt sich nach einem Update gegenueber der Vorgaengerfassung',
|
|
'needs' => 'app_version am Monitor und Kennzahlen ueber den Rollout hinweg',
|
|
],
|
|
|
|
// ------------------------------------------------------------------
|
|
// Die Zeitachse
|
|
// ------------------------------------------------------------------
|
|
// Ein Test laeuft Millisekunden auf frischem Zustand, ein
|
|
// Kundenprozess Wochen. Alles, was sich ansammelt, ist fuer Tests
|
|
// strukturell unsichtbar.
|
|
|
|
'trend.monotonic' => [
|
|
'origin' => self::ORIGIN_CLIENT,
|
|
'finds' => 'Kennzahl waechst ueber mehrere Fenster nur nach oben - Speicher-, Handle-, Cache-Leck',
|
|
'needs' => 'Eine Messreihe ueber Stunden, von wem auch immer gemeldet',
|
|
],
|
|
'trend.saturation' => [
|
|
'origin' => self::ORIGIN_CLIENT,
|
|
'finds' => 'Kennzahl naehert sich einer harten Grenze - Platte, Verbindungspool, Dateihandles',
|
|
'needs' => 'bekannte Obergrenze',
|
|
],
|
|
'trend.latency' => [
|
|
'origin' => self::ORIGIN_CLIENT,
|
|
'finds' => 'Antwortzeiten driften ueber Tage nach oben - etwa Index-Degradation',
|
|
'needs' => 'Zeitmessung an einer wiederkehrenden Operation, regelmaessig gemeldet',
|
|
],
|
|
|
|
// ------------------------------------------------------------------
|
|
// Stellen, an denen still verziehen wird
|
|
// ------------------------------------------------------------------
|
|
// Der ergiebigste und billigste Fang: Zaehler auf die Stellen, an
|
|
// denen die Software heute schon still verzeiht. Dort sitzen die
|
|
// Fehler, die niemand meldet.
|
|
|
|
'swallowed.exception' => [
|
|
'origin' => self::ORIGIN_CLIENT,
|
|
'finds' => 'catch-Bloecke, die einen Fehler schlucken und weitermachen',
|
|
'needs' => 'Ein Melder an den catch-Bloecken, die einen Fehler schlucken',
|
|
],
|
|
'fallback.usage' => [
|
|
'origin' => self::ORIGIN_CLIENT,
|
|
'finds' => 'Rueckfall auf den degradierten Pfad haeuft sich',
|
|
'needs' => 'Ein Melder im Ausweichzweig',
|
|
],
|
|
'retry.storm' => [
|
|
'origin' => self::ORIGIN_CLIENT,
|
|
'finds' => 'Wiederholungsversuche haeufen sich',
|
|
'needs' => 'Ein Melder an der Wiederholung',
|
|
],
|
|
'probe.failed' => [
|
|
'origin' => self::ORIGIN_CLIENT,
|
|
'finds' => 'Registrierte Probe schlaegt fehl',
|
|
'needs' => 'Ein Pruefwerkzeug, das die Pruefung ausfuehrt und das Ergebnis meldet',
|
|
],
|
|
'probe.slow' => [
|
|
'origin' => self::ORIGIN_CLIENT,
|
|
'finds' => 'Probe antwortet, aber deutlich langsamer als ueblich',
|
|
'needs' => 'Wie probe.failed, zusaetzlich mit Zeitmessung',
|
|
],
|
|
|
|
// ------------------------------------------------------------------
|
|
// Fachliche Zusicherungen
|
|
// ------------------------------------------------------------------
|
|
// Die einzige Gruppe, die tatsaechlich "Unit-Test zur Laufzeit" ist -
|
|
// und fuer ihren Zweck ist genau das richtig. Sie deckt die Klasse ab,
|
|
// die sonst komplett durchfaellt: semantisch falsche, aber
|
|
// unauffaellige Ergebnisse. Der Bericht zeigt eine falsche Zahl, die
|
|
// Rundung stimmt seit Monaten nicht, ein Mandant sieht Daten eines
|
|
// anderen. Nichts stuerzt ab, keine Kennzahl weicht ab, keine Signatur
|
|
// ist neu - kein anderer Detektor sieht das.
|
|
|
|
'invariant.violated' => [
|
|
'origin' => self::ORIGIN_CLIENT,
|
|
'finds' => 'Vom Projekt formulierte Zusicherung gegen echte Daten verletzt',
|
|
'needs' => 'Ein Melder, der die Zusicherung gegen echte Daten prueft',
|
|
],
|
|
];
|
|
|
|
/** @return list<string> */
|
|
public static function names(): array
|
|
{
|
|
return array_keys(self::DETECTORS);
|
|
}
|
|
|
|
public static function isKnown(string $detector): bool
|
|
{
|
|
return isset(self::DETECTORS[$detector]);
|
|
}
|
|
|
|
/**
|
|
* Wo entsteht dieser Befund normalerweise?
|
|
*
|
|
* Unbekannte Detektoren gelten als clientseitig - ein Projekt darf einen
|
|
* eigenen Namen melden, ohne dass der Katalog dafuer erweitert werden
|
|
* muss. Es taucht dann in der Auswertung als eigene Reihe auf und faellt
|
|
* beim Durchsehen auf, statt still verworfen zu werden.
|
|
*/
|
|
public static function originOf(string $detector): string
|
|
{
|
|
return self::DETECTORS[$detector]['origin'] ?? self::ORIGIN_CLIENT;
|
|
}
|
|
|
|
/**
|
|
* Nur die Detektoren einer Herkunft.
|
|
*
|
|
* @return array<string,array{origin:string,finds:string,needs:string}>
|
|
*/
|
|
public static function byOrigin(string $origin): array
|
|
{
|
|
return array_filter(
|
|
self::DETECTORS,
|
|
static fn(array $meta): bool => $meta['origin'] === $origin
|
|
);
|
|
}
|
|
}
|