# Vos logs ne devraient pas savoir dans quel cloud ils tournent

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.

<div class="article-figure">
<svg viewBox="0 0 900 380" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Trois clusters, EKS sur AWS, GKE sur GCP et AKS sur Azure, chacun exécutant le même pod api qui émet du JSON avec service.name et trace_id, et le même DaemonSet OpenTelemetry Collector dont le processeur resourcedetection fixe cloud.provider à aws, gcp ou azure. Les trois collecteurs envoient de l'OTLP sur HTTP avec compression zstd vers un seul Loki, que Grafana interroge avec une seule expression LogQL.">
<defs><marker id="arrL1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#4fffb0"/></marker></defs>
<g font-family="Inter,system-ui,sans-serif">
<rect x="30" y="20" width="240" height="190" rx="14" fill="#151b2e" stroke="#2a3150" stroke-width="1.5"/>
<text x="150" y="46" text-anchor="middle" fill="#ffd166" font-size="14" font-weight="700">AWS · EKS</text>
<rect x="50" y="62" width="200" height="50" rx="8" fill="#0d1120" stroke="#7b8cff" stroke-width="1.5"/>
<text x="150" y="82" text-anchor="middle" fill="#f1f3ff" font-size="12">pod api</text>
<text x="150" y="100" text-anchor="middle" fill="#9aa3c7" font-size="11">JSON + service.name + trace_id</text>
<line x1="150" y1="112" x2="150" y2="130" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrL1)"/>
<rect x="50" y="132" width="200" height="62" rx="8" fill="#0d1120" stroke="#4fffb0" stroke-width="1.5"/>
<text x="150" y="152" text-anchor="middle" fill="#f1f3ff" font-size="12">OTel Collector (DaemonSet)</text>
<text x="150" y="170" text-anchor="middle" fill="#9aa3c7" font-size="11">resourcedetection →</text>
<text x="150" y="186" text-anchor="middle" fill="#4fffb0" font-size="11">cloud.provider=aws</text>
<rect x="330" y="20" width="240" height="190" rx="14" fill="#151b2e" stroke="#2a3150" stroke-width="1.5"/>
<text x="450" y="46" text-anchor="middle" fill="#ffd166" font-size="14" font-weight="700">GCP · GKE</text>
<rect x="350" y="62" width="200" height="50" rx="8" fill="#0d1120" stroke="#7b8cff" stroke-width="1.5"/>
<text x="450" y="82" text-anchor="middle" fill="#f1f3ff" font-size="12">pod api</text>
<text x="450" y="100" text-anchor="middle" fill="#9aa3c7" font-size="11">JSON + service.name + trace_id</text>
<line x1="450" y1="112" x2="450" y2="130" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrL1)"/>
<rect x="350" y="132" width="200" height="62" rx="8" fill="#0d1120" stroke="#4fffb0" stroke-width="1.5"/>
<text x="450" y="152" text-anchor="middle" fill="#f1f3ff" font-size="12">OTel Collector (DaemonSet)</text>
<text x="450" y="170" text-anchor="middle" fill="#9aa3c7" font-size="11">resourcedetection →</text>
<text x="450" y="186" text-anchor="middle" fill="#4fffb0" font-size="11">cloud.provider=gcp</text>
<rect x="630" y="20" width="240" height="190" rx="14" fill="#151b2e" stroke="#2a3150" stroke-width="1.5"/>
<text x="750" y="46" text-anchor="middle" fill="#ffd166" font-size="14" font-weight="700">Azure · AKS</text>
<rect x="650" y="62" width="200" height="50" rx="8" fill="#0d1120" stroke="#7b8cff" stroke-width="1.5"/>
<text x="750" y="82" text-anchor="middle" fill="#f1f3ff" font-size="12">pod api</text>
<text x="750" y="100" text-anchor="middle" fill="#9aa3c7" font-size="11">JSON + service.name + trace_id</text>
<line x1="750" y1="112" x2="750" y2="130" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrL1)"/>
<rect x="650" y="132" width="200" height="62" rx="8" fill="#0d1120" stroke="#4fffb0" stroke-width="1.5"/>
<text x="750" y="152" text-anchor="middle" fill="#f1f3ff" font-size="12">OTel Collector (DaemonSet)</text>
<text x="750" y="170" text-anchor="middle" fill="#9aa3c7" font-size="11">resourcedetection →</text>
<text x="750" y="186" text-anchor="middle" fill="#4fffb0" font-size="11">cloud.provider=azure</text>
<path d="M150,210 C150,250 430,230 440,262" fill="none" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrL1)"/>
<path d="M450,210 L450,262" fill="none" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrL1)"/>
<path d="M750,210 C750,250 470,230 460,262" fill="none" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrL1)"/>
<text x="270" y="248" text-anchor="middle" fill="#9aa3c7" font-size="11">OTLP/HTTP · zstd</text>
<text x="630" y="248" text-anchor="middle" fill="#9aa3c7" font-size="11">OTLP/HTTP · zstd</text>
<rect x="320" y="264" width="260" height="46" rx="10" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/>
<text x="450" y="284" text-anchor="middle" fill="#f1f3ff" font-size="13" font-weight="700">Loki</text>
<text x="450" y="301" text-anchor="middle" fill="#9aa3c7" font-size="11">ingestion OTLP · object storage · un seul index</text>
<line x1="450" y1="310" x2="450" y2="326" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrL1)"/>
<rect x="320" y="328" width="260" height="40" rx="10" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/>
<text x="450" y="346" text-anchor="middle" fill="#f1f3ff" font-size="12" font-weight="700">Grafana</text>
<text x="450" y="361" text-anchor="middle" fill="#7b8cff" font-size="11">{service_name="api"} |= "error"</text>
</g>
</svg>
</div>

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.

```yaml
# 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 :

```yaml
# 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 :

```logql
{service_name="api"} |= "error"
```

La même chose, sur GCP seulement :

```logql
{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 :

```logql
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 :

```logql
{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 :

```yaml
# 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.

<div class="article-figure">
<svg viewBox="0 0 900 280" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Trois façons de gérer l'egress des logs. Un : compresser et échantillonner à l'agent, 100 Go par jour deviennent 10 à 20 Go, egress d'environ 20 à 45 dollars par mois, le compromis est que vous choisissez ce qui est jeté. Deux : un Loki par cloud avec un seul Grafana fédéré, seuls les résultats de requête sortent, egress proche de zéro, le compromis est trois Loki à opérer. Trois : centraliser sur le cloud dominant, les clouds plus petits paient 9 à 12 cents par Go, egress d'environ 180 à 240 dollars par mois pour des logs bruts, le compromis est qu'un cloud devient plus égal que les autres.">
<defs><marker id="arrL2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#4fffb0"/></marker></defs>
<g font-family="Inter,system-ui,sans-serif">
<rect x="20" y="15" width="260" height="250" rx="14" fill="#151b2e" stroke="#2a3150" stroke-width="1.5"/>
<text x="150" y="42" text-anchor="middle" fill="#ffd166" font-size="13" font-weight="700">1 · Compresser + échantillonner à l'agent</text>
<rect x="38" y="70" width="96" height="40" rx="8" fill="#0d1120" stroke="#ff6b8a" stroke-width="1.5"/>
<text x="86" y="94" text-anchor="middle" fill="#f1f3ff" font-size="12">100 Go/jour</text>
<line x1="136" y1="90" x2="166" y2="90" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrL2)"/>
<text x="151" y="64" text-anchor="middle" fill="#9aa3c7" font-size="10">zstd + échantillonnage</text>
<rect x="168" y="70" width="94" height="40" rx="8" fill="#0d1120" stroke="#4fffb0" stroke-width="1.5"/>
<text x="215" y="94" text-anchor="middle" fill="#f1f3ff" font-size="12">10–20 Go/jour</text>
<line x1="215" y1="112" x2="215" y2="140" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrL2)"/>
<text x="232" y="130" text-anchor="start" fill="#9aa3c7" font-size="10">egress</text>
<rect x="70" y="142" width="160" height="36" rx="8" fill="#0d1120" stroke="#ffd166" stroke-width="1.5"/>
<text x="150" y="164" text-anchor="middle" fill="#f1f3ff" font-size="12">Loki (central)</text>
<text x="150" y="222" text-anchor="middle" fill="#4fffb0" font-size="12" font-weight="700">egress ≈ 20–45 $ / mois</text>
<text x="150" y="248" text-anchor="middle" fill="#ffd166" font-size="11">compromis : vous choisissez ce qui est jeté</text>
<rect x="320" y="15" width="260" height="250" rx="14" fill="#151b2e" stroke="#2a3150" stroke-width="1.5"/>
<text x="450" y="42" text-anchor="middle" fill="#ffd166" font-size="13" font-weight="700">2 · Un Loki par cloud + Grafana fédéré</text>
<rect x="400" y="60" width="100" height="34" rx="8" fill="#0d1120" stroke="#7b8cff" stroke-width="1.5"/>
<text x="450" y="82" text-anchor="middle" fill="#f1f3ff" font-size="12">Grafana</text>
<rect x="335" y="140" width="70" height="40" rx="8" fill="#0d1120" stroke="#ffd166" stroke-width="1.5"/>
<text x="370" y="158" text-anchor="middle" fill="#f1f3ff" font-size="11">Loki</text>
<text x="370" y="172" text-anchor="middle" fill="#9aa3c7" font-size="10">aws</text>
<rect x="415" y="140" width="70" height="40" rx="8" fill="#0d1120" stroke="#ffd166" stroke-width="1.5"/>
<text x="450" y="158" text-anchor="middle" fill="#f1f3ff" font-size="11">Loki</text>
<text x="450" y="172" text-anchor="middle" fill="#9aa3c7" font-size="10">gcp</text>
<rect x="495" y="140" width="70" height="40" rx="8" fill="#0d1120" stroke="#ffd166" stroke-width="1.5"/>
<text x="530" y="158" text-anchor="middle" fill="#f1f3ff" font-size="11">Loki</text>
<text x="530" y="172" text-anchor="middle" fill="#9aa3c7" font-size="10">azure</text>
<line x1="370" y1="138" x2="440" y2="96" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrL2)"/>
<line x1="450" y1="138" x2="450" y2="96" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrL2)"/>
<line x1="530" y1="138" x2="460" y2="96" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrL2)"/>
<text x="450" y="200" text-anchor="middle" fill="#9aa3c7" font-size="10">résultats de requête seulement (kilooctets)</text>
<text x="450" y="222" text-anchor="middle" fill="#4fffb0" font-size="12" font-weight="700">egress ≈ 0 $</text>
<text x="450" y="248" text-anchor="middle" fill="#ffd166" font-size="11">compromis : trois Loki à opérer</text>
<rect x="620" y="15" width="260" height="250" rx="14" fill="#151b2e" stroke="#2a3150" stroke-width="1.5"/>
<text x="750" y="42" text-anchor="middle" fill="#ffd166" font-size="13" font-weight="700">3 · Centraliser sur le cloud dominant</text>
<rect x="640" y="60" width="220" height="70" rx="10" fill="#0d1120" stroke="#2a3150" stroke-width="1.5"/>
<text x="655" y="80" text-anchor="start" fill="#9aa3c7" font-size="11">AWS · 80 % des workloads</text>
<rect x="740" y="90" width="100" height="32" rx="8" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/>
<text x="790" y="110" text-anchor="middle" fill="#f1f3ff" font-size="12">Loki</text>
<rect x="640" y="150" width="90" height="34" rx="8" fill="#0d1120" stroke="#7b8cff" stroke-width="1.5"/>
<text x="685" y="171" text-anchor="middle" fill="#f1f3ff" font-size="11">GKE · 10 %</text>
<rect x="770" y="150" width="90" height="34" rx="8" fill="#0d1120" stroke="#7b8cff" stroke-width="1.5"/>
<text x="815" y="171" text-anchor="middle" fill="#f1f3ff" font-size="11">AKS · 10 %</text>
<line x1="700" y1="148" x2="770" y2="124" stroke="#ff6b8a" stroke-width="1.5" marker-end="url(#arrL2)"/>
<line x1="812" y1="148" x2="800" y2="124" stroke="#ff6b8a" stroke-width="1.5" marker-end="url(#arrL2)"/>
<text x="750" y="200" text-anchor="middle" fill="#ff6b8a" font-size="10">0,09–0,12 $ / Go sur chaque octet qui traverse</text>
<text x="750" y="222" text-anchor="middle" fill="#ff6b8a" font-size="12" font-weight="700">egress ≈ 180–240 $ / mois (brut)</text>
<text x="750" y="248" text-anchor="middle" fill="#ffd166" font-size="11">compromis : un cloud plus égal que les autres</text>
</g>
</svg>
</div>

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](/fr/blog/agent-factory-platform) 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](/contact).
