Es ist 02:40 Uhr und die Bestell-Pipeline verliert Events. Die API läuft auf EKS. Der Worker, der Bestellungen anreichert, ist letztes Quartal nach GKE umgezogen, weil das ML-Team dort zu Hause ist. Der alte Abrechnungsdienst liegt noch auf AKS. Sie haben drei Browser-Tabs offen: CloudWatch Logs Insights, Cloud Logging mit seiner eigenen Abfragesprache und Azure Monitor mit KQL. Drei Syntaxen. Drei Schreibweisen für „die letzten fünfzehn Minuten“. CloudWatch zeigt Zeitstempel in UTC, das Azure-Portal in Ihrer Ortszeit, Google in dem, was der Browser sagt. Sie korrelieren mit dem Auge, kopieren eine Request-ID aus einem Tab in das Suchfeld des nächsten und liegen zweimal daneben, bevor Sie richtig liegen.
Niemand hat das geplant. Jedes Team hat das Logging genommen, das seine Cloud angeboten hat, weil es da war und vorkonfiguriert war, und jede Entscheidung war für sich genommen richtig. Der Vorfall ist die Rechnung für all diese Entscheidungen, die auf einmal eintrifft.
Der Instinkt sagt: mit Tooling lösen. Etwas kaufen, das alle drei abfragt, ein Skript schreiben, das aus drei APIs zieht. Das behandelt das Symptom. Das Problem ist architektonisch: Sie haben der Cloud erlaubt, Ihre Logs zu besitzen. Die Anwendung hat nach stdout geschrieben, die Plattform hat es aufgesammelt, und ab diesem Moment wurden Format, Abfragesprache, Aufbewahrung und Preis vom Anbieter entschieden. Logs sind ein Output Ihrer Anwendung. Wo sie landen, sollte Ihre Entscheidung sein, einmal getroffen, und sie sollte die nächste Infrastrukturänderung überleben.
Wie Sie hierher gekommen sind
Jede Cloud gibt Ihnen Logging umsonst, in dem Sinne, dass nichts zu installieren ist. Ein Container auf EKS schreibt nach stdout, und mit einem Add-on erscheinen die Zeilen in CloudWatch. Auf GKE steckt der Agent bereits im Node-Image. Auf AKS setzen Sie ein Häkchen bei „Container Insights“. Es ist der Weg des geringsten Widerstands, und bei der ersten Cloud ist es tatsächlich die richtige Entscheidung: Sie haben an einem Nachmittag etwas Funktionierendes und wenden sich dem Produkt zu.
Die versteckten Kosten zeigen sich in drei Momenten. Bei der zweiten Cloud, wenn Sie feststellen, dass nichts von dem, was Sie auf der ersten gebaut haben, übertragbar ist: nicht die Queries, nicht die Dashboards, nicht die Alerts, nicht das Muskelgedächtnis. Bei der Migration, wenn ein Workload umzieht und seine sechs Monate Historie in einem Speicher zurückbleiben, für den Sie weiter bezahlen. Und bei der Ingestion-Rechnung, die mit Ihrem Traffic wächst statt mit Ihrer Engineering-Mannschaft und die dazu neigt, der drittgrößte Posten auf der Rechnung zu werden, bevor es jemand bemerkt.
| CloudWatch Logs | Cloud Logging | Azure Monitor Logs | |
|---|---|---|---|
| Abfragesprache | Logs Insights (eigene Syntax) | Logging query language | KQL |
| Ingestion (Listenpreis) | $0,50 / GB | $0,50 / GiB, erste 50 GiB kostenlos | $2,76 / GB Analytics-Stufe, $0,65 / GB Basic-Stufe |
| Aufbewahrung | $0,03 / GB-Monat | 30 Tage inklusive | 31 Tage inklusive |
| Export | Subscription Filter nach Kinesis, Firehose oder Lambda; Batch-Export nach S3 | Log Sinks nach Pub/Sub, Cloud Storage, BigQuery | Diagnostic Settings nach Event Hubs oder Storage; Data Export |
Drei Dienste, drei Abfragesprachen, drei Preismodelle und drei verschiedene Antworten auf „wie bekomme ich meine Logs hier raus“. Keiner davon ist schlecht. Sie gehören nur nicht Ihnen.
Das Prinzip: Emittieren von Speichern trennen
Die Lösung ist eine Grenze. Teilen Sie das Logging in drei Schichten und geben Sie jeder einen Vertrag, den die anderen nicht brechen dürfen.
Die Anwendung emittiert strukturiertes JSON mit den semantischen Konventionen von OpenTelemetry. Sie kennt ihren eigenen Namen, service.name, und die trace_id des Requests, den sie gerade bearbeitet. Sie weiß nicht, und es darf sie nicht kümmern, in welcher Cloud sie läuft. Kein SDK für CloudWatch, keine Client-Bibliothek für Cloud Logging. Stdout, oder OTLP nach localhost, und sonst nichts.
Der Agent läuft einmal pro Host oder Cluster: ein OpenTelemetry Collector oder Grafana Alloy. Das ist die einzige Komponente, die von der Cloud wissen darf. Sie ermittelt cloud.provider, cloud.region und den Clusternamen über den Metadata-Endpunkt, stempelt sie auf jeden Datensatz, bündelt, komprimiert und verschickt. Wenn Sie die Cloud wechseln, ist das das Einzige, was sich ändert, und wie wir unten sehen werden, ändert es sich nicht einmal viel.
Das Backend ist neutral: Loki für die Speicherung, Grafana obendrauf. Es spricht OTLP hinein und LogQL heraus. Es hat keine Ahnung, ob eine Logzeile aus Virginia, Frankfurt oder von einem Laptop kam.
Der Vertrag zwischen den Schichten ist OTLP plus eine Handvoll Resource-Attribute. Das ist alles. Jede der drei Schichten kann ausgetauscht werden, ohne dass die anderen beiden es bemerken.
Das konkrete Beispiel: ein Dienst, drei Cluster
Derselbe api-Dienst läuft auf EKS, GKE und AKS. Unten steht die Collector-Konfiguration, die auf allen dreien als DaemonSet ausgerollt ist. Es ist eine Datei. Nirgendwo darin steht ein if cloud == aws.
# otel-collector.yaml — identisch auf EKS, GKE und AKS
receivers:
otlp:
protocols:
grpc: { endpoint: 0.0.0.0:4317 }
http: { endpoint: 0.0.0.0:4318 }
filelog:
include: [/var/log/pods/*/*/*.log]
operators:
- type: container # parst CRI-O- / containerd- / docker-Zeilenformate
processors:
resourcedetection:
detectors: [env, eks, gcp, aks, azure, ec2, system]
timeout: 5s
override: false # nie überschreiben, was die App schon gesetzt hat
k8sattributes:
extract:
metadata: [k8s.namespace.name, k8s.deployment.name, k8s.pod.name]
batch:
timeout: 5s
send_batch_size: 4096
exporters:
otlphttp/loki:
endpoint: https://loki.observability.internal/otlp
compression: zstd
headers:
X-Scope-OrgID: platform
service:
pipelines:
logs:
receivers: [otlp, filelog]
processors: [resourcedetection, k8sattributes, batch]
exporters: [otlphttp/loki]
Die interessante Zeile ist detectors. Der Collector probiert jeden Detektor der Reihe nach; die, die nicht zutreffen, scheitern leise. Auf EKS setzt der eks-Detektor cloud.provider=aws, cloud.platform=aws_eks und den Clusternamen. Auf GKE setzt der gcp-Detektor cloud.provider=gcp, cloud.platform=gcp_kubernetes_engine und cloud.region. Auf AKS setzt der aks-Detektor cloud.provider=azure und cloud.platform=azure_aks. Die Anwendung hat keinen dieser Werte je angefasst. Dasselbe Image, dasselbe Manifest, drei verschiedene Antworten, alle richtig.
Auf der Empfängerseite nimmt Loki 3 OTLP nativ entgegen und macht aus Resource-Attributen entweder Index-Labels oder Structured Metadata. Standardmäßig werden service.name, k8s.namespace.name und cloud.region zu Labels. Wir fügen cloud.provider zu dieser Liste hinzu, weil „welche Cloud“ das Erste ist, wonach wir bei einem Vorfall gruppieren:
# loki.yaml (Auszug)
limits_config:
otlp_config:
resource_attributes:
attributes_config:
- action: index_label
attributes: [cloud.provider, cloud.platform]
Alles andere, was der Collector angehängt hat, etwa der Pod-Name oder das Deployment, landet in Structured Metadata: abfragbar, aber nicht indiziert, sodass die Label-Kardinalität flach bleibt.
Nun die Queries. Alle Fehler des api-Dienstes, über alle drei Clouds, in einer Zeile:
{service_name="api"} |= "error"
Dasselbe, nur auf GCP:
{service_name="api", cloud_provider="gcp"} |= "error"
Fehlerrate pro Cloud über die letzten fünf Minuten, also das Panel, das Sie auf dem Incident-Dashboard wirklich wollen:
sum by (cloud_provider) (
rate({service_name="api"} | json | level="error" [5m])
)
Und die Query, die wir um 02:40 Uhr mit dem Auge gemacht haben, einem einzelnen Request über alle drei Cluster hinweg folgend:
{service_name=~"api|order-worker|billing"} | json | trace_id="4bf92f3577b34da6a3ce929d0e0e4736"
Was Grafana für die letzte zurückgibt, verdichtet:
2026-09-03 02:41:07.113 cloud_provider=aws service_name=api POST /orders 202 trace_id=4bf9…4736
2026-09-03 02:41:07.201 cloud_provider=gcp service_name=order-worker enrich start order_id=88231 trace_id=4bf9…4736
2026-09-03 02:41:09.870 cloud_provider=gcp service_name=order-worker level=error upstream timeout billing trace_id=4bf9…4736
2026-09-03 02:41:09.871 cloud_provider=azure service_name=billing level=error connection reset by peer trace_id=4bf9…4736
Vier Zeilen, drei Clouds, ein Zeitstempelformat, eine Zeitzone, sortiert. Der Abrechnungsdienst auf AKS setzt Verbindungen zurück, und der Worker auf GKE läuft deswegen in Timeouts. Das zu finden hat elf Sekunden gedauert statt vierzig Minuten, und an der Anwendung hat sich nichts geändert, um das möglich zu machen.
Für die lokale Entwicklung und für einen ersten Versuch ist das das ganze Backend:
# docker-compose.yml — Loki + Grafana, OTLP-Eingang auf :3100/otlp
services:
loki:
image: grafana/loki:3.5
command: -config.file=/etc/loki/loki.yaml
ports: ["3100:3100"]
volumes:
- ./loki.yaml:/etc/loki/loki.yaml:ro
- loki-data:/loki
grafana:
image: grafana/grafana:12.1
ports: ["3000:3000"]
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
volumes:
- ./grafana-datasource.yaml:/etc/grafana/provisioning/datasources/loki.yaml:ro
depends_on: [loki]
volumes:
loki-data: {}
Richten Sie den Exporter des Collectors auf http://localhost:3100/otlp, öffnen Sie Grafana auf Port 3000, und die Queries oben funktionieren unverändert. Das ist der Punkt: der Laptop des Entwicklers ist einfach ein weiterer Wert von cloud_provider.
Der echte Einwand: Egress
Jemand wird ihn im Design-Review vorbringen, und er wird recht haben: Logs, die eine Cloud verlassen, kosten Geld. Jeder Anbieter berechnet Bytes, die ins Internet oder zu einem anderen Anbieter gehen, grob $0,09 bis $0,12 pro GB. Bei 100 GB Rohlogs pro Tag sind das bis zu $360 im Monat pro Cloud, und es fühlt sich an, als würde man fürs Gehen bezahlen.
Es gibt drei ehrliche Antworten, jede mit einem Trade-off.
Am Agenten komprimieren und samplen. JSON-Logs komprimieren mit zstd fünf- bis zehnfach, und der Collector tut das, bevor irgendetwas über die Leitung geht. Ergänzen Sie eine Sampling-Policy, die alle Fehler, alle Zeilen mit einem gesampelten Trace und jede zehnte Debug-Zeile behält, und aus Ihren 100 GB werden 10 bis 20 GB. Egress fällt auf $15–40 im Monat. Der Trade-off: Sie haben nicht mehr jede Zeile und müssen vorab entscheiden, welche Sie nicht brauchen. Für die meisten Teams ist das die richtige Antwort und die einzige, die sie je brauchen werden.
Loki pro Cloud, föderiertes Grafana. Betreiben Sie ein kleines Loki in jeder Cloud, das in den Object Storage dieser Cloud schreibt, und fügen Sie jedes als Data Source in einem einzigen Grafana hinzu. Ein Mixed-Data-Source-Panel fragt alle drei ab und führt das Ergebnis zusammen. Egress ist null: nur Query-Ergebnisse verlassen die Cloud, und das sind Kilobytes. Der Trade-off: drei Loki-Deployments zu betreiben, und Cloud-übergreifende Queries sind ein Merge in Grafana statt ein einziger Index, sodass „alles nach Zeit sortieren“ funktioniert, aber „Join auf trace_id über Clouds hinweg“ langsamer ist. Wählen Sie das, wenn Datenresidenz-Regeln die Logs ohnehin in der Region halten.
Auf der dominanten Cloud zentralisieren. Wenn 80 % Ihrer Workloads auf AWS liegen, stellen Sie Loki dorthin und zahlen Egress für die 20 %. Ein Index, ein Speicher, ein Abfragepfad, am einfachsten zu betreiben. Der Trade-off: es ist die teuerste Option, und sie macht stillschweigend eine Cloud gleicher als die anderen, was Sie ja gerade vermeiden wollten.
Für 100 GB pro Tag, gleichmäßig auf die drei Clouds verteilt, die Egress-Rechnung für die drei Optionen, zum Listenpreis und ohne Commitments:
| Option | Bytes, die pro Monat abfließen | Egress-Kosten |
|---|---|---|
| Roh, zentralisiert | ~2 TB (zwei von drei Clouds) | ~$180–240 |
| Komprimiert und gesampelt, zentralisiert | ~200–400 GB | ~$20–45 |
| Loki pro Cloud, föderierte Queries | nur Query-Ergebnisse | ~$0 |
Was nativ bleibt, und das ist in Ordnung
Dieser Artikel handelt von Anwendungslogs. Ihre Cloud produziert auch Control-Plane-Logs: CloudTrail, VPC Flow Logs, Azure Activity Log, GCP Audit Logs. Lassen Sie die, wo sie sind. Sie werden von der Plattform erzeugt, sie sind mit dem Tooling der Plattform am nützlichsten (GuardDuty, Defender, Security Command Center), und sie zu verschieben bringt Ihnen nichts, es sei denn, ein Auditor verlangt ein einziges Archiv. Wenn dieser Tag kommt, exportieren Sie sie per Sink oder Subscription Filter in denselben Object Storage und fragen sie ebenfalls aus Grafana ab. Bis dahin ist es eine Ablenkung.
Die Linie ist einfach: Wenn Ihr Code es emittiert hat, geht es durch den Collector und weiß nichts von der Cloud. Wenn die Cloud es emittiert hat, darf die Cloud es behalten.
Die Kosten
Die Zahlen, die das Argument machen. Annahmen: 100 GB pro Tag, 30 Tage Aufbewahrung, Listenpreise im September 2026, keine Enterprise-Rabatte, grob 3 TB Ingestion pro Monat.
| Backend | Ingestion | Aufbewahrung (30 Tage) | Monatssumme |
|---|---|---|---|
| CloudWatch Logs, Standard-Klasse | $1.500 | ~$90 | ~$1.600 plus $0,005 / GB, die Queries scannen |
| Google Cloud Logging | ~$1.475 (50 GiB kostenlos) | inklusive | ~$1.475 |
| Azure Monitor, Analytics-Stufe | $8.280 | inklusive (31 Tage) | ~$8.300 |
| Azure Monitor, Basic-Stufe | $1.950 | inklusive | ~$1.950, mit eingeschränktem KQL |
| Loki auf Object Storage, 3 Knoten | $0 | ~$10–15 (300–400 GB komprimiert) | ~$350–500 Compute und Speicher |
Lokis Zeile wird von den drei Instanzen dominiert, die es betreiben, und die wachsen nicht mit Ihrem Logvolumen, bis Sie deutlich über einem Terabyte pro Tag liegen. Die Zeilen der Managed Services wachsen linear mit jedem Byte. Bei 10 GB pro Tag ist der Unterschied ein Rundungsfehler, und Sie sollten die native Option nehmen und sich um Ihr Produkt kümmern. Bei 100 GB pro Tag bezahlt er einen Engineer. Bei einem Terabyte pro Tag bezahlt er das Team.
Ein Sternchen: Sie betreiben jetzt Loki. Das sind echte Kosten, in Stunden, nicht in dieser Tabelle. Es sind kleine, Loki ist ein einzelnes Binary, das Object Storage liest und schreibt, aber sie sind nicht null, und wenn niemand im Team es besitzen will, liegt das gehostete Loki von Grafana Cloud preislich zwischen den beiden Spalten und nimmt Ihnen den Betrieb ab.
Alternativen, kurz
Loki ist nicht das einzige neutrale Backend, und der Architektur ist egal, welches Sie wählen, solange es OTLP entgegennimmt.
VictoriaLogs ist leichter als Loki, kennt keine Kardinalitätsangst, und seine Abfragesprache ist einfacher. Wählen Sie es bei einem kleinen Team und vielen hochkardinalen Feldern wie User-IDs. OpenSearch gibt Ihnen echte Volltextsuche und ein riesiges Ökosystem, zum Preis eines Clusters, der sich um Heap und Shards kümmert. Wählen Sie es, wenn Ihre Suchen wirklich Textsuchen sind und keine Label-Lookups. SigNoz bündelt Logs, Traces und Metriken in einem Produkt auf ClickHouse-Basis mit eigener Oberfläche. Wählen Sie es, wenn Sie eine einzige Sache installieren wollen und noch kein Grafana haben.
Was immer Sie wählen, die Collector-Konfiguration oben ändert sich nicht. Nur der Exporter-Endpunkt.
Fazit
Logs, die nicht wissen, wo sie laufen, sind Logs, die die nächste Infrastrukturentscheidung überleben. Verschieben Sie einen Dienst von EKS nach GKE, und seine Historie bleibt nicht zurück. Fügen Sie eine vierte Cloud hinzu, oder einen Bare-Metal-Cluster beim Kunden vor Ort, und er erscheint als neuer Wert von cloud_provider in einer Query, die Sie schon haben. Verhandeln Sie mit einem Anbieter neu, ohne dass Ihre Observability in dem Gespräch zur Geisel wird.
Wir sind nicht über ein Foliendeck hierher gekommen. Unsere Agent Factory betreibt KI-Agenten in den Umgebungen von Kunden, und Kunden haben die Clouds, die sie haben. Einer ist komplett auf AWS, einer ist wegen eines Microsoft-Vertrags auf Azure, einer betreibt Kubernetes on-premises und lässt keine Telemetrie das Gebäude verlassen. Hingen die Logs der Plattform von der Cloud der Plattform ab, würden wir drei Observability-Stacks pflegen und um 02:40 Uhr mit dem Auge korrelieren. Stattdessen emittiert jeder Agent OTLP, ein Collector pro Cluster stempelt die Cloud darauf, und wir haben eine einzige Query für „zeig mir jeden fehlgeschlagenen Agentenlauf der letzten Stunde“, die überall funktioniert.
Die Cloud ist ein Infrastrukturdetail. Behandeln Sie sie als eines. Wenn Sie Hilfe dabei möchten, diese Grenze in Ihrem eigenen Stack zu ziehen, sprechen Sie mit uns.