# Des alertes qui ne savent pas dans quel cloud elles tournent

Chaque cloud vous vend des alarmes. CloudWatch Alarms, règles d'alerte Azure Monitor, politiques d'alerte Cloud Monitoring : chacune est préconfigurée, chacune se met en place en quelques clics, et chacune est définie dans un format que seul son propre cloud comprend. Si vous tournez sur plus d'un, vous finissez avec trois systèmes d'alerte, trois routages d'astreinte, trois définitions de « l'API est en mauvaise santé », et, tôt ou tard, trois seuils différents pour la même chose parce que quelqu'un en a ajusté un et a oublié les autres.

Nous avons défendu l'idée que [les logs](/fr/blog/your-logs-should-not-know-which-cloud) et [les traces](/fr/blog/your-traces-should-not-know-which-cloud) devraient être émis par l'application et stockés quelque part que le cloud ne possède pas. Les alertes sont la troisième pièce, et c'est celle qui compte à 02 h 40, parce qu'une alerte est ce qui décide si vous êtes réveillé. Voici comment nous les définissons une fois, à partir de métriques que l'application émet, pour que « taux d'erreur au-dessus de 1 % » veuille dire la même chose sur EKS, GKE et AKS, et que déplacer un service de l'un à l'autre ne change pas ce qui vous appelle.

## Alerter sur ce que voit l'utilisateur, pas sur ce que voit le cloud

Les alarmes natives du cloud portent sur les ressources du cloud : CPU d'une instance, nombre de 5xx sur un équilibreur, limitations sur une table. Elles sont utiles, et ce sont les mauvaises pour appeler quelqu'un, parce qu'un utilisateur ne ressent pas le CPU. Un utilisateur ressent une requête lente ou échouée. Donc les alertes qui appellent portent sur trois nombres par service, mesurés du point de vue de l'application elle-même :

- **Le débit** : requêtes par seconde, pour qu'une chute à zéro soit visible.
- **Les erreurs** : fraction des requêtes qui ont échoué.
- **La durée** : latence au 95e ou 99e percentile.

Ils proviennent du même pipeline OpenTelemetry qui transporte les traces. Le connecteur `spanmetrics` du collecteur transforme chaque span serveur en un histogramme de durées et un compteur d'appels, étiquetés avec `service.name`, la route HTTP, le code de statut et, parce que `resourcedetection` a tourné avant, `cloud.provider`. Aucune nouvelle instrumentation dans l'application. Les spans que vous émettez déjà deviennent les métriques sur lesquelles vous alertez.

```yaml
connectors:
  spanmetrics:
    histogram:
      explicit: { buckets: [50ms, 100ms, 250ms, 500ms, 1s, 2s, 5s] }
    dimensions:
      - name: http.route
      - name: http.response.status_code
    exemplars: { enabled: true }
    resource_metrics_key_attributes: [service.name, cloud.provider, cloud.region]

service:
  pipelines:
    traces:  { receivers: [otlp], processors: [resourcedetection, batch], exporters: [otlphttp/tempo, spanmetrics] }
    metrics: { receivers: [spanmetrics, otlp], processors: [batch], exporters: [prometheusremotewrite/mimir] }
```

Les métriques vont dans Mimir (ou un simple Prometheus, ou VictoriaMetrics ; l'architecture s'en moque). Les exemplaires sont activés, donc un point sur le graphique de latence renvoie à une vraie trace qui l'a produit.

<div class="article-figure">
<svg viewBox="0 0 900 220" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pipeline des spans jusqu'à l'appel d'astreinte. Les applications sur EKS, GKE et AKS émettent des spans en OTLP. Le collecteur exécute resourcedetection, puis le connecteur spanmetrics dérive les compteurs de requêtes et les histogrammes de durée étiquetés par service, route, statut et fournisseur de cloud. Les métriques vont dans Mimir. Les règles d'alerte Grafana, définies une fois en tant que code, évaluent du PromQL sur tous les clouds et routent vers le pager d'astreinte. Les alarmes natives du cloud pour l'infrastructure alimentent le même pager mais n'appellent jamais seules.">
<defs><marker id="arrA" 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" font-size="12">
<rect x="15" y="40" width="150" height="90" rx="10" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/><text x="90" y="62" text-anchor="middle" fill="#f1f3ff" font-weight="700">applications</text><text x="90" y="82" text-anchor="middle" fill="#ffd166" font-size="11">EKS · GKE · AKS</text><text x="90" y="100" text-anchor="middle" fill="#9aa3c7" font-size="11">spans en OTLP</text><text x="90" y="118" text-anchor="middle" fill="#9aa3c7" font-size="11">aucun code de métriques</text>
<line x1="167" y1="85" x2="200" y2="85" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrA)"/>
<rect x="203" y="40" width="190" height="90" rx="10" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/><text x="298" y="62" text-anchor="middle" fill="#f1f3ff" font-weight="700">collecteur</text><text x="298" y="82" text-anchor="middle" fill="#9aa3c7" font-size="11">resourcedetection →</text><text x="298" y="100" text-anchor="middle" fill="#4fffb0" font-size="11">connecteur spanmetrics</text><text x="298" y="118" text-anchor="middle" fill="#9aa3c7" font-size="11">débit · erreurs · durée</text>
<line x1="395" y1="85" x2="428" y2="85" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrA)"/>
<rect x="431" y="40" width="130" height="90" rx="10" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="496" y="62" text-anchor="middle" fill="#f1f3ff" font-weight="700">Mimir</text><text x="496" y="82" text-anchor="middle" fill="#9aa3c7" font-size="11">les labels incluent</text><text x="496" y="100" text-anchor="middle" fill="#7b8cff" font-size="11">cloud_provider</text><text x="496" y="118" text-anchor="middle" fill="#9aa3c7" font-size="11">exemplaires → Tempo</text>
<line x1="563" y1="85" x2="596" y2="85" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrA)"/>
<rect x="599" y="40" width="150" height="90" rx="10" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="674" y="62" text-anchor="middle" fill="#f1f3ff" font-weight="700">Grafana alerting</text><text x="674" y="82" text-anchor="middle" fill="#9aa3c7" font-size="11">règles en tant que code</text><text x="674" y="100" text-anchor="middle" fill="#9aa3c7" font-size="11">burn rate sur les SLO</text><text x="674" y="118" text-anchor="middle" fill="#9aa3c7" font-size="11">une règle, tous les clouds</text>
<line x1="751" y1="85" x2="784" y2="85" stroke="#ff6b8a" stroke-width="1.5" marker-end="url(#arrA)"/>
<rect x="787" y="40" width="98" height="90" rx="10" fill="#151b2e" stroke="#ff6b8a" stroke-width="1.5"/><text x="836" y="80" text-anchor="middle" fill="#ff6b8a" font-weight="700">pager</text><text x="836" y="100" text-anchor="middle" fill="#9aa3c7" font-size="11">une seule rotation</text>
<rect x="203" y="160" width="358" height="44" rx="10" fill="#0d1120" stroke="#2a3150" stroke-width="1.5" stroke-dasharray="5,3"/><text x="382" y="178" text-anchor="middle" fill="#9aa3c7" font-size="11">alarmes natives : CPU RDS, erreurs NAT, ACU Aurora, quotas</text><text x="382" y="195" text-anchor="middle" fill="#9aa3c7" font-size="11">restent dans chaque cloud · vers le même pager en basse priorité</text>
<path d="M561,182 L836,182 L836,132" fill="none" stroke="#2a3150" stroke-width="1.5" stroke-dasharray="5,3" marker-end="url(#arrA)"/>
</g>
</svg>
</div>

## Les règles, écrites une seule fois

Les règles d'alerte Grafana vivent dans un fichier YAML du dépôt et sont provisionnées à chaque déploiement, comme les tableaux de bord. Une règle par SLO, pas une par service et par cloud. La requête fait l'éventail :

```yaml
# alerting/rules.yaml
groups:
  - name: api-slo
    interval: 1m
    rules:
      - alert: ApiErrorBudgetBurn
        for: 2m
        labels: { severity: page, team: platform }
        annotations:
          summary: 'api error budget burning {{ $labels.cloud_provider }} · {{ $value | humanizePercentage }}'
          runbook: https://runbooks.internal/api-errors
        expr: |
          (
            sum by (cloud_provider) (rate(calls_total{service_name="api", http_response_status_code=~"5.."}[5m]))
            /
            sum by (cloud_provider) (rate(calls_total{service_name="api"}[5m]))
          ) > (14.4 * 0.001)
          and
          (
            sum by (cloud_provider) (rate(calls_total{service_name="api", http_response_status_code=~"5.."}[1h]))
            /
            sum by (cloud_provider) (rate(calls_total{service_name="api"}[1h]))
          ) > (14.4 * 0.001)
```

C'est une alerte de burn rate multi-fenêtres pour un SLO de disponibilité de 99,9 % : appeler quand le taux d'erreur des cinq dernières minutes *et* de la dernière heure dépassent tous deux 14,4 fois le budget d'erreur, ce qui correspond à brûler tout le budget de 30 jours en environ deux jours. Le `sum by (cloud_provider)` est toute l'histoire multi-cloud. Une règle, une expression, et elle se déclenche séparément pour `aws`, `gcp` et `azure`, avec le cloud dans le titre de l'alerte, si un seul d'entre eux brûle. Déplacer l'API d'EKS vers GKE change quelle valeur de label se déclenche. Ça ne change pas la règle.

Une seconde règle avec une fenêtre plus longue (6 heures et 3 jours, seuil 1×) attrape les combustions lentes comme un ticket plutôt qu'un appel. La latence reçoit le même traitement sur `histogram_quantile(0.99, sum by (le, cloud_provider) (rate(duration_bucket{...}[5m])))`.

<div class="article-figure">
<svg viewBox="0 0 900 210" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Graphique du taux d'erreur sur 24 heures pour le service api, une ligne par cloud. AWS et Azure restent bien en dessous de l'objectif de 0,1 pour cent. GCP grimpe à 2 pour cent à 02 h 40 pendant vingt minutes ; les fenêtres de burn rate de 5 minutes et de 1 heure franchissent toutes deux le seuil de 14,4 fois et un appel se déclenche pour cloud_provider gcp seulement. Un plateau lent à 0,3 pour cent sur Azure l'après-midi ne franchit que la règle à longue fenêtre et crée un ticket.">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<text x="20" y="22" fill="#f1f3ff" font-size="14" font-weight="700">taux d'erreur api par cloud · une règle, trois valeurs de label</text>
<line x1="60" y1="170" x2="880" y2="170" stroke="#9aa3c7"/><line x1="60" y1="40" x2="60" y2="170" stroke="#9aa3c7"/>
<line x1="60" y1="164" x2="880" y2="164" stroke="#2a3150" stroke-dasharray="3,3"/><text x="884" y="167" fill="#9aa3c7" font-size="10">SLO 0,1 %</text>
<line x1="60" y1="120" x2="880" y2="120" stroke="#ff6b8a" stroke-dasharray="4,3"/><text x="884" y="123" fill="#ff6b8a" font-size="10">14,4× · appel</text>
<polyline points="60,167 200,166 400,167 600,166 880,167" fill="none" stroke="#ffd166" stroke-width="2"/><text x="70" y="52" fill="#ffd166" font-size="10">aws</text>
<polyline points="60,167 140,167 150,60 175,58 190,166 400,167 880,167" fill="none" stroke="#4fffb0" stroke-width="2"/><text x="70" y="66" fill="#4fffb0" font-size="10">gcp</text>
<polyline points="60,167 500,167 520,150 700,150 720,167 880,167" fill="none" stroke="#7b8cff" stroke-width="2"/><text x="70" y="80" fill="#7b8cff" font-size="10">azure</text>
<rect x="148" y="40" width="44" height="130" fill="#ff6b8a" opacity="0.12"/><text x="170" y="192" text-anchor="middle" fill="#ff6b8a" font-size="10">02:40 · appel : gcp</text>
<rect x="515" y="40" width="210" height="130" fill="#7b8cff" opacity="0.08"/><text x="620" y="192" text-anchor="middle" fill="#7b8cff" font-size="10">14:00–19:00 · combustion lente : ticket, azure</text>
<text x="60" y="206" fill="#9aa3c7" font-size="10">00:00</text><text x="880" y="206" text-anchor="end" fill="#9aa3c7" font-size="10">24:00</text>
</g>
</svg>
</div>

## Ce qui reste natif, et comment ça se raccorde

Les alarmes propres au cloud ne disparaissent pas ; elles changent de rôle. CPU RDS, utilisation des ACU Aurora, erreurs d'allocation de ports sur la NAT gateway, limitations DynamoDB, quotas de service : ce sont des propriétés d'une infrastructure que le cloud possède, et la supervision du cloud les voit en premier et le mieux. Nous gardons ces alarmes natives, définies dans le même CDK qui définit la ressource, et les routons vers le même pager en *basse priorité*. Elles informent ; elles ne réveillent personne. Si un NAT commence à perdre des paquets, l'alerte de taux d'erreur de l'API appelle de toute façon dans les deux minutes, et l'alarme NAT est là dans le canal d'incident pour expliquer pourquoi.

Cette inversion est tout l'intérêt. Les symptômes appellent. Les causes annotent. Les alertes de symptôme sont neutres vis-à-vis du cloud parce que l'application a émis les données ; les alertes de cause sont spécifiques au cloud parce que le cloud a émis les données. Aucune ne prétend être l'autre.

## Trois choses qui nous ont pris plus de temps qu'elles n'auraient dû

**La cardinalité.** `spanmetrics` avec `http.route` comme dimension, ça va. Avec `http.target` (le chemin brut, identifiants inclus), ça génère une série temporelle par commande et Mimir s'effondre. Utilisez le gabarit de route, jamais le chemin.

**Le silence pendant les déploiements.** Un déploiement progressif produit une rafale de connexions réinitialisées pendant quelques secondes. Le `for: 2m` sur la règle d'appel l'absorbe. Sans lui, chaque déploiement a appelé quelqu'un la première semaine.

**Le pager doit être une seule chose.** Nous avions CloudWatch vers un outil d'astreinte et Grafana vers un autre, parce qu'ils avaient été mis en place à des moments différents. Deux rotations, deux applications sur le téléphone, et une nuit où la personne d'astreinte pour l'un ne l'était pas pour l'autre. Tout passe maintenant par les points de contact de Grafana vers une seule rotation. Les alarmes natives y arrivent via un topic SNS → webhook, ce qui a pris une heure à configurer et aurait dû être fait le premier jour.

## La version courte

Alerter sur les trois nombres que l'utilisateur ressent, dérivés de spans que vous émettez déjà, stockés dans un backend de métriques qui vous appartient, évalués par des règles que vous gardez dans git avec un `sum by (cloud_provider)` dedans. Garder les alarmes du cloud pour les choses du cloud, en basse priorité, vers le même pager. Quand un service change de cloud, une valeur de label change et personne n'édite une alerte.

Vous voulez le fichier de règles pour vos propres services, calibré sur de vrais SLO plutôt que sur des chiffres ronds ? [Parlons-en](/contact).
