Al final de la lliçó anterior, Rutas Norte tenia per fi memòria: Prometheus desa trenta dies de mètriques de tots els components i li podem preguntar qualsevol cosa en PromQL. Però va quedar un problema obert: tot això viu en una interfície austera on cal escriure consultes a mà, i ningú l'està mirant a les tres de la matinada. Una dada que ningú veu i de la qual ningú rep avís no ha resolt res.

Aquesta lliçó fa aquest salt: de la consulta solta al quadre de comandament que explica una història d'un cop d'ull, i d'aquí a l'alerta que desperta algú perquè de debò cal. Veurem Grafana per visualitzar, PrometheusRule per definir quan alguna cosa va malament, i Alertmanager per decidir a qui s'avisa, quan i amb quina agrupació. I sobretot veurem el criteri que separa un sistema d'alertes útil d'un generador de soroll que tothom acaba silenciant: alertar sobre símptomes que percep el client, no sobre causes.

Contingut

  1. Grafana: què és i com es connecta a Prometheus
  2. Anatomia d'un panell i els tipus que de debò es fan servir
  3. Variables de panell: un quadre de comandament per als tres entorns
  4. Importar quadres de comandament de la comunitat
  5. El quadre de comandament de Rutas Norte, panell a panell
  6. Proveir quadres de comandament com a codi
  7. Regles d'alerta amb PrometheusRule i la importància de for
  8. El catàleg d'alertes de Rutas Norte
  9. Alertmanager: rutes, receptors, inhibició i silencis
  10. El criteri: alertar sobre símptomes, no sobre causes
  11. SLO i pressupost d'error de Rutas Norte
  12. Errors comuns i consells
  13. Exercicis

  1. Grafana: què és i com es connecta a Prometheus

Grafana és una eina de visualització que es connecta a fonts de dades —Prometheus, Elasticsearch, PostgreSQL, desenes més— i dibuixa quadres de comandament. No emmagatzema mètriques: només consulta i pinta. Si Prometheus cau, Grafana es queda en blanc.

Aquesta separació de responsabilitats és deliberada i molt sana: Prometheus s'especialitza a recollir, desar i avaluar; Grafana a mostrar. Cadascun fa bé una cosa.

Quan a 07-03 vam instal·lar el kube-prometheus-stack, Grafana va venir inclosa i amb la font de dades ja configurada. El chart crea automàticament la connexió al Service de Prometheus, així que no cal introduir cap URL a mà.

# Comprovar que Grafana està corrent
kubectl -n monitoratge get pods -l app.kubernetes.io/name=grafana
NAME                                   READY   STATUS    RESTARTS   AGE
monitoratge-grafana-6d84b8c7f9-w2xnk   3/3     Running   0          2d4h

Fixa't en el 3/3: a més del contenidor de Grafana hi ha dos sidecars, un que vigila els ConfigMaps de quadres de comandament i un altre els de fonts de dades. El primer serà clau a l'apartat 6.

Accés

kubectl -n monitoratge port-forward svc/monitoratge-grafana 3000:80
# Obrir http://localhost:3000

Les credencials inicials són en un Secret creat pel chart:

kubectl -n monitoratge get secret monitoratge-grafana \
  -o jsonpath='{.data.admin-user}' | base64 -d; echo

kubectl -n monitoratge get secret monitoratge-grafana \
  -o jsonpath='{.data.admin-password}' | base64 -d; echo
admin
canviar-en-produccio

Abans de continuar. Aquell valor el vam posar al fitxer de valors de Helm a 07-03, i a rutas-norte-pro és inacceptable per dos motius: està en text pla en un fitxer versionat a Git, i és una contrasenya feble. En producció cal (a) generar la contrasenya com a Secret extern i referenciar-la amb admin.existingSecret, i (b) millor encara, delegar l'autenticació en el proveïdor d'identitat de l'empresa (OAuth o LDAP), desactivant l'usuari local. Grafana dóna accés de lectura a totes les mètriques de la plataforma, incloses les de negoci.

A rutas-norte-pro publicaríem Grafana amb un Ingress i TLS gestionat per cert-manager, exactament com vam fer a 04-04 i 04-05:

# k8s/entorns/pro/ingress-grafana.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: grafana
  namespace: monitoratge
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-produccio
    nginx.ingress.kubernetes.io/auth-type: basic          # capa extra
    nginx.ingress.kubernetes.io/auth-secret: grafana-basic-auth
spec:
  ingressClassName: nginx
  tls:
    - hosts: [metriques.rutasnorte.example]
      secretName: grafana-tls
  rules:
    - host: metriques.rutasnorte.example
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: monitoratge-grafana
                port:
                  number: 80

Verificar la font de dades

A la interfície: Connections → Data sources → Prometheus. Ha d'aparèixer configurada i amb el botó "Save & test" retornant verd. La URL interna que fa servir és el DNS del Service (04-03):

http://monitoratge-kube-pr-prometheus.monitoratge.svc.cluster.local:9090

Un detall de configuració que convé ajustar: el camp Scrape interval ha de coincidir amb l'interval dels teus ServiceMonitor (30 s en el nostre cas). Grafana el fa servir per calcular la variable $__rate_interval, que veurem de seguida.

  1. Anatomia d'un panell i els tipus que de debò es fan servir

Un panell de Grafana té quatre parts, i entendre-les evita el 90 % dels quadres de comandament illegibles.

  1. La consulta

Una o diverses expressions PromQL. Grafana ofereix dos modes: el constructor visual (útil per aprendre) i el mode codi (el que faràs servir tan bon punt sàpigues PromQL).

Dues variables especials que Grafana substitueix automàticament:

Variable Què és Quan fer-la servir
$__rate_interval Finestra adaptada al zoom i a l'interval d'escaneig Sempre dins de rate()
$__interval Resolució del gràfic segons l'amplada en píxels En increase() sobre finestres variables

Fes servir sempre rate(metrica[$__rate_interval]) en lloc de rate(metrica[5m]). Amb la finestra fixa, en fer zoom a 7 dies el gràfic es torna sorollós o produeix forats; $__rate_interval s'adapta i garanteix que sempre hi hagi almenys quatre mostres a la finestra.

  1. La llegenda amb plantilles d'etiquetes

Per defecte, Grafana mostra la llegenda amb totes les etiquetes de la sèrie:

{__name__="rutasnorte_peticions_total", codi="200", component="api-reserves", entorn="pro", instance="10.244.2.17:9090", job="api-reserves", metode="GET", namespace="rutas-norte-pro", pod="api-reserves-7d9f8c4b5-x2klm", ruta="/api/rutes", versio="2.8.1"}

Illegible. Amb una plantilla al camp Legend:

{{ruta}} · {{codi}}
/api/rutes · 200
/api/reserves · 201
/api/reserves · 422

Regla pràctica: la llegenda ha de cabre en una línia i contenir només el que distingeix aquella sèrie de les altres del panell.

  1. Unitats i llindars

A Standard options → Unit. Aquest ajust és més important del que sembla:

Mètrica Unitat correcta Sense ella es veu
Latència en segons seconds (s) 0.412 en lloc de 412 ms
Taxa d'error (0-1) Percent (0.0-1.0) 0.023 en lloc de 2,3 %
Memòria en bytes bytes (IEC) 536870912 en lloc de 512 MiB
Peticions per segon requests/sec (rps) Un número nu sense context

Els llindars (Thresholds) coloregen el panell segons el valor. Per a la taxa d'error d'api-reserves: verd fins a 0,01, groc de 0,01 a 0,05, vermell per sobre. Un quadre de comandament ben pintat permet detectar el problema des de l'altre costat de l'oficina, sense llegir cap número.

  1. El tipus de visualització

De les desenes que ofereix Grafana, a la pràctica se'n fan servir quatre:

Tipus Quan fer-lo servir Exemple a Rutas Norte
Time series Evolució d'un valor en el temps. El 70 % dels panells Peticions/s, latència p95, memòria
Stat Un número gran, l'estat actual d'un indicador Reserves confirmades l'última hora
Table Comparar l'estat de diverses entitats alhora Estat de tots els pods, amb reinicis i edat
Heatmap Distribució completa d'un histograma en el temps Distribució de latències, no només el p95

Sobre el mapa de calor: és el tipus més infrautilitzat i el que més informació dóna sobre latència. Un p95 de 400 ms pot amagar dues poblacions molt diferents (una massa a 20 ms i una cua a 3 s) o una distribució uniforme. El mapa de calor les distingeix d'un cop d'ull, i s'alimenta directament de les cubetes de l'histograma:

sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn="$entorn"}[$__rate_interval]))

Amb el format de la consulta posat a Heatmap i l'opció Format: Heatmap activada.

Tipus que convé evitar: els indicadors d'agulla (gauge) ocupen molt i diuen poc, i els pastissos són gairebé sempre una mala elecció per a dades temporals.

  1. Variables de panell: un quadre de comandament per als tres entorns

Sense variables, hauries de duplicar el quadre de comandament tres vegades: un per a rutas-norte-dev, un altre per a pre i un altre per a pro. Tres còpies que es desincronitzen a la primera modificació.

Les variables converteixen el quadre de comandament en una plantilla amb desplegables a la part superior.

Definir-les

A Dashboard settings → Variables.

Variable entorn (tipus Query):

Nom:           entorn
Tipus:         Query
Data source:   Prometheus
Query:         label_values(rutasnorte_peticions_total, entorn)
Sort:          Alphabetical (asc)
Multi-value:   No

label_values(metrica, etiqueta) és una funció específica de Grafana que retorna tots els valors diferents que té aquella etiqueta. El desplegable s'omple sol amb dev, pre i pro, i si demà afegim un quart entorn apareixerà automàticament.

Variable namespace (dependent de l'anterior):

Nom:           namespace
Query:         label_values(kube_pod_info{namespace=~"rutas-norte-$entorn"}, namespace)

Variable component (multivalor):

Nom:           component
Query:         label_values(kube_pod_info{namespace="$namespace"}, created_by_name)
Multi-value:   Sí
Include All:   Sí
All value:     .*

Amb Multi-value pots seleccionar-ne diversos alhora; amb Include All, l'opció "All".

Variable interval (tipus Interval, per ajustar la finestra de les taxes):

Nom:     interval
Tipus:   Interval
Valors:  1m,5m,15m,30m,1h,6h,24h

Fer-les servir a les consultes

# Amb una variable simple
sum(rate(rutasnorte_peticions_total{entorn="$entorn"}[$__rate_interval]))

# Amb una variable multivalor: OBLIGATORI fer servir =~ i el format regex
sum by (pod) (rate(container_cpu_usage_seconds_total{
  namespace="$namespace",
  pod=~"$component.*"
}[$__rate_interval]))

Detall que causa molts maldecaps: amb variables multivalor, Grafana substitueix $component per (api-reserves|botiga-web). Això només funciona amb l'operador =~, mai amb =. Si fas servir =, la consulta no retorna res i no hi ha cap missatge d'error que ho expliqui.

Per forçar el format explícitament: ${component:regex} o ${component:pipe}.

El resultat

Un únic quadre de comandament, versionat una sola vegada, que serveix per als tres entorns. Canvies el desplegable de pro a pre i veus els mateixos panells amb les dades de l'altre entorn. I si afegim un component nou amb les etiquetes de la convenció de Rutas Norte, apareix sol al desplegable.

  1. Importar quadres de comandament de la comunitat

Grafana té un catàleg públic amb milers de quadres de comandament. S'importen pel seu identificador numèric: Dashboards → New → Import → enganxar l'ID.

Els que de debò interessen per a un clúster de Kubernetes:

ID Quadre de comandament Què mostra
315 Kubernetes cluster monitoring Visió general del clúster: nodes, pods, xarxa
1860 Node Exporter Full Tot el que exposa node-exporter, molt complet
6417 Kubernetes Cluster (Prometheus) Recursos per namespace i per workload
9628 PostgreSQL Database Per a l'exportador de postgres-reserves
11835 Redis Dashboard Per a redis-cache
7645 NGINX Ingress Controller Trànsit de l'Ingress de 04-04

A més, el kube-prometheus-stack ja porta instal·lats una vintena de quadres de comandament molt bons (ús per namespace, per pod, per node, estat del pla de control). Abans d'importar res, mira el que ja tens.

Per què convé revisar-los abans de fiar-se'n

Un quadre de comandament de la comunitat és codi escrit per un desconegut per al seu clúster, no per al teu. Comprovacions obligatòries:

  1. Existeixen les mètriques al teu Prometheus? Molts quadres fan servir noms de versions antigues de kube-state-metrics o de cAdvisor. Un panell buit no sempre significa "tot bé": pot significar "aquesta mètrica no existeix aquí".
  2. Coincideix el nom de la font de dades? En importar, Grafana demana mapar la font de dades. Si el quadre espera una anomenada Prometheus i la teva es diu d'una altra manera, tots els panells fallen.
  3. Són raonables les consultes per a la teva escala? Un panell amb rate(...[1m]) sobre 50 000 sèries pot trigar 20 segons i castigar Prometheus cada vegada que algú obre la pàgina.
  4. Reflecteix la teva realitat? Un quadre genèric de Kubernetes no sap què és api-reserves ni què és una reserva confirmada. Serveix per a infraestructura; no substitueix el quadre de comandament propi.
  5. S'ha actualitzat recentment? Un quadre de 2019 farà servir mètriques que ja no existeixen.

Estratègia recomanada per a Rutas Norte:

  • Quadres de la comunitat per a infraestructura genèrica (nodes, PostgreSQL, Redis, Ingress). No hi aportem res reinventant-los.
  • Quadre propi per a la plataforma: els quatre indicadors daurats dels nostres components i les mètriques de negoci. Ningú de la comunitat pot escriure això per nosaltres.

  1. El quadre de comandament de Rutas Norte, panell a panell

Dissenyem el quadre de comandament principal. Estructura: una fila de resum a dalt i una fila per component a sota, totes plegables.

flowchart TB
    subgraph DASH["Quadre de comandament: Plataforma Rutas Norte — [entorn] [namespace]"]
        subgraph F0["Fila 0: Resum executiu"]
            A["Reserves/min<br/>Stat"]
            B["Taxa d'error<br/>Stat"]
            C["Latència p95<br/>Stat"]
            D["Pods no llestos<br/>Stat"]
        end
        subgraph F1["Fila 1: api-reserves"]
            E["Trànsit"] --- F["Errors"] --- G["Latència p50/p95/p99"] --- H["Saturació"]
        end
        subgraph F2["Fila 2: postgres-reserves"]
            I["Connexions"] --- J["Transaccions/s"] --- K["Mida al disc"] --- L["Interbloqueigs"]
        end
        subgraph F3["Fila 3: resta de components"]
            M["botiga-web"] --- N["redis-cache"] --- O["worker-notificacions"] --- P["informes-ocupacio"]
        end
    end

Fila 0 — Resum executiu

Quatre panells Stat que responen d'un cop d'ull a "va bé la plataforma?".

Panell 1: Reserves confirmades per minut. La mètrica del negoci.

sum(rate(rutasnorte_reserves_confirmades_total{entorn="$entorn"}[$__rate_interval])) * 60
  • Tipus: Stat, amb gràfic de tendència de fons (Graph mode: Area).
  • Unitat: short, sufix res/min.
  • Llindars: vermell per sota d'1, groc fins a 5, verd per sobre.
  • Per què és el primer panell del quadre: si això cau a zero, tant se val que els pods estiguin Running. La plataforma existeix per vendre bitllets.

Panell 2: Taxa d'error.

sum(rate(rutasnorte_peticions_total{entorn="$entorn", codi=~"5.."}[$__rate_interval]))
  /
sum(rate(rutasnorte_peticions_total{entorn="$entorn"}[$__rate_interval]))
  • Unitat: Percent (0.0-1.0).
  • Llindars: verd < 0,5 %, groc < 2 %, vermell ≥ 2 %.

Panell 3: Latència p95 global. Fem servir la regla de gravació que vam crear a 07-03:

max(apireserves:latencia_p95:5m{entorn="$entorn"})
  • Unitat: seconds (s). Llindars: verd < 0,3 s, groc < 1 s, vermell ≥ 1 s.

Panell 4: Pods no llestos. Directament de kube-state-metrics:

sum(kube_deployment_status_replicas_unavailable{namespace="$namespace"})
  +
sum(kube_statefulset_status_replicas_current{namespace="$namespace"}
    - kube_statefulset_status_replicas_ready{namespace="$namespace"})
  • Llindars: verd 0, vermell ≥ 1. Qualsevol valor diferent de zero és una anomalia.

Fila 1 — api-reserves: els quatre indicadors daurats

Trànsit (Time series):

sum by (ruta) (rate(rutasnorte_peticions_total{entorn="$entorn"}[$__rate_interval]))

Llegenda: {{ruta}}. Unitat: reqps.

Errors (Time series, amb dues consultes superposades):

# A: taxa d'error de la nostra plataforma
sum(rate(rutasnorte_peticions_total{entorn="$entorn", codi=~"5.."}[$__rate_interval]))
  / sum(rate(rutasnorte_peticions_total{entorn="$entorn"}[$__rate_interval]))

# B: taxa d'error de la passarel·la externa
sum(rate(rutasnorte_passarela_pagaments_crides_total{entorn="$entorn", resultat=~"error|timeout"}[$__rate_interval]))
  / sum(rate(rutasnorte_passarela_pagaments_crides_total{entorn="$entorn"}[$__rate_interval]))

Superposar totes dues és una decisió de disseny deliberada: permet veure d'un cop si els nostres errors coincideixen amb els del proveïdor extern. Aquesta correlació visual estalvia vint minuts d'investigació en un incident.

Latència (Time series, tres percentils):

histogram_quantile(0.50, sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn="$entorn"}[$__rate_interval])))
histogram_quantile(0.95, sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn="$entorn"}[$__rate_interval])))
histogram_quantile(0.99, sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn="$entorn"}[$__rate_interval])))

Llegendes: p50, p95, p99. Veure els tres junts és el que distingeix "tot va lent" (els tres pugen) de "hi ha una cua de peticions patològiques" (només puja el p99).

Saturació (Time series, dos eixos):

# CPU consumida davant del límit
sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="$namespace", pod=~"api-reserves-.*", container="api"}[$__rate_interval]))
  / sum by (pod) (kube_pod_container_resource_limits{namespace="$namespace", pod=~"api-reserves-.*", resource="cpu"})

# Fracció de períodes estrangulats: la confirmació del throttling
sum by (pod) (rate(container_cpu_cfs_throttled_periods_total{namespace="$namespace", pod=~"api-reserves-.*"}[$__rate_interval]))
  / sum by (pod) (rate(container_cpu_cfs_periods_total{namespace="$namespace", pod=~"api-reserves-.*"}[$__rate_interval]))

# El pool de connexions: el fenomen de 07-01, ara visible
max(rutasnorte_connexions_pool_actives{entorn="$entorn"}) / 20

Fila 2 — postgres-reserves

Tot del sidecar exportador de 06-04, ja connectat a 07-03:

# Connexions respecte al màxim
pg_stat_database_numbackends{datname="reserves"} / pg_settings_max_connections

# Transaccions per segon (confirmades i revertides)
rate(pg_stat_database_xact_commit{datname="reserves"}[$__rate_interval])
rate(pg_stat_database_xact_rollback{datname="reserves"}[$__rate_interval])

# Mida de la base de dades, amb projecció
pg_database_size_bytes{datname="reserves"}

# Espai lliure al PVC (ve del kubelet, no de l'exportador)
kubelet_volume_stats_available_bytes{persistentvolumeclaim="dades-postgres-reserves-0"}
  / kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="dades-postgres-reserves-0"}

# Interbloqueigs: zero és el normal, qualsevol cosa diferent mereix mirar-se
increase(pg_stat_database_deadlocks{datname="reserves"}[$__interval])

Fila 3 — Resta de components

worker-notificacions — el panell més important és la profunditat de la cua:

max(rutasnorte_cua_pendents{entorn="$entorn"})
max(rutasnorte_cua_espera_segons{entorn="$entorn"})
sum(rate(rutasnorte_correus_enviats_total{entorn="$entorn", resultat="ok"}[$__rate_interval])) * 60

informes-ocupacio — el CronJob de 06-03. No té indicadors daurats clàssics:

# Segons des de l'última execució amb èxit
time() - kube_job_status_completion_time{job_name=~"informes-ocupacio.*"}

# Durada de l'última execució
kube_job_status_completion_time{job_name=~"informes-ocupacio.*"}
  - kube_job_status_start_time{job_name=~"informes-ocupacio.*"}

Panell d'estat general (Table), molt útil com a resum:

kube_pod_container_status_restarts_total{namespace="$namespace"}

Amb transformacions per mostrar pod, contenidor, reinicis i edat en columnes ordenables.

Un consell de disseny

Un quadre de comandament ha de respondre a una pregunta, no mostrar-ho tot. El nostre respon a "està la plataforma servint bé els clients i, si no, on és el problema?". Quadres amb quaranta panells no els mira ningú; vuit panells ben triats es miren cada dia.

  1. Proveir quadres de comandament com a codi

Si construeixes el quadre de comandament clicant a la interfície, viu a la base de dades SQLite interna de Grafana. I aquella base de dades, en un pod sense volum persistent, desapareix quan el pod es recrea. És un desastre que li passa a tothom una vegada.

A més, els quadres de comandament són configuració: han d'estar a Git, revisar-se en una petició de canvi i desplegar-se igual que la resta de manifests.

El mecanisme del sidecar

El chart desplega al costat de Grafana un contenidor sidecar que vigila tots els ConfigMaps del clúster buscant una etiqueta concreta. Quan en troba un, escriu el seu contingut al directori de quadres de comandament de Grafana, que el carrega automàticament.

# Fragment dels valors del chart (07-03) que activa el mecanisme
grafana:
  sidecar:
    dashboards:
      enabled: true
      label: grafana_dashboard      # <-- l'etiqueta que busca
      labelValue: "1"
      searchNamespace: ALL          # busca a tots els namespaces
      folderAnnotation: grafana_folder
      provider:
        foldersFromFilesStructure: true

El ConfigMap del quadre de comandament

# k8s/base/monitoratge/dashboard-rutas-norte.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: dashboard-rutas-norte
  namespace: monitoratge
  labels:
    grafana_dashboard: "1"          # el sidecar el detecta per aquesta etiqueta
    app.kubernetes.io/part-of: rutas-norte
  annotations:
    grafana_folder: "Rutas Norte"   # carpeta on apareixerà
data:
  rutas-norte-general.json: |
    {
      "title": "Plataforma Rutas Norte — General",
      "uid": "rutasnorte-general",
      "tags": ["rutas-norte", "produccio"],
      "timezone": "Europe/Madrid",
      "refresh": "30s",
      "time": { "from": "now-6h", "to": "now" },
      "templating": {
        "list": [
          {
            "name": "entorn",
            "type": "query",
            "datasource": { "type": "prometheus", "uid": "prometheus" },
            "query": "label_values(rutasnorte_peticions_total, entorn)",
            "current": { "text": "pro", "value": "pro" },
            "sort": 1
          },
          {
            "name": "namespace",
            "type": "query",
            "datasource": { "type": "prometheus", "uid": "prometheus" },
            "query": "label_values(kube_pod_info{namespace=~\"rutas-norte-$entorn\"}, namespace)",
            "sort": 1
          }
        ]
      },
      "panels": [
        {
          "id": 1,
          "title": "Reserves confirmades per minut",
          "type": "stat",
          "gridPos": { "h": 5, "w": 6, "x": 0, "y": 0 },
          "datasource": { "type": "prometheus", "uid": "prometheus" },
          "targets": [
            {
              "expr": "sum(rate(rutasnorte_reserves_confirmades_total{entorn=\"$entorn\"}[$__rate_interval])) * 60",
              "legendFormat": "reserves/min"
            }
          ],
          "fieldConfig": {
            "defaults": {
              "unit": "short",
              "decimals": 1,
              "thresholds": {
                "mode": "absolute",
                "steps": [
                  { "color": "red",   "value": null },
                  { "color": "yellow","value": 1 },
                  { "color": "green", "value": 5 }
                ]
              }
            }
          },
          "options": { "graphMode": "area", "colorMode": "background" }
        },
        {
          "id": 2,
          "title": "Taxa d'error 5xx",
          "type": "stat",
          "gridPos": { "h": 5, "w": 6, "x": 6, "y": 0 },
          "datasource": { "type": "prometheus", "uid": "prometheus" },
          "targets": [
            {
              "expr": "sum(rate(rutasnorte_peticions_total{entorn=\"$entorn\", codi=~\"5..\"}[$__rate_interval])) / sum(rate(rutasnorte_peticions_total{entorn=\"$entorn\"}[$__rate_interval]))",
              "legendFormat": "taxa error"
            }
          ],
          "fieldConfig": {
            "defaults": {
              "unit": "percentunit",
              "decimals": 2,
              "thresholds": {
                "mode": "absolute",
                "steps": [
                  { "color": "green",  "value": null },
                  { "color": "yellow", "value": 0.005 },
                  { "color": "red",    "value": 0.02 }
                ]
              }
            }
          }
        },
        {
          "id": 3,
          "title": "Latència d'api-reserves",
          "type": "timeseries",
          "gridPos": { "h": 9, "w": 12, "x": 0, "y": 5 },
          "datasource": { "type": "prometheus", "uid": "prometheus" },
          "targets": [
            {
              "expr": "histogram_quantile(0.50, sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn=\"$entorn\"}[$__rate_interval])))",
              "legendFormat": "p50"
            },
            {
              "expr": "histogram_quantile(0.95, sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn=\"$entorn\"}[$__rate_interval])))",
              "legendFormat": "p95"
            },
            {
              "expr": "histogram_quantile(0.99, sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn=\"$entorn\"}[$__rate_interval])))",
              "legendFormat": "p99"
            }
          ],
          "fieldConfig": {
            "defaults": { "unit": "s", "custom": { "fillOpacity": 10 } }
          }
        }
      ]
    }

El flux de treball recomanat

No escriguis aquell JSON a mà. El procediment pràctic:

  1. Construeix el quadre de comandament a la interfície de Grafana, que és còmoda.
  2. Dashboard settings → JSON Model → Copiar.
  3. Enganxa'l al ConfigMap, dins de data, amb la indentació correcta.
  4. kubectl apply i revisió en una petició de canvi.
  5. A Grafana, marca aquell quadre de comandament com de només lectura per evitar que algú l'editi a la interfície i perdi els canvis al desplegament següent.
kubectl apply -f k8s/base/monitoratge/dashboard-rutas-norte.yaml

# El sidecar el detecta en segons; verificar-ho al seu log
kubectl -n monitoratge logs deploy/monitoratge-grafana -c grafana-sc-dashboard --tail=10
{"time": "2026-08-06T11:42:03", "msg": "Working on configmap monitoratge/dashboard-rutas-norte"}
{"time": "2026-08-06T11:42:03", "msg": "Writing /tmp/dashboards/rutas-norte-general.json"}

Quan arribem a 10-04 (Kustomize) i 10-05 (GitOps), aquest ConfigMap serà un artefacte més del repositori, desplegat automàticament en fer merge.

  1. Regles d'alerta amb PrometheusRule i la importància de for

Grafana té el seu propi motor d'alertes, però en un entorn amb el Prometheus Operator el natural és definir les alertes a Prometheus mitjançant el recurs PrometheusRule. Avantatges: viuen a Git al costat de l'aplicació, les avalua Prometheus (que és on són les dades) i les encamina Alertmanager.

Anatomia d'una regla

- alert: ApiReservesTaxaErrorAlta
  expr: |
    sum(rate(rutasnorte_peticions_total{entorn="pro", codi=~"5.."}[5m]))
      /
    sum(rate(rutasnorte_peticions_total{entorn="pro"}[5m]))
    > 0.05
  for: 5m
  labels:
    severity: critica
    component: api-reserves
    equip: plataforma
  annotations:
    summary: "api-reserves retorna més d'un 5% d'errors"
    description: >
      La taxa d'errors 5xx d'api-reserves en producció és del
      {{ $value | humanizePercentage }} durant els últims 5 minuts.
      Els clients no poden completar reserves.
    runbook_url: "https://runbooks.rutasnorte.example/api-reserves-errors-5xx"
    dashboard_url: "https://metriques.rutasnorte.example/d/rutasnorte-general"
Camp Funció
alert Nom de l'alerta. En PascalCase, descriptiu, sense espais
expr Expressió PromQL. L'alerta està "activa" per a cada sèrie que retorni
for Quant de temps s'ha de complir de manera continuada abans de disparar
labels Metadades per a l'encaminament a Alertmanager. Aquí va la severitat
annotations Text per a l'humà que la rep. No afecten l'encaminament

La importància de for

Aquest és el camp que separa un sistema d'alertes usable d'un d'insuportable.

Sense for, l'alerta dispara tan bon punt l'expressió es compleix una sola vegada. Un pic de 30 segons per un desplegament, un reinici puntual o un escaneig perdut genera un avís. Multiplicat per vint alertes i tres entorns, és un canal de Slack que ningú llegeix.

Amb for: 5m, Prometheus observa l'expressió a cada cicle d'avaluació (per defecte cada 30 s) i només dispara si s'ha complert en totes les avaluacions d'aquests 5 minuts. Un sol cicle en què la condició no es compleixi reinicia el comptador.

Estats d'una alerta:

stateDiagram-v2
    [*] --> Inactive: l'expressió no es compleix
    Inactive --> Pending: l'expressió es compleix
    Pending --> Inactive: deixa de complir-se abans d'esgotar for
    Pending --> Firing: es compleix durant tot el període for
    Firing --> Inactive: deixa de complir-se (s'envia la resolució)

Només en Firing s'envia res a Alertmanager. Pending és visible a la interfície de Prometheus (Alerts), cosa que és útil per depurar.

Criteri per triar for:

Tipus d'alerta for recomanat Raó
Caiguda total (up == 0) 2m Ràpida, però tolera un reinici o un desplegament
Taxa d'error 5m Un pic curt no ha de despertar ningú
Latència alta 10m Molt sorollosa si és més curta
Disc omplint-se 15m És una tendència, no un esdeveniment
Certificat caducant 1h No hi ha cap pressa
Pod en CrashLoopBackOff 10m Tolera una arrencada lenta legítima

Anotacions i plantilles

Les anotacions admeten plantilles Go amb accés al valor i a les etiquetes:

Expressió Resultat
{{ $value }} 0.0734829
{{ $value | humanizePercentage }} 7.35%
{{ $value | humanize }} 73.5m
{{ $value | humanizeDuration }} 1h 12m 30s
{{ $labels.pod }} api-reserves-7d9f8c4b5-x2klm
{{ $labels.namespace }} rutas-norte-pro

El runbook_url no és opcional. Una alerta que arriba a les tres de la matinada a algú que no va escriure el codi i conté només "ApiReservesTaxaErrorAlta" és inútil. Amb un enllaç a un procediment escrit —què comprovar, en quin ordre, què fer, a qui escalar— l'alerta és accionable. Els runbooks complets són matèria d'11-06, però l'enllaç es posa des del primer dia.

  1. El catàleg d'alertes de Rutas Norte

# k8s/base/monitoratge/alertes-rutas-norte.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: alertes-rutas-norte
  namespace: monitoratge
  labels:
    app.kubernetes.io/part-of: rutas-norte
    prometheus: monitoratge
spec:
  groups:
    # =====================================================================
    # GRUP 1: SÍMPTOMES. El que el client percep. Màxima prioritat.
    # =====================================================================
    - name: rutasnorte.simptomes
      interval: 30s
      rules:

        - alert: PlataformaSenseVendes
          expr: |
            sum(rate(rutasnorte_reserves_confirmades_total{entorn="pro"}[10m])) == 0
            and
            sum(rate(rutasnorte_peticions_total{entorn="pro"}[10m])) > 0.5
          for: 10m
          labels:
            severity: critica
            equip: plataforma
          annotations:
            summary: "Rutas Norte no ha confirmat CAP reserva en 10 minuts"
            description: >
              Hi ha trànsit entrant ({{ $value | humanize }} peticions/s) però
              zero reserves confirmades. La plataforma no està venent.
              Aquesta és l'alerta més important del sistema.
            runbook_url: "https://runbooks.rutasnorte.example/sense-vendes"

        # Justificació: la condició "hi ha trànsit" evita que dispari de
        # matinada, quan és normal no vendre res durant deu minuts.

        - alert: ApiReservesTaxaErrorAlta
          expr: |
            sum(rate(rutasnorte_peticions_total{entorn="pro", codi=~"5.."}[5m]))
              / sum(rate(rutasnorte_peticions_total{entorn="pro"}[5m])) > 0.05
          for: 5m
          labels:
            severity: critica
            component: api-reserves
            equip: plataforma
          annotations:
            summary: "api-reserves retorna més d'un 5% d'errors"
            description: >
              Taxa d'error del {{ $value | humanizePercentage }} durant
              5 minuts. Els clients no poden completar les seves compres.
            runbook_url: "https://runbooks.rutasnorte.example/api-errors-5xx"

        - alert: ApiReservesLatenciaAlta
          expr: apireserves:latencia_p95:5m{entorn="pro"} > 1
          for: 10m
          labels:
            severity: avis
            component: api-reserves
            equip: plataforma
          annotations:
            summary: "El p95 de latència d'api-reserves supera 1 segon"
            description: >
              El percentil 95 és de {{ $value | humanizeDuration }} a la
              ruta {{ $labels.ruta }}. L'SLO de Rutas Norte són 500 ms.
            runbook_url: "https://runbooks.rutasnorte.example/api-latencia"

        - alert: BotigaWebCaiguda
          expr: |
            sum(kube_deployment_status_replicas_available{
              namespace="rutas-norte-pro", deployment="botiga-web"}) == 0
          for: 2m
          labels:
            severity: critica
            component: botiga-web
            equip: plataforma
          annotations:
            summary: "No queda cap rèplica de botiga-web disponible"
            description: "www.rutasnorte.example està caiguda per a tots els clients."
            runbook_url: "https://runbooks.rutasnorte.example/botiga-web-caiguda"

    # =====================================================================
    # GRUP 2: CAUSES I CAPACITAT. Avisen abans que hi hagi símptoma.
    # =====================================================================
    - name: rutasnorte.capacitat
      interval: 60s
      rules:

        - alert: DiscPostgresSOmplira
          expr: |
            predict_linear(
              kubelet_volume_stats_available_bytes{
                persistentvolumeclaim="dades-postgres-reserves-0"}[6h],
              4 * 3600
            ) < 0
          for: 30m
          labels:
            severity: critica
            component: postgres-reserves
            equip: plataforma
          annotations:
            summary: "El disc de postgres-reserves s'omplirà en menys de 4 hores"
            description: >
              Segons la tendència de les últimes 6 hores, el volum
              dades-postgres-reserves-0 es quedarà sense espai en menys de
              4 hores. Queden {{ $value | humanize1024 }}B lliures.
              Ampliar el PVC (procediment de 05-05) ABANS que passi:
              una base de dades amb el disc ple deixa d'acceptar escriptures.
            runbook_url: "https://runbooks.rutasnorte.example/ampliar-pvc-postgres"

        # predict_linear ajusta una recta de regressió sobre la finestra [6h]
        # i extrapola 4*3600 segons cap endavant. Si el resultat és
        # negatiu, significa que la recta creua el zero abans de 4 hores.
        # És la diferència entre avisar d'un problema i avisar del futur.

        - alert: PostgresConnexionsExhaurintse
          expr: |
            pg_stat_database_numbackends{datname="reserves"}
              / pg_settings_max_connections > 0.85
          for: 10m
          labels:
            severity: avis
            component: postgres-reserves
            equip: plataforma
          annotations:
            summary: "postgres-reserves al {{ $value | humanizePercentage }} de connexions"
            description: >
              Quan s'esgotin, api-reserves fallarà la readiness i sortirà
              dels Endpoints. Revisar consultes lentes i el pool de l'API.
            runbook_url: "https://runbooks.rutasnorte.example/postgres-connexions"

        - alert: CertificatCaducaAviat
          expr: |
            (certmanager_certificate_expiration_timestamp_seconds - time()) / 86400 < 15
          for: 1h
          labels:
            severity: avis
            equip: plataforma
          annotations:
            summary: "El certificat {{ $labels.name }} caduca en {{ $value | humanize }} dies"
            description: >
              cert-manager hauria de renovar-lo automàticament (04-05). Que no
              ho hagi fet indica un problema amb l'emissor o amb el repte ACME.
            runbook_url: "https://runbooks.rutasnorte.example/certificats"

        - alert: ContenidorAmbThrottlingSever
          expr: |
            sum by (namespace, pod, container) (
              rate(container_cpu_cfs_throttled_periods_total{namespace=~"rutas-norte-.*"}[5m]))
            / sum by (namespace, pod, container) (
              rate(container_cpu_cfs_periods_total{namespace=~"rutas-norte-.*"}[5m]))
            > 0.30
          for: 15m
          labels:
            severity: avis
            equip: plataforma
          annotations:
            summary: "{{ $labels.container }} estrangulat el {{ $value | humanizePercentage }} del temps"
            description: >
              El contenidor està arribant al seu limits.cpu de manera sostinguda.
              Revisar la recalibració de recursos de 07-02.
            runbook_url: "https://runbooks.rutasnorte.example/throttling-cpu"

    # =====================================================================
    # GRUP 3: SALUT DELS OBJECTES. Font: kube-state-metrics.
    # =====================================================================
    - name: rutasnorte.workloads
      interval: 60s
      rules:

        - alert: PodEnCrashLoop
          expr: |
            increase(kube_pod_container_status_restarts_total{
              namespace=~"rutas-norte-.*"}[15m]) > 3
          for: 10m
          labels:
            severity: avis
            equip: plataforma
          annotations:
            summary: "{{ $labels.pod }} s'ha reiniciat més de 3 vegades en 15 min"
            description: >
              Contenidor {{ $labels.container }} a {{ $labels.namespace }}.
              Causes freqüents: OOMKilled, error d'arrencada, o una
              livenessProbe massa agressiva (07-01).
              Recollir evidències amb 'kubectl logs --previous' ABANS de tocar res.
            runbook_url: "https://runbooks.rutasnorte.example/crashloop"

        - alert: CronJobInformesNoExecutat
          expr: |
            time() - max(kube_job_status_completion_time{
              job_name=~"informes-ocupacio.*"}) > 100000
          for: 30m
          labels:
            severity: avis
            component: informes-ocupacio
            equip: dades
          annotations:
            summary: "El CronJob informes-ocupacio no s'ha executat amb èxit"
            description: >
              Han passat {{ $value | humanizeDuration }} des de l'última
              execució correcta. El CronJob nocturn s'hauria d'executar cada
              24 hores. Sense informes, el departament comercial es queda cec.
            runbook_url: "https://runbooks.rutasnorte.example/cronjob-informes"

        # 100000 segons ≈ 27,8 h: un marge sobre les 24 h del cron que
        # tolera un retard puntual sense generar falsos positius.

        - alert: JobFallit
          expr: kube_job_status_failed{namespace=~"rutas-norte-.*"} > 0
          for: 5m
          labels:
            severity: avis
            equip: plataforma
          annotations:
            summary: "El Job {{ $labels.job_name }} ha fallat"
            runbook_url: "https://runbooks.rutasnorte.example/job-fallit"

        - alert: CuaNotificacionsCreixent
          expr: |
            max(rutasnorte_cua_espera_segons{entorn="pro"}) > 900
          for: 10m
          labels:
            severity: avis
            component: worker-notificacions
            equip: plataforma
          annotations:
            summary: "Correus de confirmació amb més de 15 minuts de retard"
            description: >
              El missatge més antic porta {{ $value | humanizeDuration }}
              en cua. Hi ha clients que han pagat i no han rebut res.
            runbook_url: "https://runbooks.rutasnorte.example/cua-notificacions"

    # =====================================================================
    # GRUP 4: META. Alertes sobre el propi sistema de monitoratge.
    # =====================================================================
    - name: rutasnorte.meta
      interval: 60s
      rules:

        - alert: ObjectiuPrometheusCaigut
          expr: up{namespace=~"rutas-norte-.*|monitoratge"} == 0
          for: 5m
          labels:
            severity: avis
            equip: plataforma
          annotations:
            summary: "Prometheus no pot escanejar {{ $labels.job }}"
            description: >
              Objectiu {{ $labels.instance }} inabastable. Revisar
              NetworkPolicies, l'endpoint /metrics i la readiness del pod.
            runbook_url: "https://runbooks.rutasnorte.example/objectiu-caigut"

        - alert: HPASenseMetriques
          expr: |
            kube_horizontalpodautoscaler_status_condition{
              condition="ScalingActive", status="false"} == 1
          for: 5m
          labels:
            severity: critica
            equip: plataforma
          annotations:
            summary: "L'HPA {{ $labels.horizontalpodautoscaler }} no pot escalar"
            description: >
              L'autoescalat està inactiu, probablement perquè
              metrics-server no respon (07-02). Durant un pic de trànsit
              la plataforma NO escalarà i ningú se n'adonarà.
            runbook_url: "https://runbooks.rutasnorte.example/hpa-inactiu"

Aquesta última alerta és la mesura preventiva que vam prometre a 07-02: l'error silenciós de l'HPA ja no pot passar desapercebut.

Aplicar i verificar:

kubectl apply -f k8s/base/monitoratge/alertes-rutas-norte.yaml

# L'operador ha carregat les regles?
kubectl -n monitoratge get prometheusrule alertes-rutas-norte

# Prometheus les està avaluant?
curl -s localhost:9090/api/v1/rules | jq -r '
  .data.groups[] | select(.name | startswith("rutasnorte")) |
  .rules[] | "\(.name)\t\(.state // "recording")"'
PlataformaSenseVendes	inactive
ApiReservesTaxaErrorAlta	inactive
ApiReservesLatenciaAlta	pending
BotigaWebCaiguda	inactive
DiscPostgresSOmplira	inactive

Provar una alerta abans de confiar-hi és imprescindible. Una manera segura a rutas-norte-dev:

# Provocar deliberadament que un objectiu caigui
kubectl -n rutas-norte-dev scale deployment api-reserves --replicas=0
# Esperar el for i comprovar que l'alerta passa a firing

  1. Alertmanager: rutes, receptors, inhibició i silencis

Prometheus decideix què va malament. Alertmanager decideix a qui li ho explica, quan i com.

El problema que resol

Imagina que rutas-norte-worker-2 cau a les 03:14. Prometheus dispara, en qüestió de segons:

  • 6 alertes PodEnCrashLoop (els pods que hi vivien).
  • 4 alertes ObjectiuPrometheusCaigut.
  • 1 BotigaWebCaiguda.
  • 1 ApiReservesTaxaErrorAlta.
  • 1 NodeNotReady (de les regles que porta l'estoc).

Sense Alertmanager, la persona de guàrdia rep tretze notificacions en dos minuts i ha de reconstruir mentalment que totes són el mateix problema. Amb Alertmanager ben configurat en rep una, agrupada, amb el node caigut destacat i les derivades silenciades.

L'arbre de rutes

# k8s/base/monitoratge/alertmanager-config.yaml
apiVersion: v1
kind: Secret
metadata:
  name: alertmanager-monitoratge-kube-pr-alertmanager
  namespace: monitoratge
stringData:
  alertmanager.yaml: |
    global:
      resolve_timeout: 5m
      smtp_smarthost: 'smtp.rutasnorte.example:587'
      smtp_from: '[email protected]'

    # -----------------------------------------------------------------
    # ARBRE DE RUTES: s'avalua de dalt a baix; la primera coincidència
    # guanya, tret que es marqui continue: true.
    # -----------------------------------------------------------------
    route:
      receiver: 'equip-plataforma-slack'     # receptor per defecte

      # Quines alertes s'agrupen en una mateixa notificació.
      # Agrupar per alertname + namespace significa: "totes les
      # PodEnCrashLoop de rutas-norte-pro arriben en un únic missatge".
      group_by: ['alertname', 'namespace', 'component']

      # Després de la PRIMERA alerta d'un grup nou, esperar 30 s per si
      # n'arriben més i enviar-les juntes. És el que converteix 13 missatges
      # en 1 quan cau un node.
      group_wait: 30s

      # Si arriben alertes NOVES a un grup ja notificat, esperar 5 min
      # abans d'enviar l'actualització.
      group_interval: 5m

      # Si l'alerta continua activa, repetir l'avís cada 4 hores.
      # Ni tan curt que saturi ni tan llarg que s'oblidi.
      repeat_interval: 4h

      routes:
        # 1. Alertes de prova i de desenvolupament: a un canal a part, sense
        #    despertar ningú. Evita que el soroll de dev arribi a la guàrdia.
        - matchers:
            - namespace =~ "rutas-norte-(dev|pre)"
          receiver: 'canal-desenvolupament'
          group_wait: 5m
          repeat_interval: 24h

        # 2. Alertes crítiques de producció: PagerDuty (desperta algú)
        #    I A MÉS Slack, gràcies a continue: true.
        - matchers:
            - severity = "critica"
            - namespace =~ "rutas-norte-pro|monitoratge"
          receiver: 'guardia-pagerduty'
          group_wait: 10s          # les crítiques, amb menys espera
          repeat_interval: 1h      # i es recorden més sovint
          continue: true

        - matchers:
            - severity = "critica"
          receiver: 'equip-plataforma-slack'

        # 3. Alertes de l'equip de dades: al seu propi canal.
        - matchers:
            - equip = "dades"
          receiver: 'equip-dades-slack'
          repeat_interval: 12h

    # -----------------------------------------------------------------
    # INHIBICIÓ: una alerta greu silencia les derivades.
    # -----------------------------------------------------------------
    inhibit_rules:
      # Si la botiga web està caiguda del tot, no cal avisar
      # també que la seva latència és alta.
      - source_matchers:
          - alertname = "BotigaWebCaiguda"
        target_matchers:
          - severity = "avis"
          - component = "botiga-web"
        equal: ['namespace']

      # Si el node està caigut, no avisar de cada pod que hi ha.
      - source_matchers:
          - alertname = "NodeNotReady"
        target_matchers:
          - alertname =~ "PodEnCrashLoop|ObjectiuPrometheusCaigut"
        equal: ['node']

      # Regla general: si hi ha una crítica del mateix component,
      # els avisos d'aquell component es callen.
      - source_matchers:
          - severity = "critica"
        target_matchers:
          - severity = "avis"
        equal: ['component', 'namespace']

    # -----------------------------------------------------------------
    # RECEPTORS
    # -----------------------------------------------------------------
    receivers:
      - name: 'equip-plataforma-slack'
        slack_configs:
          - api_url_file: /etc/alertmanager/secrets/slack/url
            channel: '#alertes-plataforma'
            send_resolved: true
            title: '{{ if eq .Status "firing" }}🔴{{ else }}✅{{ end }} {{ .CommonLabels.alertname }}'
            text: |
              {{ range .Alerts }}
              *{{ .Annotations.summary }}*
              {{ .Annotations.description }}
              Entorn: `{{ .Labels.namespace }}` · Severitat: `{{ .Labels.severity }}`
              <{{ .Annotations.runbook_url }}|📖 Runbook> · <{{ .Annotations.dashboard_url }}|📊 Quadre de comandament>
              {{ end }}

      - name: 'guardia-pagerduty'
        pagerduty_configs:
          - routing_key_file: /etc/alertmanager/secrets/pagerduty/key
            description: '{{ .CommonAnnotations.summary }}'
            severity: 'critical'
            details:
              runbook: '{{ .CommonAnnotations.runbook_url }}'
              namespace: '{{ .CommonLabels.namespace }}'

      - name: 'canal-desenvolupament'
        slack_configs:
          - api_url_file: /etc/alertmanager/secrets/slack/url
            channel: '#alertes-desenvolupament'
            send_resolved: false

      - name: 'equip-dades-slack'
        slack_configs:
          - api_url_file: /etc/alertmanager/secrets/slack/url
            channel: '#alertes-dades'
            send_resolved: true

      - name: 'correu-responsables'
        email_configs:
          - to: '[email protected]'
            send_resolved: true

Els quatre temps

Paràmetre Què controla Valor típic Si és massa curt Si és massa llarg
group_wait Espera abans del primer avís d'un grup 30 s (10 s crítica) Arriben missatges solts, no agrupats Es retarda la detecció
group_interval Espera abans d'avisar d'alertes noves del grup 5 m Spam en incidents que evolucionen T'assabentes tard que empitjora
repeat_interval Cada quant es recorda una alerta activa 4 h (1 h crítica) Fatiga i silenciament massiu S'oblida un problema obert
resolve_timeout Quant esperar sense dades abans de donar-la per resolta 5 m Falses resolucions Alertes fantasma

Inhibició

La inhibició és el que converteix tretze missatges en un. La sintaxi té tres parts:

  • source_matchers: l'alerta que silencia.
  • target_matchers: les alertes que se silencien.
  • equal: les etiquetes que han de coincidir entre totes dues. Sense això, una BotigaWebCaiguda a dev silenciaria els avisos de pro.

El camp equal és el que més s'oblida i el que fa que la inhibició sigui segura.

Silencis durant un manteniment

Un silenci és una supressió temporal creada per una persona, normalment abans d'una intervenció planificada: ampliar el PVC de PostgreSQL, migrar un node, fer un desplegament gran.

Des de la interfície d'Alertmanager (Silences → New Silence), o per API:

kubectl -n monitoratge port-forward svc/monitoratge-kube-pr-alertmanager 9093:9093 &

# Silenciar totes les alertes de postgres-reserves durant 2 hores
curl -s -X POST http://localhost:9093/api/v2/silences \
  -H 'Content-Type: application/json' \
  -d '{
    "matchers": [
      {"name": "component", "value": "postgres-reserves", "isRegex": false},
      {"name": "namespace", "value": "rutas-norte-pro", "isRegex": false}
    ],
    "startsAt": "2026-08-07T02:00:00Z",
    "endsAt":   "2026-08-07T04:00:00Z",
    "createdBy": "joan.costa",
    "comment": "Ampliació programada del PVC de postgres-reserves (tiquet OPS-1842)"
  }'

Bones pràctiques amb els silencis:

  • Sempre amb data de fi. Un silenci indefinit és una alerta esborrada de facto, i tothom se n'oblida.
  • Sempre amb comentari i tiquet. D'aquí a sis setmanes ningú recordarà per què existeix.
  • El més específic possible. Silenciar namespace=rutas-norte-pro sencer durant una intervenció a la base de dades et deixa cec davant d'un problema no relacionat.
  • Revisar els silencis actius periòdicament. Un silenci oblidat ha amagat més d'un incident greu.
# Llistar silencis actius
curl -s http://localhost:9093/api/v2/silences | \
  jq -r '.[] | select(.status.state=="active") |
  "\(.id)\t\(.comment)\tfins \(.endsAt)"'

  1. El criteri: alertar sobre símptomes, no sobre causes

Tot l'anterior és mecànica. Això és criteri, i és el que determina si el sistema serveix.

El problema de la fatiga d'alertes

Un equip que rep quaranta notificacions al dia deixa de llegir-les en dues setmanes. Quan arriba la que de debò importa, es perd entre el soroll. Un sistema amb massa alertes és pitjor que un sense cap, perquè genera una falsa sensació de cobertura.

Regla 1: alertar sobre símptomes, no sobre causes

Un símptoma és una cosa que el client percep. Una causa és una cosa del sistema que pot traduir-se o no en símptoma.

Causa (mala alerta) Símptoma (bona alerta) Per què
"El pod api-reserves-x2klm s'ha reiniciat" "La taxa d'error d'api-reserves supera el 5 %" Amb 6 rèpliques i readiness, un reinici no afecta ningú
"La CPU del node està al 85 %" "El p95 de latència supera 1 s" Un node al 85 % pot ser perfectament sa
"Hi ha 4 rèpliques en lloc de 6" "La botiga web està caiguda" 4 rèpliques poden bastar de matinada
"La memòria de redis-cache està al 70 %" "La taxa d'encerts de cau ha caigut" El 70 % pot ser l'estat normal

La prova definitiva: si aquesta alerta dispara a les tres de la matinada i ningú fa res, passa alguna cosa dolenta? Si la resposta és no, no hauria de despertar ningú.

Això no significa que les causes no es monitorin: es visualitzen al quadre de comandament i es consulten durant el diagnòstic. Simplement no desperten ningú.

Excepció legítima: les alertes de capacitat predictives. DiscPostgresSOmplira és una causa, no un símptoma. Es justifica perquè avisa amb hores d'antelació d'un símptoma catastròfic i irreversible que encara es pot evitar. Aquest és el criteri per fer excepcions: antelació suficient per actuar i conseqüència greu si no s'actua.

Regla 2: cada alerta ha de ser accionable

Abans de crear una alerta, respon per escrit a tres preguntes:

  1. Què ha de fer qui la rebi? Si la resposta és "mirar si s'arregla sol", no és una alerta: és un panell.
  2. Està escrit aquell procediment? Si no, escriu-lo abans d'activar l'alerta i posa'l a runbook_url.
  3. Pot actuar la persona de guàrdia, o cal escalar sempre? Si sempre cal escalar, encamina directament a l'equip que pot actuar.

Regla 3: revisar les alertes després de cada incident

Després de cada incident, dues preguntes:

  • Ens va avisar alguna alerta? Si no, en falta una. Crea-la.
  • Ens van avisar alertes que no van aportar res? Si sí, en sobra. Esborra-la o puja el seu llindar.

Sense aquesta revisió periòdica, el catàleg només creix i acaba sent soroll.

Nivells de severitat de Rutas Norte

Severitat Significat Canal Exemple
critica Afecta clients ara. Requereix acció immediata, de dia o de nit PagerDuty + Slack PlataformaSenseVendes
avis Afectarà si no s'actua. S'atén en horari laboral Slack PostgresConnexionsExhaurintse
info Context. No requereix acció Només al quadre de comandament Un desplegament ha acabat

Regla dura: si una alerta critica no justifica una trucada de telèfon a les quatre de la matinada, no és crítica.

  1. SLO i pressupost d'error de Rutas Norte

Els SLO (Service Level Objectives) formalitzen la pregunta "què significa que la plataforma funciona bé?" amb un número acordat amb el negoci.

Definir els SLO

Un SLO té tres parts: un indicador (SLI, què es mesura), un objectiu (quin valor ha d'assolir) i una finestra (en quant de temps s'avalua).

SLO de Rutas Norte, acordats amb la direcció:

Servei Indicador (SLI) Objectiu (SLO) Finestra
api-reserves disponibilitat % de peticions sense error 5xx 99,5 % 30 dies
api-reserves latència % de peticions sota 500 ms 99,0 % 30 dies
botiga-web disponibilitat % de peticions sense error 5xx 99,9 % 30 dies
Correus de confirmació % lliurats en menys de 5 min 99,0 % 30 dies

Un matís important: 99,5 % no és 100 %, i això és deliberat. Perseguir el 100 % és infinitament car i frena qualsevol canvi. L'SLO reconeix que un percentatge d'error és acceptable.

El pressupost d'error

El pressupost d'error és el complement de l'SLO: l'error que et pots permetre.

SLO de disponibilitat d'api-reserves: 99,5 % en 30 dies
Pressupost d'error = 100 % − 99,5 % = 0,5 %

En temps:  30 dies × 24 h × 0,5 % = 3 h 36 min d'indisponibilitat al mes
En peticions: amb ~4 milions de peticions/mes → 20 000 peticions poden fallar

Aquest pressupost és una eina de decisió, no una dada curiosa:

  • Si queda pressupost, l'equip pot desplegar, experimentar i assumir riscos. La velocitat de canvi no està limitada.
  • Si el pressupost està esgotat, es congelen els canvis que no siguin correccions de fiabilitat fins al període següent. Es deixa d'afegir funcionalitat i s'arregla el que falla.

És un mecanisme objectiu que substitueix la discussió eterna entre "cal treure la funcionalitat nova" i "cal estabilitzar la plataforma".

Mesurar el pressupost en PromQL

- name: rutasnorte.slo
  interval: 60s
  rules:
    # Ràtio d'èxit a la finestra de 30 dies
    - record: apireserves:slo_disponibilitat:30d
      expr: |
        1 - (
          sum(increase(rutasnorte_peticions_total{entorn="pro", codi=~"5.."}[30d]))
          /
          sum(increase(rutasnorte_peticions_total{entorn="pro"}[30d]))
        )

    # Pressupost d'error CONSUMIT, en tant per u.
    # 0 = intacte, 1 = esgotat, >1 = SLO incomplert.
    - record: apireserves:pressupost_error_consumit:30d
      expr: |
        (
          sum(increase(rutasnorte_peticions_total{entorn="pro", codi=~"5.."}[30d]))
          /
          sum(increase(rutasnorte_peticions_total{entorn="pro"}[30d]))
        ) / 0.005

    # SLO de latència: fracció de peticions sota 500 ms
    - record: apireserves:slo_latencia:30d
      expr: |
        sum(increase(rutasnorte_duracio_peticio_segons_bucket{entorn="pro", le="0.5"}[30d]))
        /
        sum(increase(rutasnorte_duracio_peticio_segons_count{entorn="pro"}[30d]))

Alertes per consum del pressupost

En lloc d'alertar sobre un llindar fix d'errors, s'alerta sobre la velocitat a la qual es consumeix el pressupost (burn rate). És més intel·ligent: tolera un pic curt però detecta ràpid una hemorràgia.

- alert: PressupostErrorConsumitRapid
  expr: |
    (
      sum(rate(rutasnorte_peticions_total{entorn="pro", codi=~"5.."}[1h]))
      / sum(rate(rutasnorte_peticions_total{entorn="pro"}[1h]))
    ) > (14.4 * 0.005)
  for: 5m
  labels:
    severity: critica
    equip: plataforma
  annotations:
    summary: "S'està consumint el pressupost d'error 14 vegades més ràpid del sostenible"
    description: >
      A aquest ritme, el pressupost de 30 dies s'esgotarà en uns 2 dies.
      Taxa d'error actual: {{ $value | humanizePercentage }}.
    runbook_url: "https://runbooks.rutasnorte.example/pressupost-error"

- alert: PressupostErrorGairebeExhaurit
  expr: apireserves:pressupost_error_consumit:30d > 0.90
  for: 30m
  labels:
    severity: avis
    equip: plataforma
  annotations:
    summary: "Queda menys del 10% del pressupost d'error del mes"
    description: >
      Consumit el {{ $value | humanizePercentage }} del pressupost.
      Considerar congelar els desplegaments no relacionats amb fiabilitat.

El factor 14,4 no és arbitrari: és el ritme al qual es consumiria el pressupost complet de 30 dies en uns 2 dies. Els factors habituals són 14,4 (1 h, crítica), 6 (6 h, crítica) i 1 (3 d, avís), combinant-se per detectar tant hemorràgies ràpides com dessagnats lents.

Un panell Stat amb apireserves:pressupost_error_consumit:30d, en tant per cent i amb llindars a 50/80/100, és probablement el panell més útil del quadre de comandament per parlar amb el negoci.

Errors Comuns i Consells

1. Quadres de comandament que no persisteixen. Construïts a la interfície, sense ConfigMap ni volum persistent, desapareixen en recrear-se el pod. Proveeix-los sempre com a codi.

2. Fer servir rate(metrica[5m]) en lloc de rate(metrica[$__rate_interval]). Amb finestra fixa, en fer zoom a 30 dies el gràfic s'omple de forats o de soroll.

3. Variable multivalor amb = en lloc de =~. Grafana la substitueix per (a|b|c), que només funciona amb l'operador d'expressió regular. El panell queda buit sense cap error visible.

4. Alertes sense for. L'error que més ràpid converteix un sistema d'alertes en soroll. Tot pic de 30 segons genera una notificació.

5. Alertes sense runbook_url. A les tres de la matinada, un nom d'alerta sense procediment associat obliga a improvisar. Escriu el runbook abans d'activar l'alerta.

6. Alertar sobre causes en lloc de símptomes. "El pod s'ha reiniciat" no és un problema si hi ha sis rèpliques i readiness. "Els clients no poden comprar" sí que ho és.

7. Oblidar equal a les regles d'inhibició. Sense ell, una alerta de dev pot silenciar els avisos de pro. Un error silenciós i perillós.

8. Silencis sense data de fi. És esborrar una alerta i oblidar-ho. Revisa periòdicament els silencis actius.

9. Llindars copiats d'un altre sistema. Un p95 d'1 s pot ser excel·lent per a un informe i catastròfic per a un autocompletat. Els llindars surten de les teves dades i del teu SLO.

10. Massa alertes crítiques. Si tot és crític, res no ho és. Reserva critica per al que justifica una trucada de matinada.

11. No provar les alertes. Una alerta amb una consulta mal escrita no dispara mai i dóna falsa tranquil·litat. Provoca la condició deliberadament a rutas-norte-dev i verifica que arriba la notificació al canal correcte.

12. Contrasenya de Grafana a Git. El valor adminPassword del fitxer de Helm és text pla versionat. Fes servir un Secret extern o autenticació delegada.

Exercicis

Exercici 1 — Corregir un catàleg d'alertes defectuós

Un company ha escrit aquestes alertes. Identifica els problemes de cadascuna i reescriu el PrometheusRule corregit.

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: alertes-equip
  namespace: monitoratge
spec:
  groups:
    - name: alertes
      rules:
        - alert: CPUAlta
          expr: |
            sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="rutas-norte-pro"}[5m])) > 0.5
          labels:
            severity: critica
          annotations:
            summary: "CPU alta"

        - alert: PodReiniciat
          expr: kube_pod_container_status_restarts_total{namespace="rutas-norte-pro"} > 0
          for: 1m
          labels:
            severity: critica
          annotations:
            summary: "Un pod s'ha reiniciat"

        - alert: MemoriaAlta
          expr: container_memory_working_set_bytes{namespace="rutas-norte-pro"} > 400000000
          for: 30s
          labels:
            severity: critica
          annotations:
            summary: "Memòria alta a {{ $labels.pod }}"

Exercici 2 — Dissenyar l'alerta predictiva de redis-cache

redis-cachemaxmemory configurat en 1 GiB amb política allkeys-lru. Quan s'omple, comença a expulsar claus i la taxa d'encerts s'esfondra, cosa que fa que api-reserves consulti PostgreSQL molt més i tot vagi lent.

L'exportador de Redis exposa:

redis_memory_used_bytes
redis_memory_max_bytes
redis_keyspace_hits_total
redis_keyspace_misses_total
redis_evicted_keys_total
  1. Escriu la consulta PromQL de la taxa d'encerts de cau (proporció d'encerts sobre el total).
  2. Escriu una alerta que avisi abans que el problema afecti els clients, fent servir predict_linear.
  3. Escriu una segona alerta sobre el símptoma, per a quan ja estigui passant, i explica per què calen totes dues.
  4. Quines regles d'inhibició hi afegiries?

Exercici 3 — Configurar l'encaminament per a un incident nocturn

Rutas Norte defineix aquesta política de guàrdia:

  • De 08:00 a 20:00, feiners: totes les alertes van al canal #alertes-plataforma de Slack.
  • Fora d'aquest horari: només les critica de rutas-norte-pro van a PagerDuty; la resta espera l'endemà al matí.
  • Les alertes de l'equip de dades (equip: dades) no van mai a PagerDuty.
  • El dissabte del pont de maig hi ha una migració programada de postgres-reserves de 02:00 a 05:00.
  1. Pot Alertmanager encaminar per franja horària? Si sí, escriu la configuració.
  2. Escriu l'arbre de rutes complet que implementa la política.
  3. Escriu l'ordre que crea el silenci per a la migració programada, amb les bones pràctiques de l'apartat 9.

Solucions

Solució 1

Problemes de CPUAlta:

  • Sense for: dispara tan bon punt un pod supera 0,5 nuclis un instant. Una arrencada de JVM o una compactació puntual generen notificació.
  • Alerta sobre una causa, no un símptoma. 0,5 nuclis pot ser perfectament normal per a postgres-reserves. Ningú sap què fer en rebre-la.
  • Llindar absolut sense context. 0,5 nuclis significa coses diferents segons el limits de cada component.
  • Severitat critica injustificada: no desperta ningú que pugui fer res útil.
  • Sense runbook_url ni descripció.

Problemes de PodReiniciat:

  • > 0 sobre un comptador acumulat: kube_pod_container_status_restarts_total no torna a baixar mai. Un pod que es va reiniciar fa tres setmanes manté l'alerta disparada per sempre. Cal fer servir increase(...[finestra]).
  • Un reinici aïllat no és un problema amb sis rèpliques i readiness (07-01).
  • for: 1m massa curt i severitat exagerada.

Problemes de MemoriaAlta:

  • for: 30s: sorollosíssim.
  • Llindar en bytes crus i absolut: 400 MB és molt per a botiga-web i poquíssim per a postgres-reserves. S'ha de comparar amb el limits del contenidor.
  • Sense filtrar container!="": inclou la sèrie agregada del pod i el contenidor pause, duplicant les alertes.
  • Alerta sobre causa. El que importa és si el contenidor serà OOMKilled, no un número de bytes.

Versió corregida:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: alertes-equip
  namespace: monitoratge
  labels:
    app.kubernetes.io/part-of: rutas-norte
spec:
  groups:
    - name: rutasnorte.recursos
      interval: 60s
      rules:

        # Substitueix CPUAlta: alerta sobre l'efecte real (throttling),
        # relatiu al límit del propi contenidor, no absolut.
        - alert: ContenidorEstrangulat
          expr: |
            sum by (namespace, pod, container) (
              rate(container_cpu_cfs_throttled_periods_total{namespace="rutas-norte-pro"}[5m]))
            / sum by (namespace, pod, container) (
              rate(container_cpu_cfs_periods_total{namespace="rutas-norte-pro"}[5m]))
            > 0.30
          for: 15m
          labels:
            severity: avis
            equip: plataforma
          annotations:
            summary: "{{ $labels.container }} estrangulat el {{ $value | humanizePercentage }} del temps"
            description: >
              El contenidor {{ $labels.container }} del pod {{ $labels.pod }}
              arriba al seu limits.cpu de manera sostinguda i això degrada la latència.
              Revisar la recalibració de recursos de 07-02.
            runbook_url: "https://runbooks.rutasnorte.example/throttling-cpu"

        # Substitueix PodReiniciat: fa servir increase() sobre una finestra,
        # exigeix diversos reinicis i baixa la severitat.
        - alert: PodEnCrashLoop
          expr: |
            increase(kube_pod_container_status_restarts_total{
              namespace="rutas-norte-pro"}[15m]) > 3
          for: 10m
          labels:
            severity: avis
            equip: plataforma
          annotations:
            summary: "{{ $labels.pod }} reiniciat més de 3 vegades en 15 minuts"
            description: >
              Contenidor {{ $labels.container }}. Causes freqüents: OOMKilled,
              error de configuració o livenessProbe massa agressiva (07-01).
              Recollir 'kubectl logs --previous' ABANS de tocar res.
            runbook_url: "https://runbooks.rutasnorte.example/crashloop"

        # Substitueix MemoriaAlta: relativa al límit, amb for raonable
        # i filtrant la sèrie agregada del pod.
        - alert: MemoriaAPropDelLimit
          expr: |
            container_memory_working_set_bytes{namespace="rutas-norte-pro", container!=""}
            / on (namespace, pod, container)
            kube_pod_container_resource_limits{namespace="rutas-norte-pro", resource="memory"}
            > 0.90
          for: 15m
          labels:
            severity: avis
            equip: plataforma
          annotations:
            summary: "{{ $labels.container }} al {{ $value | humanizePercentage }} del seu límit de memòria"
            description: >
              El contenidor és a prop del seu limits.memory i serà OOMKilled
              si el supera. Revisar si hi ha una fuita o si el límit és curt.
            runbook_url: "https://runbooks.rutasnorte.example/memoria-limit"

        # Alerta sobre el SÍMPTOMA real: el contenidor JA va ser mort.
        - alert: ContenidorOOMKilled
          expr: |
            increase(kube_pod_container_status_last_terminated_reason{
              namespace="rutas-norte-pro", reason="OOMKilled"}[15m]) > 0
          for: 1m
          labels:
            severity: critica
            equip: plataforma
          annotations:
            summary: "{{ $labels.container }} ha estat mort per falta de memòria"
            description: >
              El kernel ha mort el contenidor per superar el seu limits.memory
              (codi de sortida 137). Ampliar el límit o corregir la fuita.
            runbook_url: "https://runbooks.rutasnorte.example/oomkilled"

Canvis de fons, més enllà de la sintaxi: s'ha passat de tres alertes sobre causes absolutes a alertes relatives al límit del propi contenidor, amb finestres raonables, severitats proporcionades i runbooks. L'única critica és la que reflecteix un fet consumat i accionable.

Solució 2

1. Taxa d'encerts de cau.

sum(rate(redis_keyspace_hits_total{entorn="pro"}[5m]))
  /
(
  sum(rate(redis_keyspace_hits_total{entorn="pro"}[5m]))
  +
  sum(rate(redis_keyspace_misses_total{entorn="pro"}[5m]))
)

Es fan servir taxes i no valors absoluts perquè els comptadors acumulats des de l'arrencada del pod dilueixen qualsevol degradació recent.

2. Alerta predictiva (la causa, amb antelació).

- alert: RedisCacheSOmplira
  expr: |
    predict_linear(
      (redis_memory_max_bytes{entorn="pro"} - redis_memory_used_bytes{entorn="pro"})[2h:],
      2 * 3600
    ) < 0
  for: 20m
  labels:
    severity: avis
    component: redis-cache
    equip: plataforma
  annotations:
    summary: "redis-cache s'omplirà en menys de 2 hores"
    description: >
      Segons la tendència de les últimes 2 hores, redis-cache arribarà al seu
      maxmemory d'1 GiB en menys de 2 hores i començarà a expulsar claus.
      Memòria lliure actual: {{ $value | humanize1024 }}B.
      Acció: ampliar maxmemory o revisar el TTL de les claus de disponibilitat.
    runbook_url: "https://runbooks.rutasnorte.example/redis-memoria"

Nota sobre la sintaxi: predict_linear necessita un vector de rang. En aplicar-lo sobre una resta de dos gauges cal fer servir un subquery ([2h:]), perquè l'expressió resultant no és una sèrie simple. És un detall que confon molt.

Alternativa més simple, sobre una sola mètrica:

predict_linear(redis_memory_used_bytes{entorn="pro"}[2h], 2*3600)
  > avg(redis_memory_max_bytes{entorn="pro"})

3. Alerta sobre el símptoma.

- alert: RedisTaxaEncertsDegradada
  expr: |
    (
      sum(rate(redis_keyspace_hits_total{entorn="pro"}[10m]))
      / (sum(rate(redis_keyspace_hits_total{entorn="pro"}[10m]))
         + sum(rate(redis_keyspace_misses_total{entorn="pro"}[10m])))
    ) < 0.80
  for: 15m
  labels:
    severity: avis
    component: redis-cache
    equip: plataforma
  annotations:
    summary: "La taxa d'encerts de redis-cache ha caigut al {{ $value | humanizePercentage }}"
    description: >
      El normal és un 95%. Amb una taxa baixa, api-reserves consulta
      postgres-reserves molt més del previst i la latència puja.
      Comprovar si redis-cache està expulsant claus per falta de memòria.
    runbook_url: "https://runbooks.rutasnorte.example/redis-encerts"

- alert: RedisExpulsantClaus
  expr: sum(rate(redis_evicted_keys_total{entorn="pro"}[5m])) > 10
  for: 10m
  labels:
    severity: avis
    component: redis-cache
    equip: plataforma
  annotations:
    summary: "redis-cache expulsa {{ $value | humanize }} claus/s per falta de memòria"
    runbook_url: "https://runbooks.rutasnorte.example/redis-memoria"

Per què calen totes dues. Compleixen funcions diferents en el temps:

  • La predictiva avisa amb dues hores de marge, quan encara es pot actuar sense presses i sense impacte per al client. És una alerta de capacitat.
  • La de símptoma cobreix el cas en què la predicció falli: un canvi brusc de patró (per exemple, un desplegament que emmagatzema en cau objectes molt més grans) pot omplir Redis en minuts sense que cap tendència ho anticipi. La regressió lineal només prediu bé el que es comporta linealment.

Confiar només en la predictiva és assumir que el futur s'assembla al passat. Confiar només en el símptoma és renunciar a prevenir.

4. Regles d'inhibició.

inhibit_rules:
  # Si Redis ja està expulsant claus, la predicció que s'omplirà
  # ja no aporta res: el fet ha passat.
  - source_matchers:
      - alertname = "RedisExpulsantClaus"
    target_matchers:
      - alertname = "RedisCacheSOmplira"
    equal: ['component', 'namespace']

  # Si la latència d'api-reserves està disparada (símptoma que pateix el
  # client), els avisos de redis-cache són la causa: no calen dues
  # notificacions separades del mateix incident.
  - source_matchers:
      - alertname = "ApiReservesLatenciaAlta"
      - severity = "critica"
    target_matchers:
      - component = "redis-cache"
      - severity = "avis"
    equal: ['namespace']

La primera regla és un exemple perfecte del criteri d'inhibició: quan la predicció es compleix, la predicció sobra.

Solució 3

1. Pot Alertmanager encaminar per franja horària?

Sí. Des d'Alertmanager 0.22 existeixen els intervals de temps (time_intervals), que es referencien a les rutes amb active_time_intervals (la ruta només aplica dins de l'interval) o mute_time_intervals (la ruta se silencia dins de l'interval).

time_intervals:
  - name: horari-laboral
    time_intervals:
      - weekdays: ['monday:friday']
        times:
          - start_time: '08:00'
            end_time: '20:00'
        location: 'Europe/Madrid'

  - name: fora-dhorari
    time_intervals:
      - weekdays: ['monday:friday']
        times:
          - start_time: '20:00'
            end_time: '24:00'
          - start_time: '00:00'
            end_time: '08:00'
        location: 'Europe/Madrid'
      - weekdays: ['saturday', 'sunday']
        location: 'Europe/Madrid'

El camp location és imprescindible: sense ell, Alertmanager fa servir UTC i a l'estiu la guàrdia començaria dues hores abans del previst.

2. Arbre de rutes complet.

route:
  receiver: 'equip-plataforma-slack'
  group_by: ['alertname', 'namespace', 'component']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h

  routes:
    # ------------------------------------------------------------------
    # 1. Equip de dades: SEMPRE al seu canal, MAI a PagerDuty.
    #    Va primer perquè la primera coincidència guanya.
    # ------------------------------------------------------------------
    - matchers:
        - equip = "dades"
      receiver: 'equip-dades-slack'
      repeat_interval: 12h

    # ------------------------------------------------------------------
    # 2. dev i pre: no desperten mai ningú.
    # ------------------------------------------------------------------
    - matchers:
        - namespace =~ "rutas-norte-(dev|pre)"
      receiver: 'canal-desenvolupament'
      group_wait: 5m
      repeat_interval: 24h

    # ------------------------------------------------------------------
    # 3. FORA D'HORARI: només les crítiques de producció desperten.
    # ------------------------------------------------------------------
    - matchers:
        - severity = "critica"
        - namespace =~ "rutas-norte-pro|monitoratge"
      active_time_intervals: ['fora-dhorari']
      receiver: 'guardia-pagerduty'
      group_wait: 10s
      repeat_interval: 1h
      continue: true          # que també quedi registre a Slack

    # 3b. La resta, fora d'horari, només a Slack: es llegeix al matí.
    - matchers:
        - namespace =~ "rutas-norte-pro|monitoratge"
      active_time_intervals: ['fora-dhorari']
      receiver: 'equip-plataforma-slack'
      group_wait: 5m
      repeat_interval: 12h     # sense insistir de matinada

    # ------------------------------------------------------------------
    # 4. HORARI LABORAL: tot a Slack, amb les crítiques més àgils.
    # ------------------------------------------------------------------
    - matchers:
        - severity = "critica"
      active_time_intervals: ['horari-laboral']
      receiver: 'equip-plataforma-slack'
      group_wait: 10s
      repeat_interval: 1h

    - active_time_intervals: ['horari-laboral']
      receiver: 'equip-plataforma-slack'

Punts a destacar del disseny:

  • L'ordre importa: la ruta de l'equip de dades va primer perquè les seves alertes crítiques no acabin a PagerDuty per la ruta 3.
  • continue: true a la ruta 3 permet que l'alerta arribi als dos llocs: PagerDuty desperta algú i Slack deixa constància per a la revisió de l'endemà.
  • Les alertes no crítiques fora d'horari tenen repeat_interval: 12h per no omplir el canal de matinada.

Consideració de robustesa: aquest encaminament per horari assumeix que la guàrdia és sempre la mateixa persona. En equips amb rotació, l'habitual és delegar la lògica de torns a la mateixa eina de guàrdia (PagerDuty, Opsgenie), que gestiona calendaris, escalats i substitucions molt millor que Alertmanager. Aquí les regles horàries serveixen sobretot per decidir què mereix despertar algú, no a qui.

3. Silenci per a la migració programada.

kubectl -n monitoratge port-forward svc/monitoratge-kube-pr-alertmanager 9093:9093 &

curl -s -X POST http://localhost:9093/api/v2/silences \
  -H 'Content-Type: application/json' \
  -d '{
    "matchers": [
      {"name": "component", "value": "postgres-reserves", "isRegex": false},
      {"name": "namespace", "value": "rutas-norte-pro",   "isRegex": false}
    ],
    "startsAt": "2026-05-02T00:00:00Z",
    "endsAt":   "2026-05-02T03:15:00Z",
    "createdBy": "[email protected]",
    "comment": "Migració programada de postgres-reserves 02:00-05:00 CEST. Tiquet OPS-1842. Aprovada al comitè de canvis del 28/04. Responsable de guàrdia: Marta Ruiz."
  }' | jq -r '.silenceID'

Bones pràctiques aplicades i el seu perquè:

  • Hores en UTC. 02:00-05:00 CEST és 00:00-03:00 UTC. És l'error més freqüent en crear silencis i deixa alertes sense silenciar justament durant la finestra.
  • 15 minuts de marge sobre la finestra prevista (fins a les 03:15 UTC), perquè les migracions s'allarguen.
  • Matchers específics: només postgres-reserves a pro. Un silenci de tot el namespace amagaria un problema no relacionat a botiga-web durant tres hores.
  • Comentari amb tiquet, aprovació i responsable: d'aquí a sis setmanes, qualsevol pot reconstruir per què existia.
  • endsAt obligatori, mai indefinit.

I el que no s'ha de silenciar durant la migració:

# Comprovar QUINES alertes queden cobertes pel silenci abans d'aplicar-lo:
# les de la plataforma completa (PlataformaSenseVendes, BotigaWebCaiguda) NO
# porten l'etiqueta component=postgres-reserves, així que continuen actives.
# Això és deliberat: si la migració deixa la plataforma sense vendre, cal
# assabentar-se'n immediatament encara que la causa sigui la migració mateixa.

curl -s http://localhost:9093/api/v2/alerts | \
  jq -r '.[] | select(.status.silencedBy | length > 0) | .labels.alertname'

Verificació després de la finestra:

# Confirmar que el silenci ha expirat i no en queda cap d'oblidat
curl -s http://localhost:9093/api/v2/silences | \
  jq -r '.[] | select(.status.state=="active") |
  "\(.id)\t\(.createdBy)\t\(.comment)\tfins \(.endsAt)"'

Aquesta última ordre s'hauria d'executar com a part de la revisió setmanal de l'equip: un silenci oblidat és una alerta que ja no existeix.

Conclusió

Rutas Norte ja no només recorda: ara mostra i avisa. En aquesta lliçó hem:

  • Connectat Grafana a Prometheus i après l'anatomia d'un panell: la consulta amb $__rate_interval, la llegenda amb plantilles d'etiquetes, les unitats i els llindars que fan llegible un número, i els quatre tipus que de debò es fan servir, inclòs el mapa de calor per veure la distribució completa de latències.
  • Fet servir variables de panell per tenir un únic quadre de comandament que serveix per a dev, pre i pro, evitant tres còpies que es desincronitzen.
  • Construït el quadre de comandament de Rutas Norte panell a panell: una fila de resum executiu encapçalada per les reserves confirmades per minut —perquè si això cau a zero tant se val que tots els pods estiguin Running— i una fila per component amb els quatre indicadors daurats.
  • Proveït els quadres de comandament com a codi en ConfigMaps amb l'etiqueta grafana_dashboard, perquè sobrevisquin a la recreació del pod i es revisin en una petició de canvi.
  • Escrit el catàleg d'alertes amb PrometheusRule, entenent que for és el camp que separa un sistema usable del soroll, que predict_linear permet avisar que el disc de PostgreSQL s'omplirà abans que s'ompli, i que runbook_url no és opcional.
  • Configurat Alertmanager: l'arbre de rutes amb els seus quatre temps, els receptors, la inhibició que converteix tretze notificacions en una quan cau un node, i els silencis amb data de fi i tiquet per als manteniments programats.
  • I per damunt de la mecànica, el criteri: alertar sobre símptomes que percep el client, no sobre causes; que cada alerta sigui accionable; i fer servir els SLO i el pressupost d'error com a eina objectiva per decidir quan cal deixar d'afegir funcionalitat i posar-se a estabilitzar.

Però hi ha una pregunta que ni les mètriques ni els quadres de comandament poden respondre. Quan ApiReservesTaxaErrorAlta dispara a les 03:14, sabem que el 7 % de les peticions fallen, sabem en quina ruta i amb quina latència. El que no sabem és per què. L'excepció concreta, el missatge de la base de dades, la traça que assenyala la línia de codi: això no viu en una mètrica. Viu en els logs, que avui estan repartits pels nodes, es perden quan un pod es recrea i no hi ha manera de buscar-hi.

A 07-05 muntarem la pila completa de registre centralitzat que vam anunciar a 06-02 quan vam desplegar el recol·lector com a DaemonSet: Elasticsearch, Fluentd i Kibana. Veurem com passar de kubectl logs a una cerca que correlaciona tots els components, per què els logs estructurats en JSON són la decisió que més millora tot el sistema, i —molt important per a una plataforma que desa el DNI i el telèfon dels seus clients— què mai ha d'acabar escrit en un log.

Curs de Kubernetes

Mòdul 1: Introducció a Kubernetes

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats