El Mòdul 6 va acabar amb una pregunta incòmoda: la plataforma és segura, però sabem si funciona? En un monòlit, "funciona" es comprovava mirant un procés i un log. A Quilòmetre Zero hi ha sis serveis, tres bases de dades, un clúster de Kafka, Redis, MinIO, un gateway i un IdP, repartits per desenes de contenidors: ningú no pot mirar tot això alhora, i quan alguna cosa va malament gairebé mai no ho diu a crits. Un inventari que respon en 900 ms en lloc de 40 continua "funcionant"; un consumidor de comandes.esdeveniments que s'ha quedat tres hores enrere no llança cap excepció; un certificat de Vault que caduca a les 03:00 d'un diumenge només es nota quan la primera comanda del matí falla. Aquesta lliçó tracta de convertir la plataforma en una cosa observable a través del seu primer pilar, les mètriques: què mesurar, com recollir-ho amb Prometheus, com expressar en números allò que Quilòmetre Zero promet a l'Anna, i com alertar abans que ella se n'assabenti. Els logs i les traces, els altres dos pilars, són la lliçó 07-02.

Contingut

  1. Observabilitat i els seus tres pilars
  2. Què mesurar: infraestructura, plataforma, aplicació i negoci
  3. Tipus de mètrica i el problema de la cardinalitat
  4. Prometheus: model pull, exporters i descobriment
  5. PromQL essencial: taxes, percentils i agregacions
  6. SLI, SLO, SLA i pressupost d'error
  7. Alertes: símptomes, burn rate i Alertmanager
  8. Dashboards a Grafana i monitoratge actiu
  9. Errors comuns i consells
  10. Exercicis i solucions
  11. Conclusió

  1. Observabilitat i els seus tres pilars

Monitorar és comprovar coses que ja sabem que poden fallar: el disc s'omple, el procés mor, la latència puja. L'observabilitat és una propietat del sistema: la capacitat de respondre preguntes que no havíem previst ("per què les comandes de Lleida amb formatge-fresc triguen el doble des d'ahir?") a partir del que el sistema emet cap enfora. En un sistema distribuït la diferència importa perquè les preguntes imprevistes són la norma: les fallades interessants són combinacions de peces, no peces aïllades.

Els tres senyals que un sistema emet, i que junts donen observabilitat, són:

Pilar Què és Pregunta que respon Cost per esdeveniment Lliçó
Mètriques Números agregats en el temps (comptadors, mesures) "Quant? Amb quina freqüència? Està pitjor que fa una hora?" Molt baix (una sèrie per combinació d'etiquetes, no per esdeveniment) 07-01
Logs Esdeveniments discrets amb context "Què va passar exactament amb la comanda P-2026-000125?" Alt (un registre per esdeveniment) 07-02
Traces El recorregut d'una petició per diversos serveis "On se n'ha anat el temps d'aquesta petició? Què ha cridat què?" Mitjà-alt (mostrejat) 07-02

Les mètriques són el punt de partida perquè són les úniques de les tres que escalen amb el nombre de sèries, no amb el trànsit: comptar 10 comandes per minut o 10 000 costa el mateix. Són el senyal que dispara alertes i el que pinta el panell de la Setmana de la Verema. Els logs i les traces entren després, quan la mètrica diu "alguna cosa va malament" i cal esbrinar què.

  1. Què mesurar: infraestructura, plataforma, aplicació i negoci

Un error freqüent és començar per la infraestructura (CPU, memòria) perquè és el que donen els agents per defecte, i acabar amb cent gràfiques que no diuen si l'Anna pot comprar. Convé pensar en quatre capes, de baix a dalt, i triar a cadascuna poques mètriques amb sentit.

2.1 Infraestructura

Allò que mesura node_exporter a cada màquina o cAdvisor a cada contenidor: CPU, memòria, disc (espai i latència d'E/S), xarxa (bytes, errors, paquets descartats). Per a aquests recursos serveix el mètode USE de Brendan Gregg: per a cada recurs, Utilització (percentatge de temps ocupat), Saturació (feina encuada que no hi cap: run queue, swap, cua de disc) i Errors.

2.2 Plataforma

Les peces intermèdies tenen mètriques pròpies que anticipen problemes de l'aplicació:

  • Kafka: el lag de cada grup de consumidors (missatges produïts menys consumits, per partició). Un lag creixent a analitica és tolerable; a repartiment significa que el panell de furgoneta-3 mostra posicions velles. També particions sense rèplica sincronitzada (es veurà a 07-03).
  • PostgreSQL: connexions actives respecte del màxim (max_connections), transaccions per segon, retard de la rèplica comandes-db-replica respecte del primari, mida de taules i bloquejos en espera.
  • Redis: hit ratio de la memòria cau de catàleg (encerts / (encerts + fallades)), memòria usada respecte del límit, claus expulsades, latència d'ordres.
  • Cassandra: latència de lectura i escriptura per taula, compactacions pendents, nodes caiguts vistos per gossip.

2.3 Aplicació: els quatre senyals daurats

Google va popularitzar a Site Reliability Engineering els quatre senyals daurats, els que cal mirar a qualsevol servei que atén peticions:

  1. Latència: quant triga a respondre, distingint peticions correctes de fallides (un error ràpid no és "bona latència").
  2. Trànsit: quanta demanda rep (peticions per segon, missatges consumits per segon).
  3. Errors: quina fracció de peticions falla, explícitament (5xx, UNAVAILABLE de gRPC) o implícitament (ha respost 200 però ha trigat més del que s'havia acordat).
  4. Saturació: com de "ple" està el servei: fils ocupats, connexions del pool en ús, cua de feina.

El mètode RED (Tom Wilkie) és la versió per a serveis: Rate, Errors, Duration. Coincideix amb els tres primers senyals daurats i és el que instrumentem de manera uniforme a tots els serveis de km0/.

Mètode S'aplica a Mesura Exemple a Quilòmetre Zero
RED Serveis (coses que atenen peticions) Rate, Errors, Duration comandes: peticions/s a POST /comandes, % de 5xx, p99 de durada
USE Recursos (coses amb capacitat finita) Utilization, Saturation, Errors Disc del broker de Kafka: 85 % usat, cua d'E/S de 12, 0 errors
Senyals daurats Sistemes orientats a l'usuari Latència, trànsit, errors, saturació L'anterior més "pool de connexions a km0_comandes al 90 %"

Taula de senyals daurats per servei:

Servei Latència Trànsit Errors Saturació
cataleg p99 de GET /cataleg/* (objectiu < 200 ms) peticions/s per mercat % 5xx; fallades de Redis hit ratio de Redis; connexions a PostgreSQL
comandes p99 de POST /comandes (< 500 ms) comandes/min % 5xx; sagues compensades / total pool de km0_comandes; fils HTTP ocupats
inventari p99 de ReservarEstoc (< 100 ms) crides gRPC/s per mètode % UNAVAILABLE + DEADLINE_EXCEEDED connexions a km0_inventari; lag de replicació inv-bcn/inv-vlc
pagaments p99 de Cobrar (< 2 s, depèn de la passarel·la) cobraments/min % rebutjats per error tècnic (no per targeta rebutjada) crides en vol a la passarel·la
repartiment retard entre posició emesa i mostrada posicions/s (140 repartidors × 1/5 s ≈ 28/s) missatges rebutjats lag del consumidor de repartiment.posicions
analitica durada del DAG km0_vendes_diaries esdeveniments/s consumits tasques fallides d'Airflow lag de comandes.esdeveniments; executors de Spark ocupats

2.4 Negoci

Les mètriques de negoci són les que expliquen a en Jordi Sala i la Marta Puig si la plataforma compleix el seu propòsit, i sovint detecten fallades que les tècniques no veuen: si comandes respon 200 en 80 ms però les comandes per minut cauen a zero un dissabte a les 11:00, alguna cosa s'ha trencat abans (el front, Kong, Keycloak). Per a Quilòmetre Zero: comandes creades per minut (per mercat i per productor), taxa de pagament rebutjat, valor mitjà de la comanda, reserves d'estoc fallides per producte, lliuraments a temps. S'instrumenten igual que les tècniques, al mateix Prometheus.

  1. Tipus de mètrica i el problema de la cardinalitat

Prometheus (i OpenTelemetry Metrics, que fa servir el mateix model conceptual) distingeix quatre tipus:

Tipus Què és Només pot Exemple Consulta típica
Counter Comptador acumulat des de l'arrencada del procés Pujar (es reinicia a 0 si el procés es reinicia) km0_comandes_total{estat="confirmada"} rate(...[5m])
Gauge Valor instantani Pujar i baixar km0_estoc_unitats{producte="tomaquet-rosa"}, connexions actives valor directe, avg_over_time
Histogram Distribució en buckets acumulatius + suma + compte Pujar km0_peticio_durada_segons_bucket{le="0.5"} histogram_quantile(0.99, ...)
Summary Quantils calculats al client Pujar ..._durada{quantile="0.99"} valor directe (no agregable entre instàncies)

Dos aclariments que estalvien molts errors:

  • Un counter mai no es llegeix en cru. km0_comandes_total val 1 284 522 i no diu res; el que interessa és la seva derivada: rate(km0_comandes_total[5m]) = comandes per segon en els darrers 5 minuts. Prometheus gestiona els reinicis (quan el comptador baixa, assumeix reinici i no produeix una taxa negativa).
  • Histogram contra summary: l'histogram desa quantes observacions han caigut per sota de cada límit (le="0.1", le="0.25", le="0.5", ...). Els percentils es calculen al servidor i, sobretot, es poden agregar entre instàncies: el p99 de comandes amb 3 rèpliques s'obté sumant buckets. Un summary calcula el p99 a cada procés i no hi ha cap manera correcta de combinar tres p99 (la mitjana de percentils no és un percentil). En un sistema distribuït, llevat de casos molt concrets, es fa servir histogram.

Cardinalitat

Cada combinació diferent de nom + etiquetes és una sèrie temporal que Prometheus desa en memòria i en disc. km0_peticio_durada_segons{servei, metode, codi} amb 6 serveis × 10 mètodes × 5 codis × 12 buckets = 3 600 sèries: bé. Si a algú se li acut afegir comanda_id com a etiqueta, cada comanda crea una sèrie nova per bucket: un milió de comandes són dotze milions de sèries, i Prometheus mor. La regla: una etiqueta només pot prendre un conjunt petit i acotat de valors (servei, mètode, codi d'estat, mercat, estat de la comanda). Mai identificadors d'entitat (comanda_id, usuari, request_id), ni adreces IP de clients, ni rutes amb paràmetres sense normalitzar (/comandes/P-2026-000123 s'ha de registrar com a /comandes/{id}). Allò que necessiti l'identificador va als logs o a les traces (07-02).

Un cas límit interessant és km0_estoc_unitats{producte}: amb uns centenars de productes és acceptable; amb cent mil no. A Quilòmetre Zero es limita als productes de les campanyes actives i, per a la resta, es publica un gauge agregat per productor.

  1. Prometheus: model pull, exporters i descobriment

Prometheus funciona en mode pull: no espera que els serveis li enviïn dades, sinó que cada 15 segons (configurable) fa GET /metrics a cada objectiu i desa el que hi troba. El format és text pla:

# HELP km0_comandes_total Comandes processades per estat final
# TYPE km0_comandes_total counter
km0_comandes_total{estat="confirmada"} 1284522
km0_comandes_total{estat="rebutjada_estoc"} 3311
km0_comandes_total{estat="rebutjada_pagament"} 8720

El pull té conseqüències importants en un sistema distribuït:

  • Prometheus sap quan un objectiu no respon: la mètrica up{job="comandes", instance="comandes-2:8000"} val 0. Amb un model push, un servei mort simplement deixa d'enviar i cal inferir-ne l'absència.
  • Els serveis no necessiten conèixer l'adreça de Prometheus ni gestionar cues d'enviament.
  • Necessita descobrir els objectius: a docker-compose n'hi ha prou amb llistar-los; a Kubernetes (07-05) els descobreix preguntant a l'API; al núvol, a l'API del proveïdor. Per a feines efímeres (un Job que dura 3 segons) existeix el Pushgateway, que és l'excepció, no la norma.

Els components que no exposen /metrics per si mateixos es cobreixen amb exporters: processos petits que tradueixen l'estat de PostgreSQL, Kafka, Redis, etc. al format de Prometheus. A Quilòmetre Zero:

flowchart LR
    subgraph serveis["Serveis km0 (prometheus_client)"]
        C[cataleg :8000/metrics]
        P[comandes :8000/metrics]
        I[inventari :9464/metrics]
        R[repartiment]
    end
    subgraph exporters["Exporters"]
        PGX[postgres_exporter<br/>km0_inventari, comandes-db-*]
        KX[kafka_exporter<br/>lag per grup]
        RX[redis_exporter<br/>hit ratio, memoria]
        NX[node_exporter<br/>CPU, disc, xarxa]
        BX[blackbox_exporter<br/>sondes HTTP/TLS]
    end
    PROM[(Prometheus<br/>scrape cada 15 s)]
    AM[Alertmanager]
    G[Grafana]
    OPS[Canal #km0-operacions<br/>Jordi, Marta]
    serveis --> PROM
    exporters --> PROM
    PROM -- regles d'alerta --> AM
    AM --> OPS
    G -- PromQL --> PROM

4.1 Instrumentar els serveis: serveis/comu/metriques.py

Tots els serveis comparteixen un mòdul amb les mètriques comunes, perquè el nom i les etiquetes siguin idèntics i els panells serveixin per a qualsevol d'ells.

# km0/serveis/comu/metriques.py
"""Mètriques Prometheus compartides pels serveis de Quilòmetre Zero."""
import time
from contextlib import contextmanager

from prometheus_client import Counter, Gauge, Histogram, start_http_server

# Buckets pensats per a latències de servei: de 5 ms a 10 s.
# Cada bucket és "quantes observacions han caigut per sota d'aquest límit".
BUCKETS_LATENCIA = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10)

COMANDES_TOTAL = Counter(
    "km0_comandes_total",
    "Comandes processades per estat final",
    ["estat"],  # confirmada | rebutjada_estoc | rebutjada_pagament | compensada
)

PETICIO_DURADA = Histogram(
    "km0_peticio_durada_segons",
    "Durada de les peticions ateses (HTTP o gRPC)",
    ["servei", "metode", "codi"],
    buckets=BUCKETS_LATENCIA,
)

ESTOC_UNITATS = Gauge(
    "km0_estoc_unitats",
    "Unitats disponibles per producte (només productes en campanya)",
    ["producte"],
)

PETICIONS_EN_VOL = Gauge(
    "km0_peticions_en_vol",
    "Peticions que s'estan atenent ara mateix (saturació)",
    ["servei"],
)


@contextmanager
def mesurar(servei: str, metode: str):
    """Mesura la durada d'un bloc i la registra amb el codi de sortida.

    Ús:
        with mesurar("comandes", "POST /comandes") as ctx:
            ...
            ctx["codi"] = "201"
    El codi es fixa dins del bloc; si salta una excepció s'anota 500.
    """
    ctx = {"codi": "200"}
    PETICIONS_EN_VOL.labels(servei=servei).inc()
    inici = time.perf_counter()
    try:
        yield ctx
    except Exception:
        ctx["codi"] = "500"
        raise
    finally:
        durada = time.perf_counter() - inici
        PETICIO_DURADA.labels(
            servei=servei, metode=metode, codi=ctx["codi"]
        ).observe(durada)
        PETICIONS_EN_VOL.labels(servei=servei).dec()


def exposar_metriques(port: int = 9464) -> None:
    """Arrenca un servidor HTTP mínim amb /metrics en un fil a part.

    El fan servir els serveis que no tenen servidor HTTP propi (inventari, repartiment).
    Els que sí que en tenen (comandes, cataleg) munten /metrics a la seva app.
    """
    start_http_server(port)

Punts que convé entendre:

  • Counter, Gauge i Histogram són registres globals del procés: es declaren un cop, a nivell de mòdul, i qualsevol part del servei els fa servir. prometheus_client els exposa tots junts a /metrics.
  • .labels(...) selecciona la sèrie concreta; la primera vegada que es fa servir una combinació, es crea. Per això cal controlar quins valors s'hi passen (cardinalitat).
  • mesurar registra el codi també quan hi ha excepció: sense aquest except, les peticions que fallen amb error intern no apareixerien a l'histograma, i la latència semblaria millor del que és.
  • PETICIONS_EN_VOL és el senyal de saturació més barat d'obtenir: quantes peticions hi ha alhora. Si a comandes supera el nombre de fils del servidor, les següents esperen a la cua.

4.2 Interceptor gRPC a inventari

inventari ja té AuthInterceptor i ServeiInterceptor (06-04). Se n'hi afegeix un més, que embolcalla cada crida i tradueix el codi d'estat gRPC a etiqueta:

# km0/serveis/inventari/metriques_interceptor.py
import grpc
from serveis.comu.metriques import mesurar


class MetriquesInterceptor(grpc.ServerInterceptor):
    """Registra durada i codi de cada RPC del servei inventari."""

    def intercept_service(self, continuation, handler_call_details):
        # handler_call_details.method és "/km0.inventari.v1.Inventari/ReservarEstoc"
        metode = handler_call_details.method.rsplit("/", 1)[-1]
        handler = continuation(handler_call_details)
        if handler is None or not handler.unary_unary:
            return handler  # streaming: es mesuraria d'una altra manera

        original = handler.unary_unary

        def embolcallat(peticio, context):
            with mesurar("inventari", metode) as ctx:
                try:
                    resposta = original(peticio, context)
                    # Si el handler ha cridat context.abort(), no arribem aquí.
                    ctx["codi"] = "OK"
                    return resposta
                except grpc.RpcError as e:
                    ctx["codi"] = e.code().name  # UNAVAILABLE, NOT_FOUND, ...
                    raise

        return grpc.unary_unary_rpc_method_handler(
            embolcallat,
            request_deserializer=handler.request_deserializer,
            response_serializer=handler.response_serializer,
        )

I a l'arrencada del servidor, l'ordre dels interceptors importa: mètriques primer, per mesurar també les crides rebutjades per autenticació.

# km0/serveis/inventari/servidor.py (fragment)
from serveis.comu.metriques import exposar_metriques, ESTOC_UNITATS

servidor = grpc.server(
    futures.ThreadPoolExecutor(max_workers=32),
    interceptors=[MetriquesInterceptor(), AuthInterceptor(), ServeiInterceptor()],
)
exposar_metriques(port=9464)

# Després de cada ajust o reserva confirmada, el servei refresca el gauge:
def publicar_estoc(producte: str, unitats: int) -> None:
    if producte in PRODUCTES_EN_CAMPANYA:  # tomaquet-rosa, formatge-curat, vi-crianca...
        ESTOC_UNITATS.labels(producte=producte).set(unitats)

4.3 Middleware HTTP a comandes i endpoint /metrics

comandes és una aplicació FastAPI; un middleware mesura cada petició i /metrics es munta a la mateixa app:

# km0/serveis/comandes/app.py (fragment)
from fastapi import FastAPI, Request
from prometheus_client import make_asgi_app
from serveis.comu.metriques import mesurar, COMANDES_TOTAL

app = FastAPI()
app.mount("/metrics", make_asgi_app())  # exposa totes les mètriques del procés


@app.middleware("http")
async def middleware_metriques(request: Request, call_next):
    # Normalitzar la ruta: /comandes/P-2026-000123 -> /comandes/{id}
    ruta = request.scope.get("route")
    plantilla = ruta.path if ruta else "desconeguda"
    with mesurar("comandes", f"{request.method} {plantilla}") as ctx:
        resposta = await call_next(request)
        ctx["codi"] = str(resposta.status_code)
        return resposta


@app.post("/comandes", status_code=201)
async def crear_comanda(comanda: ComandaEntrada):
    resultat = await saga_comanda.executar(comanda)   # 03-05
    COMANDES_TOTAL.labels(estat=resultat.estat).inc()
    return resultat

L'ús de la plantilla de la ruta (/comandes/{id}) en lloc de la URL real és l'aplicació pràctica de la regla de cardinalitat.

4.4 observabilitat/prometheus.yml

# km0/observabilitat/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s     # cada quant s'avaluen les regles d'alerta
  external_labels:
    entorn: produccio

rule_files:
  - /etc/prometheus/alertes.yml

alerting:
  alertmanagers:
    - static_configs:
        - targets: ["alertmanager:9093"]

scrape_configs:
  - job_name: comandes
    static_configs:
      - targets: ["comandes-1:8000", "comandes-2:8000", "comandes-3:8000"]
  - job_name: cataleg
    static_configs:
      - targets: ["cataleg-1:8000", "cataleg-2:8000"]
  - job_name: inventari
    static_configs:
      - targets: ["inventari-1:9464", "inventari-2:9464"]
  - job_name: repartiment
    static_configs:
      - targets: ["repartiment:9464"]

  - job_name: postgres
    static_configs:
      - targets: ["postgres-exporter-inventari:9187",
                  "postgres-exporter-comandes-primari:9187",
                  "postgres-exporter-comandes-replica:9187"]
  - job_name: kafka
    static_configs:
      - targets: ["kafka-exporter:9308"]
  - job_name: redis
    static_configs:
      - targets: ["redis-exporter:9121"]
  - job_name: nodes
    static_configs:
      - targets: ["node-exporter:9100"]

  # Sondes sintètiques: Prometheus demana al blackbox_exporter que comprovi cada URL.
  - job_name: blackbox_http
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
          - https://api.km0.example/api/v1/cataleg/mercats
          - https://api.km0.example/salut/viu
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target      # la URL passa com a paràmetre ?target=
      - source_labels: [__param_target]
        target_label: instance            # i es conserva com a etiqueta
      - target_label: __address__
        replacement: blackbox-exporter:9115   # a qui es fa el scrape realment

A docker-compose.yml la pila d'observabilitat s'afegeix al costat dels serveis:

# km0/docker-compose.yml (fragment)
  prometheus:
    image: prom/prometheus:v2.53.0
    volumes:
      - ./observabilitat/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./observabilitat/alertes.yml:/etc/prometheus/alertes.yml:ro
      - prometheus-dades:/prometheus
    command: ["--config.file=/etc/prometheus/prometheus.yml",
              "--storage.tsdb.retention.time=30d"]
    ports: ["9090:9090"]
  alertmanager:
    image: prom/alertmanager:v0.27.0
    volumes:
      - ./observabilitat/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
    ports: ["9093:9093"]
  grafana:
    image: grafana/grafana:11.1.0
    volumes:
      - ./observabilitat/grafana/provisioning:/etc/grafana/provisioning:ro
      - ./observabilitat/grafana/dashboards:/var/lib/grafana/dashboards:ro
    environment:
      GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin
    ports: ["3000:3000"]
  postgres-exporter-inventari:
    image: prometheuscommunity/postgres-exporter:v0.15.0
    environment:
      DATA_SOURCE_NAME: "postgresql://exporter@inventari-db:5432/km0_inventari?sslmode=require"
  kafka-exporter:
    image: danielqsj/kafka-exporter:v1.7.0
    command: ["--kafka.server=kafka-1:9092", "--kafka.server=kafka-2:9092"]
  redis-exporter:
    image: oliver006/redis_exporter:v1.62.0
    environment:
      REDIS_ADDR: "redis:6379"
  node-exporter:
    image: prom/node-exporter:v1.8.2
    pid: host
  blackbox-exporter:
    image: prom/blackbox-exporter:v0.25.0

L'usuari exporter de PostgreSQL és de només lectura sobre les vistes pg_stat_*; en un desplegament real s'obtindria de Vault (06-04).

  1. PromQL essencial: taxes, percentils i agregacions

PromQL és el llenguatge de consulta. Quatre construccions cobreixen el 90 % dels usos:

rate(counter[finestra]): taxa per segon dins la finestra. Peticions per segon a comandes, sumant les tres rèpliques i desglossant per mètode:

sum by (metode) (rate(km0_peticio_durada_segons_count{servei="comandes"}[5m]))

Es fa servir el sufix _count de l'histograma (nombre d'observacions) com a comptador de peticions: cada petició mesurada és una observació.

Taxa d'error: fracció de peticions amb codi 5xx:

sum(rate(km0_peticio_durada_segons_count{servei="comandes", codi=~"5.."}[5m]))
/
sum(rate(km0_peticio_durada_segons_count{servei="comandes"}[5m]))

histogram_quantile(q, buckets): percentil a partir dels buckets. El p99 de comandes, agregant totes les instàncies:

histogram_quantile(
  0.99,
  sum by (le) (rate(km0_peticio_durada_segons_bucket{servei="comandes", metode="POST /comandes"}[5m]))
)

El sum by (le) és obligatori: cal sumar els buckets de totes les instàncies conservant l'etiqueta le (límit del bucket), que és la que histogram_quantile necessita per interpolar.

increase(counter[finestra]): quant ha pujat dins la finestra (útil per a "comandes en la darrera hora"):

sum(increase(km0_comandes_total{estat="confirmada"}[1h]))

Altres que apareixen a diari: avg_over_time(gauge[10m]) per suavitzar un gauge, absent(up{job="comandes"}) per detectar que no hi ha cap objectiu, topk(5, ...) per als cinc productes amb menys estoc, i operadors entre vectors (/, and, unless) que aparellen sèries per etiquetes.

Percentils contra mitjanes

Amb 1 000 peticions de 50 ms i 10 de 5 s, la mitjana és 99 ms: "tot bé". El p99 és 5 s: un de cada cent clients espera cinc segons. En un sistema distribuït, on una pàgina del catàleg fa diverses crides internes, la latència de cua s'amplifica: si cada crida té un 1 % de probabilitat de ser lenta i una petició en fa 10, un 10 % de les peticions veuen almenys una crida lenta. Per això els objectius es fixen sobre percentils (p50 per a "el que és normal", p99 per a "el que pateix algú cada minut") i mai sobre mitjanes.

  1. SLI, SLO, SLA i pressupost d'error

A 01-03 es van calcular els "nous": un servei amb un 99,9 % de disponibilitat pot estar caigut 43 minuts al mes. Aquell càlcul parlava d'"estar caigut"; ara hi ha instruments per definir-ho amb precisió.

  • SLI (Service Level Indicator): una mesura concreta del servei, expressada com a fracció d'esdeveniments bons sobre el total. "Fracció de peticions a POST /comandes que responen sense 5xx en menys de 500 ms."
  • SLO (Service Level Objective): l'objectiu intern per a aquest SLI en una finestra. "El 99,9 % de les peticions, mesurat en finestres de 30 dies."
  • SLA (Service Level Agreement): el compromís contractual amb conseqüències (penalitzacions). Sempre més lax que l'SLO: Quilòmetre Zero promet als productors un 99,5 % i s'exigeix un 99,9 %.

Els SLO del servei comandes:

SLI SLO Com es calcula en PromQL
Disponibilitat: peticions sense 5xx 99,9 % en 30 dies 1 - (sum(rate(..._count{codi=~"5.."}[30d])) / sum(rate(..._count[30d])))
Latència: peticions amb durada < 500 ms 99 % en 30 dies (p99 < 500 ms) sum(rate(..._bucket{le="0.5"}[30d])) / sum(rate(..._count[30d]))

Fixeu-vos com l'histograma permet expressar l'SLO de latència com una fracció: "quantes observacions han caigut al bucket le="0.5"" dividit pel total és exactament "fracció de peticions per sota de 500 ms". Això només funciona si 0,5 és un límit de bucket, i per això BUCKETS_LATENCIA inclou 0.5 i no, per exemple, 0.4 i 0.6.

Pressupost d'error

Un SLO del 99,9 % significa que es permet un 0,1 % de peticions dolentes: aquest és el pressupost d'error. Amb 2 milions de peticions al mes, són 2 000 errors. El pressupost converteix la fiabilitat en una moneda que es pot gastar:

  • Si queda pressupost, l'equip desplega ràpid, prova coses, fa experiments de caos (07-06).
  • Si el pressupost s'esgota (un incident de 40 minuts se l'endú gairebé sencer), es congelen els desplegaments no urgents i s'inverteix en fiabilitat.

És també el que dona sentit a les alertes: no importa un pic d'errors en si mateix, importa a quina velocitat es consumeix el pressupost.

  1. Alertes: símptomes, burn rate i Alertmanager

7.1 Alertar per símptomes, no per causes

Una alerta ha de despertar algú només quan un usuari està patint o patirà, i aquest algú ha de poder fer-hi alguna cosa. "CPU al 85 % a inventari-2" no compleix ni una cosa ni l'altra: pot ser normal durant la Setmana de la Verema, i no diu què fer. "El 2 % de les comandes falla des de fa 5 minuts" sí. Les causes (CPU, memòria, lag) van als dashboards, per diagnosticar quan l'alerta de símptoma salta, o es converteixen en alertes de baixa prioritat (ticket, no trucada).

Les excepcions raonables són les alertes predictives sobre causes amb termini cert: un certificat que caduca d'aquí a 12 h o un disc que s'omplirà d'aquí a 4 h al ritme actual (predict_linear) són avisos que eviten el símptoma.

7.2 Burn rate multifinestra

El burn rate és la velocitat a la qual es consumeix el pressupost d'error, relativa a la velocitat "justa". Burn rate 1 significa que, al ritme actual, el pressupost de 30 dies s'esgota exactament en 30 dies. Burn rate 14,4 significa que s'esgota en 2 dies. La tècnica que recomana el llibre SRE Workbook és alertar amb dues finestres alhora, llarga i curta:

Severitat Finestra llarga Finestra curta Burn rate Pressupost consumit abans d'alertar S'esgota en
Pàgina (trucada) 1 h 5 min 14,4 2 % ~2 dies
Pàgina 6 h 30 min 6 5 % ~5 dies
Ticket 3 dies 6 h 1 10 % 30 dies

La finestra llarga evita que un pic de 30 segons desperti ningú; la finestra curta fa que l'alerta es resolgui tan bon punt el problema s'atura (sense ella, l'alerta d'1 h continuaria activa una hora després d'haver-ho arreglat).

7.3 observabilitat/alertes.yml

# km0/observabilitat/alertes.yml
groups:
  - name: slo_comandes
    rules:
      # Regles d'enregistrament: precalculen la taxa d'error en diverses finestres.
      # Nom convencional: nivell:metrica:operacio_finestra
      - record: job:km0_comandes_taxa_error:ratio_rate5m
        expr: |
          sum(rate(km0_peticio_durada_segons_count{servei="comandes",codi=~"5.."}[5m]))
          / sum(rate(km0_peticio_durada_segons_count{servei="comandes"}[5m]))
      - record: job:km0_comandes_taxa_error:ratio_rate1h
        expr: |
          sum(rate(km0_peticio_durada_segons_count{servei="comandes",codi=~"5.."}[1h]))
          / sum(rate(km0_peticio_durada_segons_count{servei="comandes"}[1h]))
      - record: job:km0_comandes_taxa_error:ratio_rate30m
        expr: |
          sum(rate(km0_peticio_durada_segons_count{servei="comandes",codi=~"5.."}[30m]))
          / sum(rate(km0_peticio_durada_segons_count{servei="comandes"}[30m]))
      - record: job:km0_comandes_taxa_error:ratio_rate6h
        expr: |
          sum(rate(km0_peticio_durada_segons_count{servei="comandes",codi=~"5.."}[6h]))
          / sum(rate(km0_peticio_durada_segons_count{servei="comandes"}[6h]))

      # SLO 99,9 % => pressupost 0,001. Burn rate 14,4 => taxa d'error > 0,0144
      - alert: ComandesBurnRateRapid
        expr: |
          job:km0_comandes_taxa_error:ratio_rate1h > (14.4 * 0.001)
          and
          job:km0_comandes_taxa_error:ratio_rate5m > (14.4 * 0.001)
        for: 2m
        labels:
          severitat: pagina
          servei: comandes
        annotations:
          resum: "comandes consumeix el pressupost d'error 14x més ràpid del permès"
          descripcio: "Taxa d'error 1h = {{ $value | humanizePercentage }}. Runbook: runbooks/comandes-errors.md"

      - alert: ComandesBurnRateLent
        expr: |
          job:km0_comandes_taxa_error:ratio_rate6h > (6 * 0.001)
          and
          job:km0_comandes_taxa_error:ratio_rate30m > (6 * 0.001)
        for: 15m
        labels:
          severitat: pagina
          servei: comandes
        annotations:
          resum: "comandes consumeix el pressupost d'error 6x més ràpid del permès (6h)"

      - alert: ComandesLatenciaP99
        expr: |
          histogram_quantile(0.99, sum by (le) (
            rate(km0_peticio_durada_segons_bucket{servei="comandes",metode="POST /comandes"}[10m])
          )) > 0.5
        for: 10m
        labels:
          severitat: pagina
          servei: comandes
        annotations:
          resum: "p99 de POST /comandes per sobre de 500 ms durant 10 minuts"

  - name: plataforma
    rules:
      - alert: KafkaLagRepartiment
        expr: sum by (consumergroup) (kafka_consumergroup_lag{topic="repartiment.posicions"}) > 5000
        for: 5m
        labels: {severitat: pagina, servei: repartiment}
        annotations:
          resum: "El panell de repartiment va amb retard: lag {{ $value }} a repartiment.posicions"

      - alert: KafkaLagAnalitica
        expr: sum by (consumergroup) (kafka_consumergroup_lag{topic="comandes.esdeveniments", consumergroup="analitica"}) > 200000
        for: 30m
        labels: {severitat: ticket, servei: analitica}
        annotations:
          resum: "analitica acumula lag a comandes.esdeveniments (no urgent, però cal revisar-ho)"

      - alert: PostgresConnexionsAltes
        expr: pg_stat_activity_count / pg_settings_max_connections > 0.85
        for: 5m
        labels: {severitat: ticket}
        annotations:
          resum: "{{ $labels.instance }} fa servir més del 85 % de les seves connexions"

      - alert: ObjectiuCaigut
        expr: up == 0
        for: 3m
        labels: {severitat: pagina}
        annotations:
          resum: "{{ $labels.job }}/{{ $labels.instance }} no respon al scrape"

  - name: seguretat_operativa
    rules:
      # blackbox_exporter publica la data de caducitat del certificat que veu en sondejar
      - alert: CertificatCaducaAviat
        expr: (probe_ssl_earliest_cert_expiry - time()) < 12 * 3600
        labels: {severitat: pagina}
        annotations:
          resum: "El certificat de {{ $labels.instance }} caduca d'aquí a menys de 12 h"

      # Cada servei exposa km0_vault_lease_segons_restants (gauge) per a les seves credencials dinàmiques
      - alert: VaultLeaseNoRenovat
        expr: km0_vault_lease_segons_restants < 900
        for: 5m
        labels: {severitat: pagina}
        annotations:
          resum: "{{ $labels.servei }} té un lease de Vault a menys de 15 min i no es renova"

      - alert: AuditoriaCadenaTrencada
        expr: increase(km0_auditoria_verificacions_fallides_total[15m]) > 0
        labels: {severitat: pagina}
        annotations:
          resum: "La verificació de la cadena d'auditoria ha fallat (06-05)"

Sobre aquestes regles:

  • Les regles d'enregistrament (record:) calculen un sol cop l'expressió cara i la desen com a mètrica nova; les alertes i els dashboards la reutilitzen. Amb finestres de 6 h sobre histogrames, fer-ho així estalvia molta feina a Prometheus.
  • for: exigeix que la condició es mantingui aquest temps abans de disparar-se. És el primer filtre contra alertes per parpelleigs.
  • Els certificats de 24 h de Vault (06-04) fan que l'alerta a 12 h sigui el llindar natural: si a mitja vida no s'ha renovat, la renovació automàtica està trencada.
  • km0_vault_lease_segons_restants i km0_auditoria_verificacions_fallides_total són mètriques que publiquen els mateixos serveis: la instrumentació de negoci i de seguretat fa servir el mateix mecanisme que la de latència.

7.4 Alertmanager: rutes, agrupació, silencis

Prometheus avalua i dispara; Alertmanager decideix a qui avisar, com agrupar i quan callar:

# km0/observabilitat/alertmanager.yml
global:
  resolve_timeout: 5m

route:
  receiver: operadors-ticket            # destí per defecte
  group_by: [alertname, servei]         # una notificació per alerta+servei, no per instància
  group_wait: 30s                       # espera per agrupar alertes que arriben juntes
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - matchers: [severitat = pagina]
      receiver: operadors-guardia
      repeat_interval: 1h
    - matchers: [servei = analitica]
      receiver: equip-dades

receivers:
  - name: operadors-guardia
    slack_configs:
      - api_url_file: /run/secrets/slack_webhook
        channel: "#km0-operacions"
        title: "[GUÀRDIA] {{ .CommonAnnotations.resum }}"
        text: "{{ range .Alerts }}{{ .Annotations.descripcio }}\n{{ end }}"
    # En producció, a més: pagerduty_configs / opsgenie_configs per trucar al mòbil
  - name: operadors-ticket
    email_configs:
      - to: [email protected]
  - name: equip-dades
    slack_configs:
      - api_url_file: /run/secrets/slack_webhook
        channel: "#km0-dades"

inhibit_rules:
  # Si un objectiu està caigut, no avisar a més de la seva latència o dels seus errors
  - source_matchers: [alertname = ObjectiuCaigut]
    target_matchers: [severitat = pagina]
    equal: [servei]

Silencis: quan en Jordi ha de reiniciar inventari-2 per a una migració, crea un silenci de 30 minuts amb un matcher (instance="inventari-2:9464") des de la interfície o amb amtool silence add instance=inventari-2:9464 --duration=30m --comment="migració". Sense silencis, l'alternativa és ignorar alertes, que és el principi de la fatiga d'alertes: quan la meitat de les notificacions són soroll, ningú no llegeix la que importa. Regles pràctiques: cada alerta de pàgina ha de tenir un runbook (07-03) i una acció possible; una alerta que salta i "s'arregla sola" tres cops seguits es degrada a ticket o s'elimina; el nombre de pàgines per setmana es revisa com una mètrica més.

  1. Dashboards a Grafana i monitoratge actiu

8.1 Què posar al panell de la Setmana de la Verema

Un dashboard no és una col·lecció de tot el que es pot graficar; és una narració de dalt a baix: primer si el negoci va bé, després si els serveis compleixen, després per què. Per a la campanya del Celler Roure Alt:

Fila Panells Font
Negoci Comandes/min (total i amb vi-crianca); taxa de pagament rebutjat; pressupost d'error restant del mes (%) km0_comandes_total, regles d'enregistrament
SLO de comandes Taxa d'error 5m/1h amb línia al 0,1 %; p50/p95/p99 amb línia a 500 ms; burn rate actual regles d'enregistrament, histogram_quantile
Dependències p99 de ReservarEstoc; estoc de vi-crianca a inv-bcn/inv-vlc; hit ratio de Redis; lag de comandes.esdeveniments per grup inventari, km0_estoc_unitats, exporters
Saturació km0_peticions_en_vol per servei; connexions de PostgreSQL; CPU dels brokers gauges, exporters
Vora 429 per minut a Kong (06-05); sondes sintètiques verdes/vermelles Kong, blackbox

Grafana es provisiona des de fitxers perquè el dashboard visqui al repositori, no en clics:

# km0/observabilitat/grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    url: http://prometheus:9090
    isDefault: true

# km0/observabilitat/grafana/provisioning/dashboards/km0.yml
apiVersion: 1
providers:
  - name: km0
    folder: Quilòmetre Zero
    type: file
    options:
      path: /var/lib/grafana/dashboards

I un dashboard JSON reduït a dos panells, per veure'n l'estructura:

{
  "title": "KM0 - Setmana de la Verema",
  "uid": "km0-verema",
  "refresh": "30s",
  "panels": [
    {
      "type": "stat", "title": "Comandes / min", "gridPos": {"x": 0, "y": 0, "w": 6, "h": 4},
      "targets": [{"expr": "sum(rate(km0_comandes_total{estat=\"confirmada\"}[5m])) * 60"}]
    },
    {
      "type": "timeseries", "title": "POST /comandes p50 / p95 / p99", "gridPos": {"x": 6, "y": 0, "w": 18, "h": 8},
      "fieldConfig": {"defaults": {"unit": "s", "thresholds": {"steps": [
        {"color": "green", "value": null}, {"color": "red", "value": 0.5}]}}},
      "targets": [
        {"legendFormat": "p50", "expr": "histogram_quantile(0.50, sum by (le) (rate(km0_peticio_durada_segons_bucket{servei=\"comandes\",metode=\"POST /comandes\"}[5m])))"},
        {"legendFormat": "p95", "expr": "histogram_quantile(0.95, sum by (le) (rate(km0_peticio_durada_segons_bucket{servei=\"comandes\",metode=\"POST /comandes\"}[5m])))"},
        {"legendFormat": "p99", "expr": "histogram_quantile(0.99, sum by (le) (rate(km0_peticio_durada_segons_bucket{servei=\"comandes\",metode=\"POST /comandes\"}[5m])))"}
      ]
    }
  ]
}

8.2 Monitoratge passiu i actiu

Tot l'anterior és passiu: mesura el trànsit que hi ha. Si a les 04:00 no hi ha trànsit, la taxa d'error és 0/0 i no diu res; si Kong té malament una ruta que encara ningú no ha fet servir, tampoc. El monitoratge actiu (sondes sintètiques) genera trànsit artificial per comprovar el camí complet: el blackbox_exporter demana GET /api/v1/cataleg/mercats a través de Kong cada 15 s i publica probe_success, probe_duration_seconds i probe_ssl_earliest_cert_expiry. Una sonda més ambiciosa (un petit script que crea i cancel·la una comanda de prova amb l'usuari [email protected], marcada perquè no compti a analítica) verifica la saga sencera. Aquestes sondes són les que detecten el certificat caducat del diumenge abans que l'Anna.

8.3 OpenTelemetry Metrics

Tot el que hem vist fa servir prometheus_client directament. OpenTelemetry és l'estàndard obert que unifica mètriques, logs i traces en un SDK i un protocol (OTLP): amb ell, km0_peticio_durada_segons seria un Histogram del MeterProvider d'OTel, exportat a un Collector que el lliura a Prometheus (o a qualsevol altre backend). La semàntica (tipus, etiquetes, cardinalitat, PromQL) és la mateixa. A Quilòmetre Zero es manté prometheus_client per a mètriques perquè és més simple, i s'adopta OpenTelemetry per a les traces i la correlació amb logs a 07-02, que és on el seu valor és més gran.

Errors Comuns i Consells

  • Mesurar només infraestructura. CPU i memòria no diuen si l'Anna pot comprar. Comença per RED a cada servei i per dues mètriques de negoci; afegeix USE de recursos com a suport al diagnòstic.
  • Etiquetes d'alta cardinalitat. comanda_id, usuari, IPs o rutes sense normalitzar multipliquen les sèries fins a tombar Prometheus. Si ho necessites per investigar, va a logs o traces.
  • Summaries en serveis replicats. Els quantils calculats al client no es poden agregar entre instàncies. Fes servir histogrames amb buckets que incloguin els llindars dels teus SLO.
  • Alertar sobre mitjanes o sobre causes. La mitjana amaga la cua; la CPU al 80 % no és un problema en si mateixa. Alerta sobre SLI d'usuari i burn rate; la resta, a dashboards o tickets.
  • Alertes sense for: ni runbook. Cada parpelleig desperta algú i ningú no sap què fer. Tota alerta de pàgina té un for, un responsable i un runbook enllaçat a l'anotació.
  • Oblidar les excepcions al middleware. Si la mètrica només es registra quan la petició acaba bé, la latència i els errors de les que fallen desapareixen. Registra al finally.
  • Confiar només en el monitoratge passiu. Sense trànsit no hi ha senyal. Una sonda sintètica per camí crític (catàleg, crear comanda, login) cobreix les hores vall i els canvis de configuració no provats.
  • Dashboards fets a mà a Grafana. Es perden, es dupliquen, ningú no sap quin és el bo. Provisiona'ls des del repositori, al costat del codi que instrumenten.

Exercicis

Exercici 1. Durant la Setmana del Formatge Artesà, la Formatgeria Montblanc pregunta quantes comandes amb formatge-curat s'han confirmat en la darrera hora, i la Marta vol saber si inventari està responent pitjor que ahir. (a) Es pot respondre la primera pregunta amb km0_comandes_total{estat="confirmada"} tal com està definida? Si no, què canviaries i quin risc de cardinalitat tindria? (b) Escriu la consulta PromQL que dona el p95 de ReservarEstoc en els darrers 5 minuts agregant les dues instàncies d'inventari, i explica per què no serveix avg(histogram_quantile(...)) per instància. (c) Quin gauge et diria si el problema és a les rèpliques inv-bcn/inv-vlc en lloc del servei?

Exercici 2. L'SLO de comandes és un 99,9 % d'èxit en 30 dies i el servei rep de mitjana 3 milions de peticions al mes. (a) Quants errors caben al pressupost? (b) A les 10:00 del dissabte comença un incident en què el 8 % de les peticions falla. Amb un trànsit de 100 peticions/s, en quant de temps s'esgota el pressupost sencer? (c) Quina de les dues alertes de burn rate (14,4 en 1 h/5 min; 6 en 6 h/30 min) saltarà primer, i aproximadament quan, tenint en compte els for:? (d) A les 10:20 l'incident es resol; per què es resol l'alerta poc després encara que la finestra d'1 h continuï contaminada?

Exercici 3. Un dilluns en Jordi troba 47 notificacions del cap de setmana a #km0-operacions: 30 de PostgresConnexionsAltes a la rèplica de comandes (que puja al 90 % cada nit durant el DAG km0_vendes_diaries i baixa sola), 12 d'ObjectiuCaigut per a repartiment durant un desplegament programat de 10 minuts, i 5 de KafkaLagAnalitica. Cap no corresponia a un problema real. Proposa, per a cada grup, el canvi concret (a alertes.yml, a alertmanager.yml o al procediment) que evita el soroll sense perdre la detecció d'un problema real, i explica quin senyal de símptoma cobriria el cas en què sí que ho fos.

Solucions

Exercici 1.

(a) No: km0_comandes_total només té l'etiqueta estat, no sap quins productes portava cada comanda. Dues opcions: un counter a part km0_linies_comanda_total{producte} incrementat per cada línia confirmada (cardinalitat = nombre de productes, acceptable si es limita als productes en campanya o si el catàleg és d'uns centenars; amb desenes de milers de productes caldria agregar per productor o respondre aquesta pregunta des d'analitica, no des de Prometheus), o afegir producte a km0_comandes_total, que és pitjor perquè una comanda amb tres productes es comptaria tres vegades i trencaria la mètrica de comandes. Mai comanda_id. (b) histogram_quantile(0.95, sum by (le) (rate(km0_peticio_durada_segons_bucket{servei="inventari", metode="ReservarEstoc"}[5m]))). Sumar primer els buckets de les dues instàncies per le reconstrueix la distribució conjunta i el percentil es calcula sobre ella; fer la mitjana de dos p95 calculats per separat no dona el p95 del conjunt (si una instància atén el 90 % del trànsit amb p95 = 40 ms i l'altra el 10 % amb p95 = 800 ms, la mitjana, 420 ms, no descriu ningú). (c) El retard de replicació que publica postgres_exporter (pg_replication_lag_seconds o l'equivalent sobre pg_stat_replication) per a inv-bcn i inv-vlc; si aquest gauge puja, les lectures a la rèplica retornen estoc vell i inventari pot estar reintentant o esperant; si és a zero, el problema és al servei o al primari.

Exercici 2.

(a) 0,1 % de 3 000 000 = 3 000 errors al mes. (b) 100 peticions/s × 8 % = 8 errors/s; 3 000 / 8 = 375 s ≈ 6 minuts i quart. Tot el pressupost del mes se'n va en poc més de sis minuts. (c) Burn rate = 0,08 / 0,001 = 80, molt per sobre de tots dos llindars. La finestra de 5 min supera l'1,44 % gairebé immediatament; la d'1 h necessita acumular: amb l'hora prèvia neta, la taxa d'error en 1 h arriba a l'1,44 % quan porten uns 11 minuts d'incident (0,08 × t / 60 min = 0,0144 → t ≈ 10,8 min); amb for: 2m, ComandesBurnRateRapid avisa cap a les 10:13. La de 6 h necessitaria 0,006 × 360 / 0,08 = 27 minuts més for: 15m, així que no arriba a saltar si l'incident dura 20 minuts. (d) Perquè la regla exigeix les dues finestres alhora (and): en resoldre's, la finestra de 5 min baixa per sota del llindar en uns minuts, i encara que la d'1 h continuï per sobre fins a les 11:20, la conjunció deixa de complir-se i Alertmanager marca l'alerta com a resolta. Aquesta és precisament la raó de ser de la finestra curta.

Exercici 3.

PostgresConnexionsAltes a la rèplica durant el DAG: és una causa, no un símptoma, i és un comportament esperat; opcions: pujar el llindar només per a la rèplica amb un unless/matcher per instance, afegir for: 30m (el DAG dura menys), o millor, degradar-la a severitat: ticket (ja ho és) i encaminar els tickets a un resum diari en lloc de a Slack en temps real (group_interval/repeat_interval llargs a la ruta per defecte); el símptoma real que sí que ha d'avisar és la latència p99 de comandes o el retard de replicació, que estan coberts per ComandesLatenciaP99. ObjectiuCaigut en desplegaments: el procediment de desplegament ha de crear un silenci (amtool silence add job=repartiment --duration=15m --comment="desplegament") com a pas del pipeline; a més, amb diverses rèpliques l'alerta hauria de ser "totes les instàncies del job caigudes" (absent(up{job="repartiment"} == 1) o count(up{job="repartiment"} == 1) == 0), no una instància; el símptoma que cobreix un desplegament realment trencat és el lag de repartiment.posicions (KafkaLagRepartiment) i la sonda sintètica del panell. KafkaLagAnalitica: 200 000 amb for: 30m és massa sensible per a un consumidor batch que es posa al dia cada nit; pujar el llindar, allargar for a diverses hores, o canviar-la per una alerta sobre el símptoma d'analítica: "el DAG km0_vendes_diaries no ha acabat a les 07:00" (mètrica d'Airflow) o "la darrera execució ha fallat". En tots els casos, el principi és el mateix: l'alerta de pàgina protegeix una experiència d'usuari concreta; la resta és informació per al diagnòstic.

Conclusió

Un sistema distribuït no avisa quan falla; cal construir els instruments. L'observabilitat es recolza en tres senyals i aquesta lliçó n'ha desenvolupat el primer: les mètriques, l'únic que escala amb el nombre de sèries i no amb el trànsit. Hem vist què mesurar en quatre capes (infraestructura amb USE, plataforma, aplicació amb RED i els senyals daurats, negoci), els quatre tipus de mètrica i per què l'histograma és el tipus adequat per a latències en serveis replicats, la regla de cardinalitat que prohibeix comanda_id com a etiqueta, el model pull de Prometheus amb els seus exporters i sondes, i el PromQL imprescindible (rate, histogram_quantile amb sum by (le), increase). Sobre aquesta base, els "nous" de 01-03 s'han convertit en SLI i SLO concrets per a comandes (99,9 % d'èxit, p99 < 500 ms), amb un pressupost d'error que es gasta i alertes de burn rate multifinestra que avisen per símptomes, encaminades per Alertmanager a qui pot actuar, i un dashboard provisionat que explica la Setmana de la Verema de dalt a baix.

Ara, quan ComandesBurnRateRapid salti un dissabte a les 10:13, en Jordi sabrà que el 8 % de les comandes falla. El que les mètriques no li diran és quines, ni per què: si és inventari que rebutja reserves de vi-crianca, la passarel·la de pagaments, o un traceparent que s'ha perdut en una capçalera de Kafka. Per a això calen els altres dos pilars: els logs estructurats i centralitzats, que expliquen què va passar amb la comanda P-2026-000125, i les traces distribuïdes, que mostren en quin servei se'n van anar els 480 ms. Aquesta és la lliçó següent.

Curs d'Arquitectures Distribuïdes

Mòdul 1: Introducció als Sistemes Distribuïts

Mòdul 2: Comunicació en Sistemes Distribuïts

Mòdul 3: Consistència i Replicació

Mòdul 4: Emmagatzematge Distribuït

Mòdul 5: Computació Distribuïda

Mòdul 6: Seguretat en Sistemes Distribuïts

Mòdul 7: Monitoratge i Manteniment

Mòdul 8: Casos d'Estudi i Aplicacions

© Copyright 2026. Tots els drets reservats