Il est 02 h 40 et le pipeline de commandes perd des événements. L'API tourne sur EKS. Le worker qui enrichit les commandes a migré vers GKE le trimestre dernier, parce que l'équipe ML vit là-bas. Le vieux service de facturation est encore sur AKS. Vous avez trois onglets ouverts : CloudWatch Logs Insights, Cloud Logging avec son propre langage de requête, et Azure Monitor avec KQL. Trois syntaxes. Trois façons d'écrire « les quinze dernières minutes ». CloudWatch affiche les horodatages en UTC, le portail Azure dans votre heure locale, Google dans ce que dit le navigateur. Vous corrélez à l'œil, vous copiez un identifiant de requête d'un onglet dans la barre de recherche du suivant, et vous vous trompez deux fois avant de tomber juste.
Personne n'a planifié ça. Chaque équipe a pris le logging que son cloud lui offrait, parce qu'il était là et préconfiguré, et chaque décision était localement correcte. L'incident, c'est la facture de toutes ces décisions qui arrive d'un coup.
L'instinct, c'est de régler ça avec de l'outillage : acheter quelque chose qui interroge les trois, écrire un script qui tire depuis trois API. Cela traite le symptôme. Le problème est architectural : vous avez laissé le cloud posséder vos logs. L'application a écrit sur stdout, la plateforme l'a ramassé, et à partir de cet instant le format, le langage de requête, la rétention et le prix ont été décidés par le fournisseur. Les logs sont une sortie de votre application. Là où ils finissent devrait être votre décision, prise une seule fois, et elle devrait survivre au prochain changement d'infrastructure.
Comment vous en êtes arrivé là
Chaque cloud vous donne du logging gratuitement, au sens où il n'y a rien à installer. Un conteneur sur EKS écrit sur stdout et, avec un add-on, les lignes apparaissent dans CloudWatch. Sur GKE l'agent est déjà dans l'image du nœud. Sur AKS vous cochez « Container Insights ». C'est le chemin de moindre résistance, et sur le premier cloud c'est vraiment le bon choix : vous avez quelque chose qui marche en un après-midi et vous passez au produit.
Le coût caché apparaît à trois moments. Le deuxième cloud, quand vous découvrez que rien de ce que vous avez construit sur le premier ne se transfère : ni les requêtes, ni les tableaux de bord, ni les alertes, ni les réflexes. La migration, quand un workload bouge et que ses six mois d'historique restent derrière, dans un stockage que vous continuez de payer. Et la facture d'ingestion, qui grandit avec votre trafic plutôt qu'avec votre effectif d'ingénieurs et qui a tendance à devenir la troisième ligne de la facture avant que quiconque ne s'en aperçoive.
| CloudWatch Logs | Cloud Logging | Azure Monitor Logs | |
|---|---|---|---|
| Langage de requête | Logs Insights (syntaxe propre) | Logging query language | KQL |
| Ingestion (prix catalogue) | 0,50 $ / Go | 0,50 $ / Gio, les 50 premiers Gio gratuits | 2,76 $ / Go niveau Analytics, 0,65 $ / Go niveau Basic |
| Rétention | 0,03 $ / Go-mois | 30 jours inclus | 31 jours inclus |
| Export | Subscription filter vers Kinesis, Firehose ou Lambda ; export batch vers S3 | Log sinks vers Pub/Sub, Cloud Storage, BigQuery | Diagnostic settings vers Event Hubs ou Storage ; Data Export |
Trois services, trois langages de requête, trois modèles de prix et trois réponses différentes à « comment je sors mes logs d'ici ». Aucun n'est mauvais. Ils ne sont juste pas à vous.
Le principe : séparer l'émission du stockage
La solution est une frontière. Découpez le logging en trois couches et donnez à chacune un contrat que les autres n'ont pas le droit de casser.
L'application émet du JSON structuré avec les conventions sémantiques OpenTelemetry. Elle connaît son propre nom, service.name, et le trace_id de la requête qu'elle traite. Elle ne sait pas, et ne doit pas se soucier, dans quel cloud elle tourne. Pas de SDK pour CloudWatch, pas de bibliothèque cliente pour Cloud Logging. Stdout, ou OTLP vers localhost, et rien d'autre.
L'agent tourne à raison d'un par hôte ou par cluster : un OpenTelemetry Collector ou Grafana Alloy. C'est le seul composant autorisé à connaître le cloud. Il découvre cloud.provider, cloud.region et le nom du cluster via l'endpoint de métadonnées, les estampille sur chaque enregistrement, regroupe, compresse et expédie. Si vous changez de cloud, c'est la seule chose qui change, et comme on le verra plus bas, elle ne change même pas beaucoup.
Le backend est neutre : Loki pour le stockage, Grafana par-dessus. Il parle OTLP en entrée et LogQL en sortie. Il n'a aucune idée si une ligne de log vient de Virginie, de Francfort ou d'un ordinateur portable.
Le contrat entre les couches, c'est OTLP et une poignée d'attributs de ressource. C'est tout. N'importe laquelle des trois couches peut être remplacée sans que les deux autres s'en aperçoivent.
L'exemple concret : un service, trois clusters
Le même service api tourne sur EKS, GKE et AKS. Ci-dessous, la configuration du collecteur déployée sur les trois en DaemonSet. C'est un seul fichier. Il n'y a aucun if cloud == aws nulle part dedans.
# otel-collector.yaml — identique sur EKS, GKE et 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 # parse les formats de ligne CRI-O / containerd / docker
processors:
resourcedetection:
detectors: [env, eks, gcp, aks, azure, ec2, system]
timeout: 5s
override: false # ne jamais écraser ce que l'app a déjà défini
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]
La ligne intéressante, c'est detectors. Le collecteur essaie chaque détecteur dans l'ordre ; ceux qui ne s'appliquent pas échouent en silence. Sur EKS, le détecteur eks fixe cloud.provider=aws, cloud.platform=aws_eks et le nom du cluster. Sur GKE, le détecteur gcp fixe cloud.provider=gcp, cloud.platform=gcp_kubernetes_engine et cloud.region. Sur AKS, le détecteur aks fixe cloud.provider=azure et cloud.platform=azure_aks. L'application n'a jamais touché à aucune de ces valeurs. Même image, même manifeste, trois réponses différentes, toutes correctes.
Côté réception, Loki 3 ingère l'OTLP nativement et transforme les attributs de ressource soit en labels indexés, soit en métadonnées structurées. Par défaut, service.name, k8s.namespace.name et cloud.region deviennent des labels. Nous ajoutons cloud.provider à cette liste, parce que « quel cloud » est la première chose par laquelle on groupe pendant un incident :
# loki.yaml (extrait)
limits_config:
otlp_config:
resource_attributes:
attributes_config:
- action: index_label
attributes: [cloud.provider, cloud.platform]
Tout le reste attaché par le collecteur, comme le nom du pod ou le déploiement, atterrit dans les métadonnées structurées : interrogeable, mais pas indexé, pour que la cardinalité des labels reste plate.
Maintenant, les requêtes. Toutes les erreurs du service api, sur les trois clouds, en une ligne :
{service_name="api"} |= "error"
La même chose, sur GCP seulement :
{service_name="api", cloud_provider="gcp"} |= "error"
Le taux d'erreurs par cloud sur les cinq dernières minutes, c'est-à-dire le panneau que vous voulez vraiment sur le tableau de bord d'incident :
sum by (cloud_provider) (
rate({service_name="api"} | json | level="error" [5m])
)
Et la requête que nous faisions à l'œil à 02 h 40, en suivant une seule requête à travers les trois clusters :
{service_name=~"api|order-worker|billing"} | json | trace_id="4bf92f3577b34da6a3ce929d0e0e4736"
Ce que Grafana renvoie pour la dernière, condensé :
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
Quatre lignes, trois clouds, un seul format d'horodatage, un seul fuseau, triées. Le service de facturation sur AKS réinitialise les connexions et le worker sur GKE tombe en timeout à cause de lui. Ça a pris onze secondes à trouver au lieu de quarante minutes, et rien dans l'application n'a changé pour rendre cela possible.
Pour le développement local et pour un premier essai, voici tout le backend :
# docker-compose.yml — Loki + Grafana, OTLP en entrée sur :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: {}
Pointez l'exporteur du collecteur vers http://localhost:3100/otlp, ouvrez Grafana sur le port 3000, et les requêtes ci-dessus fonctionnent sans modification. C'est tout l'intérêt : l'ordinateur du développeur n'est qu'une valeur de plus de cloud_provider.
La vraie objection : l'egress
Quelqu'un la soulèvera en revue de conception, et il aura raison : des logs qui quittent un cloud coûtent de l'argent. Chaque fournisseur facture les octets qui partent vers Internet ou vers un autre fournisseur, environ 0,09 à 0,12 $ par Go. À 100 Go par jour de logs bruts, cela fait jusqu'à 360 $ par mois et par cloud, et on a l'impression de payer pour partir.
Il y a trois réponses honnêtes, chacune avec un compromis.
Compresser et échantillonner à l'agent. Les logs JSON se compressent cinq à dix fois avec zstd, et le collecteur le fait avant que quoi que ce soit ne traverse le réseau. Ajoutez une politique d'échantillonnage qui garde toutes les erreurs, toutes les lignes portant une trace échantillonnée et une ligne de debug sur dix, et vos 100 Go deviennent 10 à 20 Go. L'egress tombe à 15–40 $ par mois. Le compromis : vous n'avez plus chaque ligne et vous devez décider à l'avance de celles dont vous n'avez pas besoin. Pour la plupart des équipes, c'est la bonne réponse et la seule dont elles auront jamais besoin.
Un Loki par cloud, Grafana fédéré. Faites tourner un petit Loki dans chaque cloud, qui écrit dans le stockage objet de ce cloud, et ajoutez chacun comme source de données dans un seul Grafana. Un panneau à sources mixtes interroge les trois et fusionne le résultat. L'egress est nul : seuls les résultats de requête sortent, et ce sont des kilooctets. Le compromis : trois déploiements Loki à opérer, et les requêtes inter-clouds sont une fusion dans Grafana plutôt qu'un index unique, donc « tout trier par date » fonctionne, mais « joindre sur trace_id entre clouds » est plus lent. Choisissez cette option quand les règles de résidence des données gardent de toute façon les logs dans la région.
Centraliser sur le cloud dominant. Si 80 % de vos workloads sont sur AWS, mettez Loki là-bas et payez l'egress sur les 20 %. Un index, un stockage, un seul chemin de requête, le plus simple à opérer. Le compromis : c'est l'option la plus chère, et elle rend discrètement un cloud plus égal que les autres, ce que vous essayiez justement d'éviter.
Pour 100 Go par jour répartis également entre les trois clouds, la facture d'egress pour les trois options, au prix catalogue et sans engagement :
| Option | Octets sortants par mois | Coût d'egress |
|---|---|---|
| Brut, centralisé | ~2 To (deux clouds sur trois) | ~180–240 $ |
| Compressé et échantillonné, centralisé | ~200–400 Go | ~20–45 $ |
| Un Loki par cloud, requêtes fédérées | résultats de requête seulement | ~0 $ |
Ce qui reste natif, et c'est très bien
Cet article parle des logs applicatifs. Votre cloud produit aussi des logs de plan de contrôle : CloudTrail, VPC Flow Logs, Azure Activity Log, GCP Audit Logs. Laissez-les où ils sont. Ils sont générés par la plateforme, ils sont le plus utiles avec l'outillage de la plateforme (GuardDuty, Defender, Security Command Center), et les déplacer ne vous apporte rien, sauf si un auditeur exige une archive unique. Si ce jour arrive, exportez-les via un sink ou un subscription filter dans le même stockage objet et interrogez-les aussi depuis Grafana. D'ici là, c'est une distraction.
La ligne est simple : si votre code l'a émis, ça passe par le collecteur et ça ne connaît pas le cloud. Si le cloud l'a émis, le cloud peut le garder.
Le coût
Les chiffres qui font l'argument. Hypothèses : 100 Go par jour, 30 jours de rétention, prix catalogue de septembre 2026, aucune remise entreprise, environ 3 To ingérés par mois.
| Backend | Ingestion | Rétention (30 jours) | Total mensuel |
|---|---|---|---|
| CloudWatch Logs, classe Standard | 1 500 $ | ~90 $ | ~1 600 $ plus 0,005 $ / Go scanné par les requêtes |
| Google Cloud Logging | ~1 475 $ (50 Gio gratuits) | incluse | ~1 475 $ |
| Azure Monitor, niveau Analytics | 8 280 $ | incluse (31 jours) | ~8 300 $ |
| Azure Monitor, niveau Basic | 1 950 $ | incluse | ~1 950 $, avec un KQL restreint |
| Loki sur stockage objet, 3 nœuds | 0 $ | ~10–15 $ (300–400 Go compressés) | ~350–500 $ calcul et stockage |
La ligne de Loki est dominée par les trois instances qui le font tourner, et celles-ci ne grandissent pas avec votre volume de logs tant que vous n'êtes pas bien au-delà d'un téraoctet par jour. Les lignes des services managés grandissent linéairement avec chaque octet. À 10 Go par jour, la différence est une erreur d'arrondi et vous devriez prendre l'option native et vous occuper de votre produit. À 100 Go par jour, elle paie un ingénieur. À un téraoctet par jour, elle paie l'équipe.
Un astérisque : vous opérez maintenant Loki. C'est un coût réel, en heures, absent de ce tableau. Il est petit, Loki est un seul binaire qui lit et écrit du stockage objet, mais il n'est pas nul, et si personne dans l'équipe ne veut en être responsable, le Loki hébergé de Grafana Cloud se situe entre les deux colonnes en prix et vous enlève l'exploitation.
Alternatives, brièvement
Loki n'est pas le seul backend neutre, et l'architecture se moque de celui que vous choisissez tant qu'il ingère de l'OTLP.
VictoriaLogs est plus léger que Loki, n'a aucune angoisse de cardinalité, et son langage de requête est plus simple. Choisissez-le si vous avez une petite équipe et beaucoup de champs à forte cardinalité comme des identifiants utilisateur. OpenSearch vous donne une vraie recherche plein texte et un écosystème énorme, au prix d'un cluster qui se soucie du heap et des shards. Choisissez-le si vos recherches sont vraiment des recherches de texte, pas des consultations de labels. SigNoz regroupe logs, traces et métriques dans un seul produit sur ClickHouse avec sa propre interface. Choisissez-le si vous voulez une seule chose à installer et que vous n'avez pas déjà Grafana.
Quel que soit votre choix, la configuration du collecteur ci-dessus ne change pas. Seul l'endpoint de l'exporteur change.
Conclusion
Des logs qui ne savent pas où ils tournent sont des logs qui survivent à la prochaine décision d'infrastructure. Déplacez un service d'EKS vers GKE et son historique ne reste pas derrière. Ajoutez un quatrième cloud, ou un cluster bare-metal chez un client, et il apparaît comme une nouvelle valeur de cloud_provider dans une requête que vous avez déjà. Renégociez avec un fournisseur sans que votre observabilité soit otage de la conversation.
Nous ne sommes pas arrivés là à partir d'une présentation. Notre Agent Factory fait tourner des agents IA dans les environnements des clients, et les clients ont les clouds qu'ils ont. L'un est entièrement sur AWS, un autre est sur Azure à cause d'un accord avec Microsoft, un autre fait tourner Kubernetes sur site et ne laisse aucune télémétrie sortir du bâtiment. Si les logs de la plateforme dépendaient du cloud de la plateforme, nous maintiendrions trois stacks d'observabilité et nous corrélerions à l'œil à 02 h 40. À la place, chaque agent émet de l'OTLP, un collecteur par cluster y estampille le cloud, et nous avons une seule requête pour « montre-moi chaque exécution d'agent en échec de la dernière heure » qui fonctionne partout.
Le cloud est un détail d'infrastructure. Traitez-le comme tel. Si vous voulez de l'aide pour tracer cette frontière dans votre propre stack, parlons-en.