post

Produkte berichten über ihre API

Produktstatistiken bleiben verständlich, wenn jedes System seine Daten selbst verdichtet und über einen kleinen, zeitlich einheitlichen HTTP-Vertrag anbietet.

≈ 10 Min. Lesezeit

Diesen Beitrag anhören (16 Min.)

MP3 herunterladen

Eine Statistik-API ist eine Produktfunktion. Dieser Satz klingt selbstverständlich, wurde in der Arbeit am Mission Cockpit aber erst nach zwei schlechteren Entwürfen klar. Zunächst sollte ein zentraler Sammler direkt auf mehrere PostgreSQL-Datenbanken zugreifen. Danach standen besondere private Reporting-Endpunkte und eigene Schlüssel im Raum. Beides hätte das Cockpit tiefer in die Produkte eingebaut, als es für einen Report nötig ist.

Übrig blieb ein kleinerer Vertrag. ContentKit, WikiKit und die Orchestrierungssoftware verdichten ihre Daten in der jeweils eigenen PostgreSQL-Datenbank. SlideKit besitzt keine Datenbank und schreibt ausschließlich anonyme Aggregate in seinen dauerhaften Laufzeitspeicher. Alle vier bieten ihre Sicht über normale, authentifizierte HTTP-Endpunkte an. Der Sammler sieht weder Tabellen noch Markdown, Namen, Schlüssel oder interne IDs.

Diese Aufteilung ist kein spezieller Kniff für Dashboards. Jeder autorisierte Verbraucher kann dieselbe API verwenden: ein Kapazitätsreport, eine Qualitätsprüfung, eine lokale Auswertung oder ein späteres Exportwerkzeug.

Datenbesitz ist mehr als Speicherort

Wer eine Tabelle besitzt, kennt ihre Bedeutung. ContentKit weiß, wann eine Revision erstellt und wann ein Release tatsächlich aktiviert wurde. WikiKit unterscheidet einen gestarteten Ingest von einem geprüften Wissensstand. SlideKit kann einen Cache-Treffer von einem vollständig gerenderten Deck trennen. Ein zentraler Sammler müsste diese Bedeutungen nachbauen und bei jeder Produktänderung mitziehen.

Direktes SQL macht diesen Nachbau besonders leicht – und damit gefährlich. Eine Abfrage funktioniert, obwohl sie vielleicht einen Zwischenstatus mitzählt, den das Produkt selbst niemals als abgeschlossen anzeigen würde. Eine neue Migration kann den Report brechen, ohne dass sich die öffentliche Produkt-API geändert hat. Der Datenbankzugriff umgeht außerdem die normale Rechteprüfung und zieht Credentials mehrerer Systeme an eine Stelle.

Mit einer Produkt-API liegt die fachliche Verdichtung dort, wo auch die Zustandsänderung implementiert ist:

  1. Produktaktion · erzeugt einen fachlich definierten Zustandsübergang
  2. Eigener Datenspeicher · bewahrt die interne Wahrheit des Produkts
  3. Aggregation · übersetzt interne Zustände in begrenzte fachliche Kennzahlen
  4. Statistik-API · liefert authentifizierte Werte mit Zeitraum und Bedeutung
  5. Collector · führt kompatible Antworten zusammen, ohne sie neu zu definieren
  6. Report · erklärt den eingefrorenen gemeinsamen Stand

Der Sammler darf die Antworten zusammenführen, aber nicht neu deuten, was ein erfolgreicher Release oder ein verifizierter Claim ist. Diese Begriffe gehören dem Produkt.

Einheitlich ist die Zeit, nicht die Fachlichkeit

Ein gemeinsamer Vertrag bedeutet nicht, dass alle Produkte dieselben Kennzahlen liefern. Das wäre eine künstliche Gleichmacherei. Gemeinsam sind nur die Eigenschaften, die ein Collector für zuverlässige Zeitfenster benötigt:

  • bucket bestimmt die Auflösung: Stunde, Tag, Monat oder Jahr.
  • from ist der eingeschlossene Anfang des Fensters.
  • to ist das ausgeschlossene Ende.
  • tz macht die Zeitzone ausdrücklich, hier durchgehend UTC.
  • buckets enthält eine dichte Reihe einschließlich leerer Intervalle.
  • totals fasst genau das angefragte Fenster zusammen.

Die Nutzlast bleibt fachlich. ContentKit berichtet über Releases, Content, Reader, Webhooks, Audio und Engagement. WikiKit berichtet über Ingests, Wissen, Modellnutzung und Webhooks. SlideKit liefert Builds, Renderdauer, Cache und asynchrone Jobs. Die Ausführungsschicht kennt Läufe, Connector-Aufrufe, Ereignisklassen, Modellnutzung und Qualität.

{
  "bucket": "hour",
  "tz": "UTC",
  "from": "2026-07-18T10:00:00.000Z",
  "to": "2026-07-18T12:00:00.000Z",
  "buckets": [
    { "ts": "2026-07-18T10:00:00.000Z", "items_created": 2 },
    { "ts": "2026-07-18T11:00:00.000Z", "items_created": 0 }
  ],
  "totals": { "items_created": 2 }
}

Die halboffene Schreibweise [from,to) verhindert Doppelzählungen. Der Lauf für 10 bis 11 Uhr enthält ein Ereignis um 10:59

, aber keines um 11:00
. Das nächste Fenster beginnt genau dort. Ein Retry fragt dasselbe Fenster erneut ab und erhält dieselbe Bedeutung.

Null, unbekannt und unvollständig

Eine dichte Zeitreihe enthält auch leere Buckets. Das ist praktisch, weil der Collector keine fehlenden Stunden erraten oder auffüllen muss. Eine Null ist dabei ein gemessener Wert: Im Fenster gab es kein entsprechendes Ereignis.

Davon zu unterscheiden sind zwei andere Zustände. „Unbekannt“ bedeutet, dass eine Quelle oder Kennzahl noch nicht angeschlossen ist. „Unvollständig“ bedeutet, dass ein erwarteter Snapshot fehlt oder das Fenster noch nicht abgeschlossen war. Diese drei Fälle dürfen im Report nicht gleich aussehen.

type ObservedValue =
  | { state: "measured"; value: number }
  | { state: "missing_source"; connector: string }
  | { state: "incomplete_window"; expected: number; observed: number };

Für jemanden, der einen Report liest, ist diese Unterscheidung wichtiger als eine weitere Nachkommastelle. Null technische Fehler sind beruhigend. Null Seitenaufrufe können stimmen. Fehlende Analytics-Daten sind keine Null und sollten als offene Verbindung erscheinen.

Datenschutz beginnt bei der Antwortform

Aggregate sollten Informationen weglassen, die der Zweck nicht benötigt. ContentKit zählt erfolgreiche, fehlgeschlagene und rate-limitierte Reader-Anmeldungen, speichert dafür aber weder Benutzernamen noch IP-Adresse, Session-ID oder Fehlerdetail in der Statistikstabelle. SlideKit hält keine Decknamen, Markdown-Inhalte, URLs, Job-IDs oder Credentials in seinem Aggregat. WikiKit gibt keine Quellentexte über die Statistik-API aus.

Eine solche Negativliste gehört in die Dokumentation und in Vertragstests. „Wir geben aktuell keine Namen zurück“ ist schwächer als ein Schema, in dem es für Namen gar kein Feld gibt. Der Collector kann dann keinen sensiblen Wert versehentlich in ein JSON-Artefakt oder eine Reportseite übernehmen.

Auch Trace-Daten bleiben auf ihren Zweck begrenzt. W3C Trace Context standardisiert traceparent und tracestate, damit ein Aufruf über mehrere Dienste korreliert werden kann. Die W3C-Empfehlung warnt ausdrücklich davor, personenbezogene oder andere sensible Informationen in diesen Headern zu transportieren. Eine Trace-ID verbindet technische Schritte; sie beschreibt keinen Menschen.

GET /v1/sites/example/stats/releases?bucket=hour&from=2026-07-18T10:00:00Z&to=2026-07-18T11:00:00Z
Authorization: Bearer <site-read-key>
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

Zero-config heißt: vorhandene Begriffe verwenden

Vollständig konfigurationslos ist Telemetrie selten. Ein Dienst braucht zumindest eine dauerhafte Ablage und eine Information darüber, ob er in Entwicklung, Test oder Produktion läuft. „Zero-config“ ist hier enger gemeint: Für das Cockpit wird im Produkt kein Cockpit-Modus aktiviert, kein besonderer Schlüssel erzeugt und kein Reportziel eingetragen.

Die Statistik-Endpunkte verwenden vorhandene Produktgrenzen. ContentKit nutzt den Site-Slug und einen normalen content:read-Key. WikiKit nutzt den Space und knowledge:read. SlideKit verwendet denselben Service-Key wie Render- und Job-Endpunkte. Die Orchestrierungssoftware nutzt ihre gewöhnlichen mandantenbezogenen Leserechte.

OpenTelemetry liefert dafür ein brauchbares gemeinsames Vokabular. service.name benennt den logischen Dienst. deployment.environment.name unterscheidet development, staging, production und test. Der ältere Schlüssel deployment.environment ist inzwischen als veraltet markiert. Solche standardisierten Felder verhindern, dass ein Dienst prod, ein anderer live und ein dritter production-eu als dasselbe Konzept behandelt.

resource:
  service.name: contentkit
  service.version: 1.14.5
  deployment.environment.name: production

Das Produkt setzt diese Werte einmal für seine Laufzeit. Jeder strukturierte Logeintrag und jeder weitergereichte Trace kann sie anschließend verwenden. Der Reporting-Collector muss keine Produktetiketten reparieren.

Die gemeinsame Bezeichnung allein genügt allerdings nicht. Im ersten produktiven Lauf las der Connector-Aggregator status_code und latency_ms, während ein älterer Egress-Pfad noch status und duration_ms schrieb. 37 erfolgreiche HTTP-Aufrufe erschienen dadurch als network_error. Der Übergang wurde absichtlich in beide Richtungen kompatibel gemacht: Der Leser versteht alte und neue Felder, der Schreiber liefert vorerst beide, und neue Tests verwenden echte Auditzeilen statt handgebauter Idealobjekte. Ein erfolgreicher S3-Aufruf ohne HTTP-Status heißt nun no_status und nicht Netzwerkfehler.

Eine zweite Lücke betraf die Umgebung selbst. Ein stündlicher Schedule verwendete PROD-Connectoren, sein Execution-Datensatz stand jedoch auf dev; die Laufzeit musste bei jedem Schritt auf PROD ausweichen und warnte zu Recht. Seitdem trägt der Schedule execution_mode als persistierten Teil seines allgemeinen Vertrags. Neue Automatisierungen verwenden prod, ein kurzer Entwicklungsloop muss dev ausdrücklich wählen. Damit stimmen Queue, Ausführung, Audit und Statistik über denselben Begriff überein.

Begrenzte Abfragen statt versteckter Last

Eine allgemeine Statistik-API braucht harte Grenzen. ContentKit erlaubt stündliche Fenster bis 31 Tage, tägliche bis 366 Tage, monatliche bis fünf Jahre und jährliche bis zehn Jahre. SlideKit begrenzt stündliche Build-Abfragen auf 90 Tage. Ungültige oder zu große Fenster liefern 422, statt die Produktdatenbank mit einer unerwarteten Vollauswertung zu belasten.

Diese Grenzen gehören zum Produktvertrag, weil nur das Produkt seine Kosten kennt. Ein Collector sollte nicht versuchen, sie mit vielen überlappenden Abfragen zu umgehen. Für einen langen Zeitraum verwendet er die gröbere Auflösung oder arbeitet in eindeutigen, nicht überlappenden Seiten.

Dasselbe gilt für Aufbewahrung. ContentKit entfernt seine anonymen Reader-Ereignisse standardmäßig nach 400 Tagen. Ein Reportartefakt kann länger bestehen, wenn sein verdichteter Inhalt dafür vorgesehen ist. Rohereignis, Produktaggregat und veröffentlichter Report haben verschiedene Lebenszyklen; sie müssen nicht in einem Speicher enden.

Bestehende Schlüssel statt einer Hintertür

Ein eigener „Reporting-Key“ wirkt zunächst ordentlich, schafft aber eine zweite Sicherheitslogik. Was darf dieser Schlüssel lesen? Gilt er für eine Site, einen Space oder das ganze Produkt? Wer rotiert ihn? Sobald jedes Produkt darauf eine andere Antwort gibt, wird aus dem vermeintlich einfachen Collector ein Sonderfall mit weitreichenden Rechten.

Die Statistik-APIs verwenden deshalb vorhandene Scopes. Ein ContentKit-Key mit content:read darf Statistiken genau der Site lesen, für die er ausgestellt wurde. Ein WikiKit-Key benötigt knowledge:read und bleibt auf seinen Space begrenzt. SlideKit setzt seinen normalen Service-Key voraus und liefert ohne konfigurierte Authentifizierung für die Statistikroute 503, statt versehentlich einen öffentlichen Endpunkt zu öffnen.

Für den Collector gilt damit dieselbe Regel wie für andere API-Nutzer: pro Produkt ein eigener, möglichst enger Schlüssel; Secrets ausschließlich in der Credential-Ablage; keine Schlüssel in Workflowdefinition, JSON-Artefakt, Log oder Report. Eine Rotation verändert die Credential-Referenz, nicht den fachlichen Ablauf.

connectors:
  - name: content-publishing-stats
    base_url: https://content.example.test
    credential_ref: content-read-key
  - name: knowledge-stats
    base_url: https://knowledge.example.test
    credential_ref: knowledge-read-key

Der Workflow kennt nur credential_ref. Der tatsächliche Wert wird beim HTTP-Aufruf eingesetzt und erscheint weder im gespeicherten Ablauf noch in dessen Ergebnis. Diese Trennung lässt sich testen: Ein Aufruf ohne Schlüssel muss 401 liefern, ein Schlüssel ohne passenden Scope 403, ein korrekter Schlüssel 200.

PostgreSQL ist intern, nicht verboten

Die Ablehnung direkter Datenbankabfragen bedeutet nicht, dass Aggregate außerhalb der Produktdatenbank liegen müssen. Für ContentKit und WikiKit ist PostgreSQL gerade der passende Ort. Ereignis und fachlicher Zustand lassen sich dort transaktional erfassen, nach Site oder Space begrenzen und mit denselben Migrationen weiterentwickeln wie der Rest des Produkts.

Wichtig ist die Richtung der Abhängigkeit. Das Produkt darf seine eigene Datenbank kennen. Ein fremder Collector darf sie nicht kennen müssen. Ob eine Antwort aus einer vorberechneten Tabelle, einer begrenzten SQL-Aggregation oder einem Cache entsteht, bleibt eine interne Entscheidung.

SlideKit zeigt den Gegenfall. Für einen kleinen zustandsarmen Renderer wäre eine neue PostgreSQL-Instanz nur für Build-Zähler unverhältnismäßig. Sein Aggregat liegt atomar im dauerhaften Plattformverzeichnis. Es enthält keine Nutzdaten und meldet eine beschädigte oder nur teilweise wiederhergestellte Datei über quality.partial und begründende Codes. Der API-Vertrag bleibt derselbe, obwohl die Ablage eine andere ist.

„Alles in PostgreSQL“ und „alles in Dateien“ wären beides technische Gleichmacherei. Entscheidend ist, dass jedes Produkt eine zu seinem Betriebsmodell passende dauerhafte Ablage wählt und nach außen denselben verlässlichen Zeitvertrag erfüllt.

Dokumentation gehört zum Vertrag

Ein neuer Endpunkt ist erst fertig, wenn OpenAPI, llms.txt, llms-full.txt, README und die ausführliche Produktdokumentation dasselbe erzählen. Sonst findet ein Mensch einen Scope in der README, ein Client generiert aus einer älteren OpenAPI-Datei und ein Agent liest eine dritte Variante.

Darum prüfen die Repositories ihre generierten Dokumente auf Drift. Vertragstests schicken gültige, ungültige, zu große und nicht autorisierte Fenster an die Handler. Integrationstests verwenden die echte Produktablage. Ein Binärtest stellt schließlich sicher, dass die ausgelieferte Datei dieselben Routen und dieselbe Versionsnummer enthält wie der Quellstand.

Diese Sorgfalt wirkt unspektakulär, verhindert aber genau jene Kopplung, die eine API eigentlich auflösen sollte. Der Collector wird gegen den veröffentlichten Vertrag gebaut, nicht gegen eine Erinnerung daran, wie die Antwort gestern aussah.

APIs machen Austausch möglich

Der unmittelbare Effekt dieser Trennung zeigt sich beim Testen. Jede Produkt-API lässt sich mit einem festen Fenster gegen ihr eigenes Schema prüfen. Der Collector erhält Fixtures ohne Zugriff auf die Datenbank. Ein Produktrelease kann unabhängig zurückgerollt werden, solange der veröffentlichte Vertrag erhalten bleibt.

flowchart TD
    P["Produktrelease"] --> T["eigene Unit- und Integrationstests"]
    T --> O["OpenAPI- und Doku-Driftprüfung"]
    O --> L["Live /stats Contract"]
    L --> C["Collector-Contract"]
    C --> R["Report-Artefakt"]
    L -. unverändert .-> X["Collector bleibt unverändert"]

Ein neuer Verbraucher braucht keine neue Tabelle im Produkt. Ein anderes Reporting-Werkzeug kann die API ebenso aufrufen. Wird SlideKit später mit einer Datenbank betrieben, darf sich seine interne Ablage ändern, ohne dass /v1/stats/builds neu erfunden wird.

Das hält auch die Produkte verständlich. ContentKit bleibt ein Publishing-System, WikiKit eine kuratierte Wissensbasis und SlideKit ein Renderer. Ihre Statistik-APIs beschreiben die eigene Arbeit. Alles Weitere – Zusammenführung, Aufbewahrung, Darstellung und fachübergreifende Analyse – geschieht außerhalb ihrer Grenzen.

Was sich seit damals geändert hat

Der Beitrag HTTP APIs: klein, stabil, lesbar formulierte 2014 einen Endpunkt als langfristiges Versprechen an unbekannte Clients. Kleine Ressourcen, eindeutige Zeitstempel, stabile Bedeutungen und lesbare Fehler sollten verhindern, dass jeder Client eine eigene Abwehrschicht baut. Diese Anforderungen gelten weiterhin.

Neu ist hier der Blick auf interne Produktstatistiken. Ein Statistikpfad wird nicht für ein bestimmtes Cockpit gebaut, sondern als begrenzte Projektion der eigenen Produktdaten. Er verwendet bestehende Schlüssel und Scopes, liefert Zeitgrenzen und Datenqualität mit und veröffentlicht nur Aggregate. OpenAPI, README und maschinenlesbare Dokumentation entstehen aus demselben Vertrag. Damit bleibt die API klein, obwohl inzwischen Menschen, Workflows und Agenten zu ihren Clients gehören.

Weiterführende Quellen

Wie fandest du diesen Beitrag?

Kommentare