Der Tag, an dem ich das Tracing endlich einschalten konnte
Vier Tage, 116 Commits: von Datadog zu einem selbst gehosteten Observability-Stack, und warum ein nutzungsbasiertes Preismodell bestimmt, wie viel man sieht.

Vorweg, auch wenn es in einem Bericht über den Umstieg seltsam klingt: Datadog ist ein gutes Produkt. Die Oberfläche ist eine der besten im Feld, die Integrationen decken alles ab, was wir betreiben, und man sieht damit Dinge, für die man anderswo drei Werkzeuge braucht.
Ich habe es trotzdem abgeschaltet. Nicht weil es schlecht wäre, sondern weil wir das meiste davon nicht brauchen — und weil das, was wir tatsächlich gebraucht hätten, in diesem Preismodell unerreichbar war.
Das ist der eigentliche Punkt dieses Berichts. Ein Monitoring, das man sich nicht leisten kann, schaltet man nicht ein. Und was man nicht einschaltet, sieht man nicht.
Wie das Preismodell bestimmt, was man misst
Es geht um eine Anwendung mit mehreren Services: zwei Spring-Boot-APIs, zwei Web-Oberflächen, Keycloak, Temporal, Redis, zwei Kubernetes-Cluster auf Exoscale SKS. Zum Zeitpunkt der Umstellung: sieben Nodes.
Datadogs Infrastructure-Tarif enthält 100 Custom Metrics pro Host. Bei sieben Hosts sind das 700. Klingt großzügig — bis man weiß, wie gezählt wird. Eine „Custom Metric“ ist nämlich nicht eine Kennzahl, sondern jede eindeutige Kombination aus Metrikname und Labels. Die Antwortzeit von HTTP-Anfragen ist eine Kennzahl; aufgeschlüsselt nach Methode, Pfad und Statuscode sind es hunderte abrechenbare Zeitreihen.
Ich habe nachgezählt, was ein Spring-Boot-Actuator tatsächlich ausliefert — heute, im laufenden System:
count({__name__=~"jvm_.+|http_server_.+|hikaricp_.+|spring_.+|tomcat_.+|temporal_.+"})
→ 8.7628.762 Zeitreihen allein aus den beiden APIs. Das Zwölffache des inkludierten Kontingents, bevor eine einzige Infrastrukturmetrik dazukommt. Jeder Heap-Pool, jeder Connection-Pool, jedes Latenz-Histogramm, jeder HTTP-Statuscode je Endpunkt ist in diesem Modell eine eigene, separat abgerechnete Custom Metric.
Die Folge war keine Kostenexplosion. Die Folge war Selbstzensur: Man nimmt eben nicht alle JVM-Metriken. Man lässt die Histogramme weg. Man sammelt Logs nicht cluster-weit, sondern mit Opt-in pro Pod. Und Distributed Tracing — also die Verfolgung eines einzelnen Requests über alle beteiligten Dienste hinweg — kostet 31 $ pro Host und Monat, plus Volumen. Das habe ich nie eingeschaltet. Die Frage „warum hat dieser Request elf Sekunden gebraucht“ war damit schlicht nicht beantwortbar. Nicht aus technischen Gründen, sondern weil die Antwort ein Budgetposten gewesen wäre.
Ein Monitoring, dessen Preis mit jeder zusätzlichen Beobachtung steigt, verleitet dazu, weniger zu messen. Die fehlenden Daten bemerkt man erst im Störfall.
Zwei Gründe, die nicht auf der Rechnung stehen
Datenhoheit. Wir verarbeiten sensible Daten. Die Telemetrie lag auf Datadogs EU-Site, das war nie das Problem — der Auftragsverarbeiter war trotzdem ein US-Unternehmen, mit allem, was rechtlich daran hängt. In unserem Verzeichnis der technischen und organisatorischen Maßnahmen stand Datadog Inc. als Sub-Auftragsverarbeiter. Dieser Eintrag entfällt jetzt. Logs, Metriken und Traces liegen in Österreich, in Buckets, auf die zwei namentlich bekannte IAM-Rollen Zugriff haben.
Nichts davon war Code. Dashboards und Monitore wurden im Web-UI zusammengeklickt. Es gab kein Review, kein Diff, kein Rollback und keine Antwort auf die Frage „wer hat diesen Schwellwert wann von 5 auf 50 gesetzt und warum“. Für ein Team, das jede Zeile Infrastruktur in Pulumi und ArgoCD hält, war das der letzte weiße Fleck.
Was an die Stelle tritt
Bevor es losgeht, die Namen — sie tauchen im Folgenden alle wieder auf. Sechs Bausteine ersetzen zusammen das, was vorher ein einzelner Anbieter geliefert hat. Die Kombination läuft in der Szene unter dem Kürzel LGTM: Loki, Grafana, Tempo, Mimir. Den Platz von Mimir nimmt hier VictoriaMetrics ein, warum, steht weiter unten.
| Baustein | Aufgabe | Wofür bei Datadog |
|---|---|---|
| Alloy | sammelt Telemetrie ein und verschickt sie — der „Agent“ | Datadog Agent |
| VictoriaMetrics | speichert Metriken: Messwerte über die Zeit | Datadog Metrics |
| Loki | speichert Logs | Datadog Logs |
| Tempo | speichert Traces: den Weg eines Requests durch alle Dienste | Datadog APM |
| Grafana | zeigt alles an und schlägt Alarm | Datadog-Oberfläche + Monitore |
| Faro | misst, was im Browser der Anwender passiert | Datadog RUM |
Alles davon ist Open Source und läuft auf der Kubernetes-Infrastruktur, die ohnehin schon da ist.
Montag: die Bestandsaufnahme
Für den self-hosted Stack war es eine grüne Wiese. Bevor ich etwas aufgebaut habe, habe ich aufgeschrieben, was Datadog heute alles liefert: jede Stelle im Repository mit Datei und Zeilennummer, 20 insgesamt.
Das Ergebnis:
- Datadog lief in beiden Clustern, mit OTLP-Receiver, Trace-Agent, Process-Agent und kube-state-metrics.
- RUM, die Messung im Browser der Anwender, lief in beiden Oberflächen, für jede Sitzung.
- Die CI lud bei jedem Build die Sourcemaps zu Datadog hoch.
All das muss der neue Stack übernehmen, ohne Lücke. Alter und neuer Stack laufen also eine Zeit lang parallel, und das bestimmt den ganzen Umbau:
Alles Additive ist erlaubt, nichts wird von Datadog weggenommen. Nach jedem Meilenstein muss Produktion funktionieren. Der Preis dafür ist ein vierter Prod-Node für 68 € im Monat.
Nebenbei fiel ein Fund auf, der mit Monitoring nichts zu tun hatte: Die Sourcemaps beider Anwendungen waren öffentlich abrufbar, .map antwortete in Produktion mit HTTP 200. Für den Umbau war das praktisch: Alloy holt sie sich selbst ab, die Sourcemap-Pipeline in der CI konnte ersatzlos wegfallen.
Dann die Werkzeugwahl. Geplant war Mimir, Grafanas Metrik-Datenbank. Für den Betrieb als einzelne Instanz gibt es aber kein Helm-Chart, also wurde es victoria-metrics-single.
Dienstag: das Fundament
VictoriaMetrics, Loki, Tempo, Alloy, ein eigener Kubernetes-Namespace, zwei Objektspeicher-Buckets mit je einem eigenen, auf genau diesen Bucket beschränkten Zugriffsrecht. Dazu Datasources, die ersten Kubernetes-Dashboards und — nicht als Nachgedanke — derselbe Stack in der lokalen Entwicklungsumgebung, damit ich eine Alert-Regel lokal testen kann, statt sie in Produktion zu entdecken.
Tempo habe ich gleich mit ausgerollt, obwohl noch keine Traces ankamen. So zeigen sich Fehler in der Einrichtung, solange noch nichts davon abhängt, und nicht erst an dem Tag, an dem die Traces umgestellt werden.
Die Fallstricke kamen sofort und waren allesamt von der gleichen Art — Konfiguration, die plausibel aussieht und nicht tut, was sie sagt:
- Ein
retentionDiskSpaceUsage-Flag, das es in dieser Schreibweise nicht gibt. - Ein Grafana-Admin-Passwort, das das Chart bei jedem Render neu würfelt und damit von dem abweicht, was in Grafanas Datenbank steht — was still das Neuladen der Provisionierung bricht.
- Und der Klassiker:
*.svc.cluster.local. Exoscale SKS benutzt das nicht. Dort heißt die Cluster-Domain<cluster-uuid>.cluster.local. Jeder voll qualifizierte Name aus einem Helm-Default löste ins Leere auf.
Genau dieser Fehler steckte auch hinter den Loki-Caches, die am Donnerstag auffielen.
Mittwoch: ein Gateway für Metriken, Logs und Traces
58 Commits an einem Tag. An diesem Tag flossen die Daten zum ersten Mal in den neuen Stack.
Die zentrale Entscheidung des Umbaus steckt in einer einzigen Komponente: einem öffentlich erreichbaren Alloy unter ingest.example.at, das nativ OTLP spricht. OTLP ist das Protokoll von OpenTelemetry, dem herstellerneutralen Standard für Telemetrie: ein Format für alle drei Signale, das inzwischen jedes Werkzeug versteht. Alles, was von außen kommt, kommt hier an. Von außen heißt hier: der externe Dienst, die Browser der Anwender und, wie sich zeigt, auch der Test-Cluster.
Angefangen hatte ich mit dem Naheliegenden: ein Prometheus-Remote-Write-Receiver, ein Loki-Push-Receiver, jeweils auf eigenem Pfad, Authentifizierung über eine Traefik-Middleware. Das funktionierte, hatte aber einen Konstruktionsfehler: Jeder Absender brauchte pro Signal einen eigenen Pfad, und wer etwas geschickt hatte, kam in der Verarbeitung nicht an. Ich habe es noch am selben Tag ersetzt durch einen OTLP-Endpunkt:
| Pfad | Signal |
|---|---|
/v1/metrics | Metriken |
/v1/logs | Logs |
/v1/traces | Traces |
/collect | Browser-Telemetrie (Faro) |
Der eigentliche Gewinn sind nicht die einheitlichen Pfade, sondern der Ort, an dem die Authentifizierung stattfindet. Sie ist von Traefik nach Alloy gewandert, denn otelcol.receiver.otlp kann die authentifizierte Identität an die Verarbeitungskette weiterreichen. Ein Prozessor stempelt daraus ein client-Attribut:
otelcol.processor.attributes "identify" {
action {
key = "client"
from_context = "auth.username"
action = "upsert"
}
...
}Ein Sender kann sich damit nicht als ein anderer ausgeben. Und ein neuer Absender ist jetzt ein Name in Pulumi — daraus werden Basic-Auth-Benutzername, htpasswd-Zeile und der Wert des client-Attributs auf allem, was er schickt. Zuvor brauchte dasselbe pro Client einen URL-Pfad, ein Ingress, eine Middleware, einen Service-Port und einen Receiver. Weder ein Loki- noch ein Prometheus-Receiver in Alloy kann Request-Header lesen; OTLP kann es.
Widerrufen läuft genauso rückwärts: Name aus der Liste entfernen, pulumi up. Was der Client bereits geschickt hat, trägt weiterhin sein Label und ist damit auffindbar — und löschbar:
{client="extern-standort-a"}Genau diese Sorte Client ist der Grund, warum sich der Aufwand gelohnt hat. Einer unserer Dienste läuft nicht bei uns, sondern als eigenständige Installation an einem externen Standort. Er hat vorher mit einer Loki-spezifischen Bibliothek direkt gepusht; jetzt spricht er über den OpenTelemetry-Logback-Appender dasselbe Protokoll wie alles andere und wird ohne zweiten Transportweg auch Traces tragen. Ohne Passwort installiert sich die Telemetrie gar nicht erst — eine Installation, die noch keine Zugangsdaten bekommen hat, ist still statt in einer Endlosschleife. Beim Start meldet die Installation, ob die Telemetrie läuft oder mangels Zugangsdaten abgeschaltet ist. Das alte Setup hat das nicht gemeldet.
Eine Feinheit, die ich unterschätzt hatte: Eingehendes OTLP wird am Gateway zurück in die nativen Formate der Backends übersetzt, statt es an deren eigene OTLP-Endpunkte weiterzureichen. Das sähe sauberer aus und kostet echte Kompatibilität — Lokis OTLP-Ingestion indiziert nur ihre feste Attributliste, benennt namespace in k8s_namespace_name und pod in k8s_pod_name um und verwirft cluster ganz. Also genau die Labels, nach denen jedes Dashboard und jeder Alert filtert.
Browser-Telemetrie kommt über /collect an, mit einem eigenen Ingress. Der gemeinsame Ingress für alle anderen Absender verlangt Zugangsdaten, und ein Browser kann keine mitschicken, ohne sie jedem Besucher offenzulegen. Faro ersetzt hier Datadog RUM: Fehler, Konsolenausgaben, Web Vitals, Nutzerinteraktionen, Browser-seitige Traces. Drei Einstellungen tragen die Sicherheit, zwei davon sind in ihrer Voreinstellung unsicher: download_from_origins ist auf unsere Domains festgenagelt, weil Alloy Sourcemaps von der Herkunft holt, die die Payload nennt — der Default ["*"] macht aus einer gebastelten Exception ein SSRF-Werkzeug. Kein vom Browser geschicktes Feld darf zum Loki-Label werden, sonst sprengt eine Session-ID das Stream-Budget und Loki fängt an, die Logs der Cluster abzulehnen. Und ein Rate-Limit am Ingress ist das Einzige zwischen dem offenen Internet und Lokis Ingest-Budget.
Abgesichert wird das Gateway selbst durch eine Blackbox-Probe, die 401 als gesunden Zustand definiert. Sie prüft damit zwei Dinge gleichzeitig: dass der Endpunkt lebt, und dass die Authentifizierung noch davor hängt. Eine 200 an dieser Stelle hieße: öffentlich beschreibbar.
Mittwoch, zweiter Teil: der Test-Cluster speichert nichts
Und damit zu dem Teil, auf den ich am meisten stolz bin, weil er nichts kostet.
Ein zweiter Cluster verführt dazu, den Stack zweimal zu bauen. Zwei VictoriaMetrics, zwei Loki, zwei Tempo, zwei Grafana — und danach zwei getrennte Datenbestände, die man nicht in einem Dashboard vergleichen kann. Ich habe stattdessen die Backends ausschließlich in Produktion aufgestellt. Im Test-Cluster läuft Alloy. Sonst nichts:
const services = [
{ service: 'alloy' },
...(zone === 'prod' ? [
{ service: 'victoria-metrics' }, { service: 'loki' }, { service: 'tempo' },
{ service: 'alloy-external' }, { service: 'blackbox-exporter' }, { service: 'alloy-gateway' },
] : []),
];Der Test-Cluster ist damit schlicht ein weiterer Ingest-Client — derselbe Endpunkt, dasselbe Verfahren, dasselbe client-Attribut wie der externe Dienst. Er authentifiziert sich mit einer eigenen Zugangsberechtigung und schickt alle drei Signale über eine Destination, statt ein Protokoll je Signal zu konfigurieren:
destinations:
gateway:
type: otlp
url: https://ingest.example.at
protocol: http
metrics: { enabled: true }
logs: { enabled: true }
traces: { enabled: true }Das ist der praktische Vorteil von nativem OTel, den man erst im Betrieb spürt: Ein Transport, eine Zugangsberechtigung, eine Adresse — für Metriken, Logs und Traces gleichzeitig. Als später die Traces dazukamen, war an dieser Stelle keine Zeile zu ändern.
Das Ergebnis: ein Grafana zeigt beide Cluster. Jedes Dashboard filtert über eine cluster-Variable, jeder Alert kann beide Umgebungen in einer Regel abdecken, und ein Fehler, der in Test auftaucht, liegt neben demselben Graphen aus Produktion statt in einem zweiten Browser-Tab. Der Test-Cluster trägt dafür keinen Speicher, keine Retention, kein Backup und keine Betriebslast — und er kann jederzeit neu aufgesetzt werden, ohne dass Historie verloren geht.
Ganz umsonst war es nicht. Über OTLP eintreffende Logs kamen zunächst mit client, exporter, job und service_name an — und sonst nichts. Alles, was Produktion nativ schreibt, trägt zusätzlich cluster, namespace, pod und container. Gezielt filtern ließen sich die Test-Logs damit nicht. Die Attribute waren durchaus da, nur nicht promoted: otelcol.exporter.loki macht aus einem Attribut erst dann ein Label, wenn ein Hint es namentlich nennt. Dazu kommt, dass die Collectors k8s.namespace.name senden, nicht namespace — was k8s_namespace_name ergeben hätte, also andere Label-Namen in Test als in Produktion. Eine Transformation kopiert die k8s.*-Namen vorher auf die kurzen. Der Promotion-Hint wird übrigens am Gateway gesetzt, nicht vom Sender: Sonst könnte ein Client ein beliebig hochkardinales Attribut zum Label erklären.
Mittwoch, dritter Teil: der Agent geht
Der Datadog-Agent kostete 650m CPU und 1,45 GiB deklarierte Requests — 2,06 GiB tatsächlich — auf Nodes, die bereits eine Speicherwarnung zeigten. Mit dem Gateway, Faro und den Collectors war alles abgedeckt, was er tat. Also flog er raus.
Dazwischen der lehrreichste Fehler des Tages. Im Test-Cluster meldete ein Alert „Pod startet wiederholt neu“ für Pods, die nachweislich nicht neu gestartet waren. Die Ursache war eine Kette von Kleinigkeiten:
- kube-state-metrics hängt an rund dreißig Datenpunkten ein Label
service_namean, den Namen des Kubernetes-Service hinter einem Ingress-Pfad. - OpenTelemetry kennt ein fast gleichnamiges Feld,
service.name, das die sendende Anwendung bezeichnet. Auf dem Weg über OTLP behandelt das Helm-Chart jedesservice_nameals dieses Feld und setzt es für den ganzen Scrape, also für alle Metriken, die gemeinsam abgeholt werden. - Damit bekamen alle 8.400
kube_*-Serien den Service-Namen eines dieser dreißig Datenpunkte. Welcher es war, hing von der Verarbeitungsreihenfolge ab und wechselte bei jedem Scrape. - Für die Metrik-Datenbank ist ein anderer Label-Wert eine andere Zeitreihe. Jeder Restart-Zähler zerfiel so in mehrere Zeitreihen, die abwechselnd verschwanden und wieder auftauchten.
- Der Alert rechnet mit
increase(), also damit, wie stark ein Zähler gestiegen ist. Eine wieder auftauchende Zeitreihe liestincrease()als Zähler, der bei null neu beginnt. Ein Restart-Zähler, der unverändert auf 7 stand, sah so aus wie sieben neue Neustarts.
Am selben Tag flogen die Allowlists raus, mit denen das Chart Metriken verwirft. Gemessen: rund 29.000 Serien pro Cluster wurden weggeworfen. Darunter container_fs_usage_bytes, die PSI-Pressure-Metriken, kube_persistentvolume_*, container_memory_rss — also genau das, wonach man in einem Incident als Erstes greift. Alles zu behalten kostet etwa 26 % mehr Serien auf einem Volume, das zu 0,2 % gefüllt ist.
Das ist der Moment, in dem das neue Kostenmodell greifbar wird. Bei Datadog wäre diese Entscheidung eine Preisfrage gewesen. Hier war sie eine Frage, ob der Platz reicht.
Donnerstag: Aufräumen, und was dabei auftauchte
Der Stack lief. Was jetzt kam, war das Aufräumen — und das förderte zutage, was jahrelang unsichtbar mitgelaufen war.
Auf beiden APIs stand ein offener JMX-Port: authenticate=false, ssl=false, local.only=false auf 9999. Angelegt für einen Datadog-JMX-Check, den es seit dem Vortag nicht mehr gab. Kein Service exponierte ihn, erreichbar war er nur innerhalb des Clusters. Von dort aus konnte aber jeder beliebige Funktionen der JVM aufrufen.
Zwölf Collector-Pods liefen ohne jeden Resource-Request; der einzige Request auf jedem Pod kam vom Sidecar: 50 MiB, gegen 200–300 MiB tatsächlicher Nutzung. Der Scheduler unterschätzte den Namespace um rund ein Gigabyte, und weil das Kubelet nach „Verbrauch über Request“ evictiert, standen ausgerechnet die Collectors bei Speicherdruck ganz vorne. Wer sie verliert, verliert die Sicht darauf, was gerade passiert.
Unser WebSocket-Dienst hatte einen echten Defekt: Keiner der beiden Redis-Clients hatte einen Error-Listener, also schrieb der Client direkt und als Klartext auf stderr, am Logger vorbei. Der Dienst hatte in dreißig Tagen nichts oberhalb von info geloggt, während Redis-Ausfälle unprotokolliert blieben.
Und Loki hatte zwei Caches, die nie benutzt wurden: als Memcached-Pods deployed, als gesund gemeldet, 805 MB reserviert — und null Anfragen jemals gesehen, weil ihre Adresse aus dem Helm-Default mit cluster.local gebaut wurde. 263 GB Egress an einem einzigen Tag, weil jeder Chunk einzeln über das Netz kam. Von außen sah alles gesund aus. Aufgefallen ist es erst über den Datenverkehr.
Was jetzt anders ist
| Telemetrie | Datadog, wie wir es hatten | Selbst gehostet, heute |
|---|---|---|
| Infrastruktur-Metriken | ja | ja, ohne Allowlist — +29.000 Serien je Cluster |
| Spring-/JVM-Metriken | Bruchteil, begrenzt durch das Custom-Metric-Budget | vollständig, 8.762 Serien |
| Distributed Tracing | nie eingeschaltet — nicht leistbar | Tempo, 30 Tage |
| Logs | Opt-in pro Pod | alle Namespaces, beide Cluster, 90 Tage |
| RUM | Datadog RUM, Session Replay aus | Faro: Fehler, Web Vitals, Interaktionen, Browser-Traces |
| Externe Installationen | Agent je Standort | ein OTLP-Endpunkt, ein Name in der IaC |
| Test-Cluster | vollwertiger Agent, eigene Sicht | nur Collector, schreibt nach Produktion |
| Keycloak | keine Metriken | 1.507 Serien, die das Image ohnehin auslieferte |
| Managed Postgres | — | vollständiges DBaaS-Dashboard, 9 Alerts |
| Dashboards & Alerts | im Web-UI geklickt | 31 Dashboards, 353 Panels, 67 Regeln — in Git |
| Dead Man's Switch | — | externer Heartbeat alle 10 Minuten |
| Auftragsverarbeiter | Datadog Inc. | entfällt |
| Datenstandort | EU-Site eines US-Anbieters | Österreich, eigene Buckets |
Die Kosten
| Posten | € / Monat |
|---|---|
4. Prod-Worker (standard.large) | ~68 |
| Blockspeicher, Metrik-Volume | ~6 |
| Objektspeicher, Logs + Traces | ~1–2 |
| Externer Uptime-Check, Heartbeat | 0 (Free Tier) |
| Summe | ~75 |
Eine ehrliche Gegenüberstellung ist schwierig, und zwar aus einem aufschlussreichen Grund: Die alte Rechnung war niedriger, als der Funktionsumfang vermuten lässt — weil der Funktionsumfang von der Rechnung diktiert wurde. Kein APM, Logs nur mit Opt-in, Metriken nur in Auswahl.
Rechnet man dagegen, was der heutige Umfang zu Listenpreisen gekostet hätte, wird die Größenordnung deutlich. Allein die Indexierung: 13,9 Millionen Logzeilen pro Tag sind rund 417 Millionen Events im Monat, bei 1,70 $ je Million (15 Tage Vorhaltung, Jahresvertrag) also über 700 $ monatlich, nur für die Log-Indexierung. APM für sieben Hosts käme mit 217 $ dazu, das Custom-Metric-Kontingent wäre zwölffach überschritten.
Gegen 75 € Infrastruktur im Monat: 200.012 aktive Zeitreihen, 90 Tage Metriken, 90 Tage Logs, 30 Tage Traces, RUM ohne Sampling, zwei Cluster, ein externer Standort.
Was ich verloren habe
Es wäre unredlich, das auszulassen.
Die Oberfläche ist schlechter. Datadogs UI ist besser als Grafana, und wer täglich damit gearbeitet hat, merkt das an jedem zweiten Klick. Session Replay ist weg. Watchdog und die automatische Anomalieerkennung sind weg. Der gesamte Bereich Security Monitoring, den wir nie genutzt haben, ist weg — der zählt nicht wirklich, aber ehrlicherweise: Er stünde bereit, wenn wir ihn bräuchten.
Und der Stack läuft jetzt im Prod-Cluster, also dort, wo auch läuft, was er überwacht. Fällt der Cluster aus, bin ich blind. Zwei externe Prüfungen sagen mir noch, dass etwas kaputt ist; die Historie für die Nachbetrachtung ist verloren. Das ist eine bewusst getroffene Entscheidung für ein kleines Team, kein Versehen — und sie steht schriftlich fest, samt der Bedingung, unter der ich sie revidiere.
Was bleibt
Vier Tage, 116 Commits, 162 Dateien, gut 42.000 Zeilen. Kein Wartungsfenster, kein Ausfall, kein Datenverlust. Am Ende steht ein monitoring-Namespace, in dem jede Zeile Konfiguration reviewbar, diffbar und rückrollbar ist.
Die Lehre ist aber nicht „selbst hosten ist billiger“. Manchmal ist es das, oft ist es das nicht, und die 75 € in der Tabelle enthalten keine einzige Arbeitsstunde.
Die Lehre ist: Ein nutzungsbasiertes Preismodell ist eine Architekturentscheidung. Es legt fest, wie viel man sehen darf, und es tut das leise, verteilt über hunderte kleine Nicht-Entscheidungen — dieses Histogramm nicht, jenen Namespace nicht, Tracing erst nächstes Jahr. Man merkt das nicht auf der Rechnung. Man merkt es um drei Uhr in der Früh, wenn die Daten fehlen, die man jetzt bräuchte.
Ich habe in diesen vier Tagen mehr über unser eigenes System gelernt als in jedem vergleichbaren Zeitraum davor. Nicht weil das neue Werkzeug besser wäre als das alte — es ist es nicht — sondern weil ich es endlich überall einschalten durfte.
Ein ähnliches Projekt im Kopf?
In einem unverbindlichen Erstgespräch schauen wir uns deine Ausgangslage an und sagen dir, was realistisch ist und wie der nächste Schritt aussieht.

// über den autor
Alexander J. Gassner, MSc
Gründer und Geschäftsführer von agsolutions, seit über 15 Jahren in der Software-Entwicklung (MSc Software Engineering, FH Hagenberg). Entwickelt und betreibt geschäftskritische Software von der Anforderung bis zum Betrieb: Kotlin, Spring Boot und React im Code, Kubernetes, Pulumi und GitOps im Betrieb, als Exoscale Certified Solution Architect.


