Zum Inhalt springen

Observability: das Lesemodell, das handelt

Workbench darf seine Datenbank wegwerfen und neu aufbauen. Diese Anwendung pollt Graylog und legt GitLab-Issues an, und ein Seiteneffekt hat kein Upstream. Warum Dry-Run zur Datenbankspalte wurde, warum das Dashboard keinen Löschen-Knopf hat, und warum die Schutzschranke an der HTTP-Grenze sitzt statt in der Konfiguration.

Patrick Lehmann
5 Min. Lesezeit
Alles links der Grenze lässt sich wegwerfen und neu aufbauen. Alles, was sie überschritten hat, nicht.

Ein Lesemodell darf falsch sein. Es ist die Projektion von etwas Massgebendem und lässt sich daraus jederzeit neu aufbauen: auf dieser Überlegung ruht Workbench, der Delivery-Index später in dieser Serie. Observability beginnt mit derselben Form (Rails, PostgreSQL, eine Hintergrund-Queue und ein Upstream, dem die Wahrheit gehört) und tut dann eine Sache, die Workbench nie tut.

Es handelt.

Es pollt alle fünf Minuten Graylog, clustert Fehler nach Fingerprint und legt GitLab-Issues an. Dieses letzte Verb ist der ganze Artikel. Eine Projektion, die nur liest, darf man beliebig oft löschen und neu aufbauen. Eine Projektion, die hinausgreift und die Welt verändert, hat Zustand erzeugt, den kein Neuaufbau reproduzieren und kein Neuaufbau zurücknehmen kann.

Die Lesehälfte ist tatsächlich wegwerfbar

Das gehört deutlich gesagt, denn es ist die Hälfte, bei der man das Problem vermutet: Cluster und Observations sind eine Projektion von Graylog, und sie lassen sich neu aufbauen. Es gibt einen Backfill, der ein Zeitfenster in Fünf-Minuten-Buckets erneut importiert.

applications/me.squibble.observability
bin/rails "observability:backfill[me_obseed_beta,2026-03-14T00:00:00Z,2026-03-17T00:00:00Z]"
bin/rails "observability:backfill_last_days[me_obseed_beta,3]"

Der Reflex «Metriken haben kein Upstream, die Historie ist das Produkt» (dorthin wollte dieser Beitrag ursprünglich, als die Serie skizziert wurde) greift also nicht. Graylog ist das Upstream. Der Neuaufbau ist durch Graylogs Retention begrenzt, nicht durch irgendetwas in dieser Anwendung, und innerhalb dieses Fensters ist der Speicher genauso wegwerfbar wie der von Workbench.

Die interessante Grenze liegt ganz woanders.

Jeder Backfill ist eine geladene Waffe

Denselben Befehl noch einmal ansehen und sich vorstellen, er läuft gegen eine aktive GitLab-Synchronisation. Drei Tage Historie werden neu abgespielt, jeder Cluster überschreitet erneut den Schwellwert, und an einem Dienstagnachmittag erscheinen ein paar hundert Issues in einem Projekt. Nichts wurde beschädigt. Die Projektion ist sogar vollkommen korrekt. Es ist die Welt, die jetzt dreihundert Zeilen Müll enthält.

Backfills erzeugen deshalb standardmässig Cluster, Observations und Dry-Run-Issue-Previews. Sonst nichts. Echte Issues anzulegen verlangt ein explizites viertes Argument.

Ein Neuaufbau ist nur so lange sicher, wie das Neuaufgebaute keine Effekte hat. Sobald es welche hat, ist «einfach nochmal laufen lassen» kein Wiederherstellungsverfahren mehr, sondern ein Vorfall.

Das ist die Linie, die ein Lesemodell von einem System trennt, das handelt, und es ist keine Linie über Datenbanken. Es geht darum, auf welcher Seite der Systemgrenze eine Zeile landet.

dry_run ist eine Spalte, keine Umgebungsvariable

Die erste Fassung hatte ein dry_run-Flag in der Konfiguration: YAML plus eine Umgebungsvariable, also hing es davon ab, wie der Prozess gerade gestartet worden war, ob ein Lauf GitLab anfasst.

Heute ist es ein persistiertes Attribut am Integration-Record, und das Entscheidungsdokument formuliert es als Regel ohne Hintertür: keine globale Umgebungsvariable darf Dry-Run erzwingen oder überschreiben.

Dieselbe Überlegung hat die gesamte Konfiguration in die Datenbank verschoben: Integration besitzt das Graylog-zu-GitLab-Mapping, Schwellwerte, Rauschfilter und Polling-Intervall; GraylogConnection und GitlabConnection besitzen Endpunkte, Authentifizierungsverfahren und verschlüsselte API-Tokens. Eine überwachte Anwendung hinzuzufügen brauchte damit kein Deployment mehr.

Der Preis ist real und gehört genannt. Konfiguration war früher YAML in Git: mit Historie, mit Diff, mit Review. Heute sind es Zeilen in einer Tabelle. Wir haben git log auf der Konfiguration verloren und dafür die Eigenschaft gewonnen, dass das Dashboard nicht darüber lügen kann, was läuft.

Die Migration hatte ihr Verfallsdatum eingebaut

Der Umzug der Konfiguration in die Datenbank lief als bewusste Doppelquellen-Phase: YAML und Datenbank, per Flag ausgewählt, daneben ein Import- und ein Paritäts-Task. Das ist der gewöhnliche, sichere Weg.

Weniger gewöhnlich ist, dass dasselbe ADR, das die Doppelquelle einführte, auch ihre Entfernung festgeschrieben hat, inklusive der Release-Reihenfolge, und genau die ist der scharfe Teil:

  1. Das Release, das Import- und Paritäts-Task noch enthält, ausrollen bzw. stehen lassen.
  2. Die dauerhaften Encryption-Keys setzen.
  3. observability:import_yaml_configs ausführen, danach observability:verify_parity.
  4. Die importierten Connections, Integrationen, den Token-Status und (ausdrücklich) die persistierten dry_run-Werte prüfen.
  5. Erst danach das Removal-Release deployen.

Das Aufräumen in dieselbe Entscheidung zu schreiben, die das Provisorium einführt, ist eine Gewohnheit, die ich jedem empfehlen würde. Doppelquellen-Phasen enden nicht von selbst. Sie geraten in Vergessenheit, und zwei Jahre später kann niemand mehr sagen, aus welcher Quelle das laufende System eigentlich liest.

Es gibt keinen Löschen-Knopf, und zwar absichtlich

Das Dashboard kann eine Integration anlegen, bearbeiten, aktivieren und deaktivieren. Löschen kann es sie nicht.

Das ist kein Versehen und kein offenes Feature-Ticket. Deaktivieren erhält die Ausführungshistorie (jeden Lauf, seinen Auslösertyp, die Phasenzeiten und Zähler) und macht den Betriebszustand explizit statt abwesend. Eine Integration, die vor zwei Monaten deaktiviert wurde, ist eine Aussage über das eigene System. Eine gelöschte Integration ist eine Lücke, die genau so aussieht wie eine, die es nie gab.

Die Admin-API behält DELETE /api/v1/integrations/:id für kontrollierte administrative Eingriffe. Das ist die ehrliche Aufteilung: destruktive Operationen existieren, aber sie sind nicht einen Fehlklick von der Listenansicht entfernt.

Die Schutzschranke muss dort sitzen, wo die Kopie nicht hinreicht

Das ist der Teil, den ich am ehesten mitgeben möchte, und er hat einen zweiten Entwurfsdurchgang gebraucht.

Es gibt einen Task, der die Produktionsdatenbank nach lokal kopiert. Er ist bewusst bytegenau: Credentials, Ciphertext, PII, Audit- und Job-Historie bleiben erhalten, denn eine bereinigte Kopie ist eine Kopie, mit der sich nicht debuggen lässt. Danach legt er ein kleines transaktionales Overlay darüber: pendente Jobs löschen, jede Integration auf enabled=false und dry_run=true setzen, ar_internal_metadata.environment umschalten.

Dieses Overlay lohnt einen zweiten Blick. Es setzt Konfiguration. Und wir hatten gerade ein ganzes Release darauf verwendet, Konfiguration massgebend und persistiert (und damit mitkopierbar) zu machen.

Wäre das Overlay der einzige Schutz, dann wäre eine Produktionskopie, bei der eine Migration schiefgeht, oder jemand, der zur Fehlersuche eine Integration aktiviert, eine lokale Maschine mit echten Graylog- und GitLab-Tokens und eingeschalteter Live-Synchronisation.

Die eigentliche Schranke ist deshalb gar keine Konfiguration. Ausgehende Graylog- und GitLab-Aufrufe sind an beiden Faraday-Request-Grenzen gesperrt, ausser Rails läuft in production und das Deployment setzt exakt SQUIBBLE_OUTBOUND_EFFECTS_ENABLED=enabled. Kopierte Tokens kommen daran nicht vorbei. Kopierte Integrations-Flags kommen daran nicht vorbei. Nichts, was im Datenbank-Dump mitgereist ist, liegt auf dem Entscheidungspfad.

Was ich behalten würde

  • Fragen, was ein Neuaufbau mit der Aussenwelt macht, nicht nur mit der Datenbank. «Wegwerfbar» ist eine Eigenschaft von Projektionen ohne Effekte. Ein einziger ausgehender Schreibvorgang, und das Wort bedeutet etwas anderes.
  • Den Schalter, der einen Seiteneffekt regiert, dorthin legen, wo der Betrieb ihn liest. Ein Verhaltens-Flag in der Umgebung heisst, dass das Dashboard ein System beschreibt, das es womöglich nicht gibt.
  • Die Entfernung des Migrationsgerüsts in die Entscheidung schreiben, die es einführt (inklusive der Frage, welches Release dabei noch laufen muss).
  • Seiteneffekte an der Request-Grenze absichern, nicht in der Konfiguration (vor allem dann, wenn die Konfiguration etwas ist, das zwischen Umgebungen kopiert wird).

Der nächste Beitrag dieser Serie behält die Nebenwirkungen und wechselt das Bedrohungsmodell: ein Secrets-Speicher, der so gebaut ist, dass der Server nicht lesen kann, was er speichert.

Patrick Lehmann

Architecture & Governance Lead

Squibble GmbH

Bringt seit über zwanzig Jahren Struktur in gewachsene IT-Landschaften — als Architekt, Entwickler und Betreiber. Schreibt hier über die Systeme, die bei Squibble tatsächlich laufen, und über die Entscheide dahinter.

Weiterlesen

Boards: ein Kanban, das nicht weiss, wo seine Karten liegen

Jedes Board scheitert irgendwann daran, dass es zur zweiten Wahrheit wird: es merkt sich, in welcher Spalte eine Karte liegt, und ab da muss dieses Wissen gepflegt werden. Workbench merkt es sich gar nicht: Spalten sind Regeln, die beim Lesen ausgewertet werden.

Patrick Lehmann
9 Min. Lesezeit

Workbench: ein Delivery-Index, der falsch liegen darf

Wir haben eine mandantenfähige Sicht über mehrere GitLab-Instanzen gebaut, ohne zur zweiten Wahrheit zu werden. Die Regel, die das trägt: die lokale Datenbank ist wegwerfbar, und korrekt macht sie die Reconciliation, nicht die Webhooks.

Patrick Lehmann
5 Min. Lesezeit

Env Manager: ein Secrets-Speicher, der seine eigenen Secrets nicht lesen kann

Ein Security-Review hat darauf hingewiesen, dass unser Secrets-Manager seinen eigenen Administratoren vertraut. Die Antwort war Zero-Knowledge-Ende-zu-Ende-Verschlüsselung, und der ehrliche Teil des Entwurfs ist die Liste der drei Stellen, an denen der Server trotzdem Klartext sieht, und warum wir jede davon akzeptiert haben.

Patrick Lehmann
6 Min. Lesezeit