# Alerte care nu știu în ce cloud rulează

Fiecare cloud îți vinde alarme. CloudWatch Alarms, reguli de alertă Azure Monitor, politici de alertare Cloud Monitoring: fiecare e preconfigurată, fiecare e la câteva click-uri și fiecare e definită într-un format pe care îl înțelege doar propriul cloud. Dacă rulezi în mai mult de unul, ajungi cu trei sisteme de alertare, trei rutări de on-call, trei definiții pentru „API-ul e nesănătos” și, mai devreme sau mai târziu, trei praguri diferite pentru același lucru, pentru că cineva a reglat unul și le-a uitat pe celelalte.

Am argumentat că [logurile](/ro/blog/your-logs-should-not-know-which-cloud) și [trace-urile](/ro/blog/your-traces-should-not-know-which-cloud) ar trebui emise de aplicație și stocate undeva unde cloud-ul nu le deține. Alertele sunt a treia piesă, și sunt cea care contează la 02:40, pentru că o alertă e lucrul care decide dacă ești treaz. Iată cum le definim o singură dată, din metrici pe care le emite aplicația, ca „rata de erori peste 1 %” să însemne același lucru pe EKS, GKE și AKS, iar mutarea unui serviciu între ele să nu schimbe ce te sună.

## Alertează pe ce vede utilizatorul, nu pe ce vede cloud-ul

Alarmele native ale cloud-ului sunt despre resursele cloud-ului: CPU pe o instanță, numărul de 5xx pe un load balancer, throttle-uri pe o tabelă. Sunt utile și sunt lucrul greșit pe care să sune pager-ul, pentru că un utilizator nu simte CPU. Un utilizator simte un request lent sau eșuat. Așa că alertele care sună sunt pe trei numere per serviciu, măsurate din punctul de vedere al aplicației:

- **Rata**: request-uri pe secundă, ca o cădere la zero să fie vizibilă.
- **Erorile**: fracțiunea de request-uri care au eșuat.
- **Durata**: latența la percentila 95 sau 99.

Vin din același pipeline OpenTelemetry care poartă trace-urile. Conectorul `spanmetrics` al collector-ului transformă fiecare span de server într-o histogramă de durate și un contor de apeluri, etichetate cu `service.name`, ruta HTTP, codul de status și, pentru că `resourcedetection` a rulat înainte, `cloud.provider`. Nicio instrumentare nouă în aplicație. Span-urile pe care le emiți deja devin metricile pe care alertezi.

```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] }
```

Metricile merg în Mimir (sau Prometheus simplu, sau VictoriaMetrics; arhitecturii nu-i pasă). Exemplarele sunt pornite, deci un punct de pe graficul de latență trimite la un trace real care l-a produs.

<div class="article-figure">
<svg viewBox="0 0 900 220" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pipeline de la span-uri la o paginare. Aplicațiile de pe EKS, GKE și AKS emit span-uri prin OTLP. Collector-ul rulează resourcedetection, apoi conectorul spanmetrics derivă numărul de request-uri și histograme de durată etichetate cu serviciu, rută, status și cloud provider. Metricile merg în Mimir. Regulile de alertă Grafana, definite o dată ca și cod, evaluează PromQL peste toate cloudurile și rutează către pager-ul de on-call. Alarmele native ale cloud-ului pentru infrastructură alimentează același pager, dar nu sună niciodată singure.">
<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">aplicații</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">span-uri prin OTLP</text><text x="90" y="118" text-anchor="middle" fill="#9aa3c7" font-size="11">fără cod de metrici</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">collector</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">conectorul spanmetrics</text><text x="298" y="118" text-anchor="middle" fill="#9aa3c7" font-size="11">rată · erori · durată</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">label-urile includ</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">exemplare → 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">reguli ca și cod</text><text x="674" y="100" text-anchor="middle" fill="#9aa3c7" font-size="11">burn rate pe SLO-uri</text><text x="674" y="118" text-anchor="middle" fill="#9aa3c7" font-size="11">o regulă, toate cloudurile</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">o singură rotație</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">alarme native: CPU RDS, erori NAT, ACU Aurora, limite de cotă</text><text x="382" y="195" text-anchor="middle" fill="#9aa3c7" font-size="11">rămân în fiecare cloud · trimise la același pager cu prioritate mică</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>

## Regulile, scrise o singură dată

Regulile de alertă Grafana trăiesc într-un fișier YAML din repository și sunt provizionate la fiecare deploy, la fel ca dashboard-urile. O regulă per SLO, nu una per serviciu per cloud. Query-ul face desfacerea:

```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)
```

E o alertă multi-window de burn rate pentru un SLO de disponibilitate de 99,9 %: sună când rata de erori din ultimele cinci minute *și* din ultima oră depășesc amândouă de 14,4 ori bugetul de erori, ceea ce corespunde consumării întregului buget pe 30 de zile în cam două zile. `sum by (cloud_provider)` e toată povestea multi-cloud. O regulă, o expresie, și se declanșează separat pentru `aws`, `gcp` și `azure`, cu cloud-ul în titlul alertei, dacă doar unul dintre ele arde. Mutarea API-ului de pe EKS pe GKE schimbă care valoare de label se declanșează. Nu schimbă regula.

O a doua regulă cu o fereastră mai lungă (6 ore și 3 zile, prag 1×) prinde arderile lente ca un tichet, nu ca o paginare. Latența primește același tratament pe `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="Grafic cu rata de erori pe 24 de ore pentru serviciul api, o linie per cloud. AWS și Azure stau mult sub obiectivul de 0,1 la sută. GCP sare la 2 la sută la 02:40 pentru douăzeci de minute; ferestrele de burn rate de 5 minute și de 1 oră depășesc amândouă pragul de 14,4 ori și se declanșează o paginare doar pentru cloud_provider gcp. Un platou lent de 0,3 la sută pe Azure după-amiaza depășește doar regula cu fereastră lungă și creează un tichet.">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<text x="20" y="22" fill="#f1f3ff" font-size="14" font-weight="700">rata de erori api pe cloud · o regulă, trei valori 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× · paginare</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 · paginare: 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 · ardere lentă: tichet, 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 rămâne nativ și cum se alătură

Alarmele proprii ale cloud-ului nu dispar; își schimbă rolul. CPU pe RDS, utilizarea ACU pe Aurora, erorile de alocare de porturi pe NAT gateway, throttle-urile DynamoDB, limitele de cotă ale serviciilor: sunt proprietăți ale infrastructurii pe care o deține cloud-ul, iar monitorizarea cloud-ului le vede prima și cel mai bine. Ținem alarmele alea native, definite în același CDK care definește resursa, și le rutăm către același pager ca *prioritate mică*. Informează; nu trezesc pe nimeni. Dacă un NAT începe să piardă pachete, alerta de rată de erori a API-ului sună oricum în două minute, iar alarma NAT stă acolo în canalul de incident explicând de ce.

Inversiunea asta e tot rostul. Simptomele sună. Cauzele adnotează. Alertele de simptom sunt neutre față de cloud pentru că aplicația a emis datele; alertele de cauză sunt specifice cloud-ului pentru că cloud-ul a emis datele. Niciuna nu se preface a fi cealaltă.

## Trei lucruri care ne-au luat mai mult decât ar fi trebuit

**Cardinalitatea.** `spanmetrics` cu `http.route` ca dimensiune e în regulă. Cu `http.target` (calea brută, cu ID-uri) generează o serie de timp per comandă și Mimir cade. Folosește șablonul rutei, niciodată calea.

**Liniște în timpul deploy-urilor.** Un rolling deploy produce o rafală de connection reset-uri câteva secunde. `for: 2m` de pe regula de paginare o absoarbe. Fără el, fiecare deploy a sunat pe cineva în prima săptămână.

**Pager-ul trebuie să fie un singur lucru.** Aveam CloudWatch mergând într-o unealtă de on-call și Grafana în alta, pentru că fuseseră configurate în momente diferite. Două rotații, două aplicații pe telefon și o noapte în care persoana de gardă pentru una nu era de gardă pentru cealaltă. Totul se rutează acum prin contact point-urile Grafana către o singură rotație. Alarmele native ajung acolo printr-un topic SNS → webhook, ceea ce a luat o oră de configurat și ar fi trebuit să fie din prima zi.

## Versiunea scurtă

Alertează pe cele trei numere pe care le simte utilizatorul, derivate din span-uri pe care le emiți deja, stocate într-un backend de metrici pe care îl deții, evaluate de reguli pe care le ții în git cu un `sum by (cloud_provider)` în ele. Ține alarmele cloud-ului pentru lucrurile cloud-ului, cu prioritate mică, în același pager. Când un serviciu se mută între clouduri, se schimbă o valoare de label și nimeni nu editează o alertă.

Vrei fișierul de reguli pentru propriile servicii, reglat pe SLO-uri reale, nu pe numere rotunde? [Hai să vorbim](/contact).
