En la lección anterior instalamos metrics-server y por fin supimos cuánto consume cada componente de Rutas Norte ahora mismo. También descubrimos su límite infranqueable: no recuerda nada. La pregunta "¿qué pasó anoche a las tres de la madrugada?" sigue sin respuesta, y con ella todas las que de verdad importan: ¿cuántas reservas por minuto estamos confirmando?, ¿ha subido la latencia desde el último despliegue?, ¿cuánto falta para que se llene el disco de postgres-reservas?

Esta lección despliega Prometheus, el estándar de facto para monitorizar Kubernetes y el segundo proyecto graduado de la CNCF después del propio Kubernetes. Vamos a entender su modelo de datos, de dónde salen realmente las métricas de un clúster (una de las mayores fuentes de confusión para quien empieza), cómo instrumentar api-reservas con métricas de negocio, cómo desplegar el stack con el operador que anunciamos en 06-07, y cómo escribir PromQL que responda a preguntas reales de Rutas Norte. Al terminar, la plataforma tendrá memoria.

Contenido

  1. Qué aporta Prometheus frente a metrics-server
  2. El modelo de extracción (pull) y los endpoints /metrics
  3. El modelo de datos: series, etiquetas y los cuatro tipos de métrica
  4. De dónde salen las métricas de un clúster: la tabla que aclara la confusión
  5. Instrumentar api-reservas con métricas de negocio
  6. Desplegar el kube-prometheus-stack con Helm
  7. El Prometheus Operator y sus recursos personalizados
  8. ServiceMonitor de Rutas Norte y diagnóstico de objetivos
  9. PromQL desde cero y con sentido
  10. Los cuatro indicadores dorados aplicados a Rutas Norte
  11. Almacenamiento, retención y los límites de Prometheus
  12. Errores comunes y consejos
  13. Ejercicios

  1. Qué aporta Prometheus frente a metrics-server

Tres cosas, y las tres son las que nos faltaban.

Histórico. Prometheus guarda cada muestra en una base de datos de series temporales en disco. Con la retención por defecto del stack tendremos varios días; configurando el almacenamiento, semanas o meses. La pregunta de las tres de la madrugada tiene respuesta.

Consultas. PromQL es un lenguaje completo para agregar, filtrar, derivar y comparar series. No es "ver un número": es "dame la tasa de errores 5xx de api-reservas en el entorno de producción, agrupada por ruta, durante la última hora".

Alertas. Prometheus evalúa expresiones periódicamente y, cuando se cumplen durante un tiempo sostenido, dispara alertas que Alertmanager enruta a quien corresponda. Eso es lo que hace que alguien se entere a las tres de la madrugada sin estar mirando una pantalla.

Y una cuarta, menos obvia pero decisiva: cualquier cosa puede exponer métricas. metrics-server solo sabe de CPU y memoria. Prometheus recoge lo que sea que un endpoint HTTP le ofrezca: peticiones por ruta, reservas confirmadas, tamaño de la cola de correos, conexiones activas de PostgreSQL, certificados a punto de caducar.

Capacidad metrics-server Prometheus
Histórico ~1 minuto en memoria Días o meses en disco
Métricas Solo CPU y memoria Cualquiera expuesta en /metrics
Lenguaje de consulta No PromQL
Alertas No Sí (con Alertmanager, 07-04)
Métricas de negocio No
Coste de operación Trivial Considerable (RAM y disco)
¿Lo usa el HPA? Sí, de forma nativa Sí, con un adaptador (09-04)

Los dos conviven. No sustituimos metrics-server: lo complementamos.

  1. El modelo de extracción (pull) y los endpoints /metrics

Aquí está la decisión de diseño que define todo lo demás.

La mayoría de sistemas de monitorización tradicionales funcionan por empuje (push): la aplicación envía sus métricas a un servidor central. Prometheus hace lo contrario: extracción (pull). Prometheus tiene una lista de objetivos (targets) y, cada cierto intervalo (típicamente 30 segundos), hace un GET a cada uno.

flowchart LR
    P[Prometheus] -->|"GET /metrics cada 30s"| A["api-reservas :8080/metrics"]
    P -->|"GET /metrics cada 30s"| B["exportador-pg :9187/metrics"]
    P -->|"GET /metrics cada 30s"| C["kube-state-metrics :8080/metrics"]
    P -->|"GET /metrics cada 30s"| D["node-exporter :9100/metrics"]
    P --> TSDB[(TSDB en disco<br/>bloques de 2 h)]

¿Por qué es mejor extraer que recibir?

  • Prometheus sabe si un objetivo está caído. Si el GET falla, la métrica sintética up vale 0. Con push, la ausencia de datos es ambigua: ¿la aplicación está caída o simplemente no ha enviado nada?
  • El control del ritmo lo tiene el sistema de monitorización, no las aplicaciones. Cien aplicaciones mal programadas no pueden inundarlo.
  • Descubrimiento automático. Prometheus consulta la API de Kubernetes para saber qué pods existen. Un pod nuevo aparece como objetivo automáticamente, sin configurar nada en la aplicación.
  • Depuración trivial. Puedes hacer curl al endpoint /metrics desde tu portátil y ver exactamente lo mismo que ve Prometheus.

La excepción son los procesos de vida muy corta, como nuestro CronJob informes-ocupacion: puede terminar antes de que Prometheus lo escanee. Para esos casos existe el Pushgateway, un intermediario al que el job empuja sus métricas y que Prometheus escanea después. Es la única excepción legítima y conviene usarla con moderación.

Cómo se ve un endpoint /metrics

kubectl -n rutas-norte-pro port-forward deploy/api-reservas 8080:8080 &
curl -s localhost:8080/metrics | head -30
# HELP rutasnorte_peticiones_total Total de peticiones HTTP atendidas
# TYPE rutasnorte_peticiones_total counter
rutasnorte_peticiones_total{ruta="/api/rutas",metodo="GET",codigo="200"} 184203
rutasnorte_peticiones_total{ruta="/api/rutas",metodo="GET",codigo="500"} 47
rutasnorte_peticiones_total{ruta="/api/reservas",metodo="POST",codigo="201"} 9821
rutasnorte_peticiones_total{ruta="/api/reservas",metodo="POST",codigo="422"} 318
# HELP rutasnorte_reservas_confirmadas_total Reservas confirmadas y pagadas
# TYPE rutasnorte_reservas_confirmadas_total counter
rutasnorte_reservas_confirmadas_total{origen="web"} 8742
rutasnorte_reservas_confirmadas_total{origen="movil"} 1079
# HELP rutasnorte_conexiones_pool_activas Conexiones en uso del pool de PostgreSQL
# TYPE rutasnorte_conexiones_pool_activas gauge
rutasnorte_conexiones_pool_activas 7

Es texto plano. Cada línea es nombre{etiquetas} valor. Las líneas # HELP y # TYPE son metadatos: descripción y tipo. Ese formato, deliberadamente simple, es todo el contrato entre una aplicación y Prometheus.

  1. El modelo de datos: series, etiquetas y los cuatro tipos de métrica

Series temporales y etiquetas

Una serie temporal en Prometheus se identifica de forma única por su nombre de métrica más el conjunto exacto de sus etiquetas. Esto es fundamental:

rutasnorte_peticiones_total{ruta="/api/rutas", metodo="GET", codigo="200"}   → serie A
rutasnorte_peticiones_total{ruta="/api/rutas", metodo="GET", codigo="500"}   → serie B (distinta)
rutasnorte_peticiones_total{ruta="/api/reservas", metodo="POST", codigo="201"} → serie C (distinta)

Cambiar el valor de una sola etiqueta crea una serie completamente nueva. Cada serie ocupa memoria y disco, y de ahí nace el peligro más citado de Prometheus: la cardinalidad.

Regla de oro de la cardinalidad: nunca uses como etiqueta un valor con muchos valores posibles. Nada de identificadores de usuario, de reserva, de sesión, direcciones IP de cliente, marcas de tiempo o mensajes de error completos.

Un ejemplo real del peligro. Si api-reservas etiquetara cada petición con el dni del cliente:

rutasnorte_peticiones_total{dni="12345678Z", ruta="/api/reservas"}   ← ¡NO!

Con 200 000 clientes y 15 rutas, tendrías 3 millones de series solo de esa métrica. Prometheus se quedaría sin memoria en minutos. Y, además, estarías metiendo datos personales en el sistema de monitorización, con las implicaciones que veremos en 07-05.

Etiquetas que Prometheus añade por sí mismo a todo lo que recoge en Kubernetes (y que usaremos constantemente):

Etiqueta Ejemplo Origen
job api-reservas Nombre del trabajo de recolección
instance 10.244.2.17:8080 IP y puerto del objetivo concreto
namespace rutas-norte-pro Metadatos de Kubernetes
pod api-reservas-7d9f8c4b5-x2klm Metadatos de Kubernetes
container api Metadatos de Kubernetes
node rutas-norte-worker-2 Metadatos de Kubernetes

Los cuatro tipos de métrica

Tipo Puede Ejemplo en Rutas Norte Función típica para consultarlo
counter Solo subir (o reiniciarse a 0) rutasnorte_peticiones_total rate(), increase()
gauge Subir y bajar rutasnorte_conexiones_pool_activas Valor directo, avg, max
histogram Contar en cubetas configurables rutasnorte_duracion_peticion_segundos histogram_quantile()
summary Percentiles calculados en el cliente Poco recomendable en general Valor directo

counter (contador). Un valor que solo crece: peticiones atendidas, errores, bytes enviados, reservas confirmadas. Nunca se consulta directamente. El valor absoluto 184203 no dice nada: ¿son muchas? ¿desde cuándo? Se consulta siempre con rate() o increase(), que calculan cuánto ha crecido por unidad de tiempo. Por convención, su nombre acaba en _total.

Prometheus detecta automáticamente los reinicios del contador (cuando un pod se recrea, el contador vuelve a 0) y rate() los compensa. No tienes que preocuparte por ello.

gauge (medidor). Un valor que sube y baja: conexiones activas, temperatura, memoria en uso, tamaño de una cola. Se consulta directamente. Es el tipo de casi todo lo que expone cAdvisor sobre memoria.

histogram (histograma). El tipo más potente y el peor entendido. En lugar de guardar cada valor individual, la aplicación clasifica cada observación en cubetas acumulativas predefinidas. Una métrica de histograma expone en realidad tres cosas:

# Cubetas acumulativas: "cuántas peticiones tardaron MENOS de X segundos"
rutasnorte_duracion_peticion_segundos_bucket{ruta="/api/reservas",le="0.05"} 8140
rutasnorte_duracion_peticion_segundos_bucket{ruta="/api/reservas",le="0.1"}  9210
rutasnorte_duracion_peticion_segundos_bucket{ruta="/api/reservas",le="0.5"}  9780
rutasnorte_duracion_peticion_segundos_bucket{ruta="/api/reservas",le="1"}    9812
rutasnorte_duracion_peticion_segundos_bucket{ruta="/api/reservas",le="+Inf"} 9821
# Suma de todos los valores observados (para calcular la media)
rutasnorte_duracion_peticion_segundos_sum{ruta="/api/reservas"} 743.28
# Número total de observaciones
rutasnorte_duracion_peticion_segundos_count{ruta="/api/reservas"} 9821

Lectura: 8140 peticiones tardaron menos de 50 ms; 9210 tardaron menos de 100 ms (incluidas las 8140 anteriores: son acumulativas); 9821 en total, luego 9 peticiones tardaron más de 1 segundo.

Con esos datos, la función histogram_quantile() puede estimar cualquier percentil. La ventaja enorme frente al summary: como las cubetas son contadores normales, se pueden sumar entre pods. Puedes calcular el percentil 95 de latencia de todo api-reservas agregando sus seis réplicas. Con un summary eso es matemáticamente imposible.

El coste: cada cubeta es una serie. Un histograma con 10 cubetas y 15 rutas son 150 series por pod. Elige las cubetas con criterio, ajustadas a la latencia real de tu servicio.

summary (resumen). La aplicación calcula los percentiles ella misma y los expone. Barato de consultar, pero no agregable entre instancias: el percentil 95 del pod A y el del pod B no se pueden combinar para obtener el del servicio. En Kubernetes, donde todo tiene varias réplicas, esto lo descarta casi siempre. Úsalo solo si necesitas un percentil exacto de una única instancia.

Convenciones de nombres

Prometheus tiene convenciones que conviene respetar porque las herramientas y los cuadros de mando las asumen:

  • Prefijo con el nombre del sistema: rutasnorte_.
  • Unidad base en el nombre y siempre en unidades base del SI: _segundos no _ms, _bytes no _mb.
  • Los contadores acaban en _total.
  • Nombres en snake_case, sin mayúsculas.

  1. De dónde salen las métricas de un clúster: la tabla que aclara la confusión

Esta es la sección que resuelve la duda que tiene todo el mundo la primera semana: "¿por qué necesito cuatro cosas distintas para monitorizar Kubernetes?".

La respuesta corta: porque son cuatro capas de realidad diferentes y ninguna de ellas puede ver las otras.

Fuente Qué observa Ejemplo de métrica ¿Hay que instalarlo?
kubelet / cAdvisor El uso real de los contenedores (cgroups) container_memory_working_set_bytes No: viene en el kubelet
node-exporter El sistema operativo del nodo node_filesystem_avail_bytes Sí, como DaemonSet
kube-state-metrics El estado de los objetos de la API kube_deployment_status_replicas_unavailable Sí, como Deployment
Exportadores de aplicación Las tripas de un software concreto pg_stat_database_numbackends Sí, uno por software
Instrumentación propia La lógica de negocio rutasnorte_reservas_confirmadas_total Sí, en el código

Vamos una a una, porque la distinción importa muchísimo a la hora de escribir consultas.

kubelet / cAdvisor: lo que los contenedores consumen

Es la misma fuente que alimenta a metrics-server (07-02), pero Prometheus la lee directamente y con mucho más detalle. Métricas clave:

container_cpu_usage_seconds_total{pod="api-reservas-...", container="api"}
container_memory_working_set_bytes{pod="api-reservas-...", container="api"}
container_cpu_cfs_throttled_periods_total{pod="api-reservas-...", container="api"}
container_network_receive_bytes_total{pod="api-reservas-..."}
container_fs_writes_bytes_total{pod="postgres-reservas-0"}

Fíjate en la tercera: el contador de periodos estrangulados que en 07-02 dijimos que kubectl top no puede darnos. Aquí está, y es la que confirmará el diagnóstico de throttling de api-reservas.

Aviso práctico: estas métricas también aparecen con container="" (el valor agregado del pod, incluido el contenedor pause de infraestructura). Casi siempre querrás filtrar con container!="" para no contar el doble.

node-exporter: lo que le pasa a la máquina

Es un DaemonSet (un pod por nodo, como estudiamos en 06-02) que lee /proc y /sys del sistema anfitrión. Ve cosas que ningún contenedor puede ver:

node_filesystem_avail_bytes{mountpoint="/var/lib/kubelet"}
node_memory_MemAvailable_bytes
node_load1
node_cpu_seconds_total{mode="idle"}
node_network_transmit_bytes_total

Es el que te avisa de que el disco del nodo se está llenando, de que la carga media está por las nubes o de que la tarjeta de red está saturada. Ningún contenedor puede decirte eso de sí mismo.

kube-state-metrics: lo que la API de Kubernetes cree

Esta es la que más cuesta entender y la que más falta hace. No mide consumo de nada. Se conecta a la API de Kubernetes y convierte el estado de los objetos en métricas:

kube_deployment_spec_replicas{deployment="api-reservas"}                  6
kube_deployment_status_replicas_available{deployment="api-reservas"}      4
kube_pod_container_status_restarts_total{pod="api-reservas-...", container="api"}  7
kube_pod_status_phase{pod="postgres-reservas-0", phase="Running"}         1
kube_job_status_failed{job_name="informes-ocupacion-28934520"}            1
kube_persistentvolumeclaim_status_phase{persistentvolumeclaim="datos-postgres-reservas-0", phase="Bound"} 1
kube_pod_container_resource_requests{pod="api-reservas-...", resource="cpu"}  0.2

Con estas métricas puedes alertar de cosas que ninguna de las otras fuentes conoce: un Deployment con menos réplicas disponibles de las deseadas, un pod reiniciándose en bucle, un Job que ha fallado, un PVC que lleva veinte minutos en Pending.

Y algo muy útil: kube_pod_container_resource_requests expone las requests declaradas. Cruzando esa con container_memory_working_set_bytes de cAdvisor obtienes, en una sola consulta PromQL, exactamente el análisis de recalibración que en 07-02 tuvimos que hacer con un script de bash.

Exportadores: traductores para software de terceros

PostgreSQL no habla el formato de Prometheus. Un exportador es un proceso que se conecta al software, consulta sus estadísticas internas y las traduce a /metrics.

Y aquí conectamos con 06-04: ya lo tenemos desplegado. Cuando añadimos el sidecar exportador de métricas al pod de postgres-reservas, dijimos explícitamente que Prometheus lo consumiría en esta lección. Ha llegado el momento.

# Recordatorio del sidecar que añadimos en 06-04, dentro del StatefulSet
# postgres-reservas. Nótese que es un sidecar nativo (initContainer con
# restartPolicy: Always), como vimos en aquella lección.
initContainers:
  - name: exportador-pg
    image: quay.io/prometheuscommunity/postgres-exporter:v0.15.0
    restartPolicy: Always          # sidecar nativo 1.29+
    ports:
      - name: metricas
        containerPort: 9187
    env:
      - name: DATA_SOURCE_URI
        value: "127.0.0.1:5432/reservas?sslmode=disable"
      - name: DATA_SOURCE_USER
        valueFrom:
          secretKeyRef:
            name: credenciales-postgres
            key: usuario-metricas
      - name: DATA_SOURCE_PASS
        valueFrom:
          secretKeyRef:
            name: credenciales-postgres
            key: password-metricas
    resources:
      requests:
        cpu: "20m"
        memory: "32Mi"
      limits:
        cpu: "100m"
        memory: "64Mi"

Lo que expone, y que nos hará falta:

pg_stat_database_numbackends{datname="reservas"}          # conexiones abiertas
pg_stat_database_xact_commit{datname="reservas"}          # transacciones confirmadas
pg_stat_database_deadlocks{datname="reservas"}            # interbloqueos
pg_database_size_bytes{datname="reservas"}                # tamaño de la base de datos
pg_stat_replication_replay_lag                            # retraso de réplica
pg_up                                                     # ¿responde PostgreSQL?

Existen exportadores para prácticamente todo: redis_exporter para redis-cache, nginx-prometheus-exporter para tienda-web, blackbox_exporter para comprobar desde fuera que https://www.rutasnorte.example responde.

Instrumentación propia: lo único que sabe de negocio

Ninguna de las cuatro fuentes anteriores puede decirte cuántos billetes has vendido. Eso solo lo sabe tu código. Es la siguiente sección.

El resumen visual

flowchart TB
    subgraph Nodo["Nodo rutas-norte-worker-2"]
        subgraph Pod1["Pod api-reservas"]
            APP["contenedor api<br/>/metrics propio"]
        end
        subgraph Pod2["Pod postgres-reservas-0"]
            PG["contenedor postgres"]
            EXP["sidecar exportador<br/>:9187/metrics"]
            EXP -.consulta.-> PG
        end
        KUBELET["kubelet + cAdvisor<br/>uso real de cgroups"]
        NE["node-exporter<br/>DaemonSet<br/>/proc y /sys"]
    end
    KSM["kube-state-metrics<br/>estado de los objetos"]
    API[(API Server)]
    KSM -.consulta.-> API
    PROM[Prometheus]
    APP --> PROM
    EXP --> PROM
    KUBELET --> PROM
    NE --> PROM
    KSM --> PROM

  1. Instrumentar api-reservas con métricas de negocio

La instrumentación propia es lo que separa un cuadro de mando de infraestructura de uno que le importa al negocio. "El pod usa 300 Mi de RAM" no interesa al director de Rutas Norte. "Estamos confirmando 12 reservas por minuto, un 40 % menos que ayer a esta hora" sí.

api-reservas está en Node.js, así que usamos la librería cliente oficial prom-client. Existen librerías equivalentes para Java, Go, Python, .NET y prácticamente cualquier lenguaje.

// metricas.js — instrumentación de api-reservas
const client = require('prom-client');

// El registro es el contenedor de todas las métricas de este proceso.
const registro = new client.Registry();

// Etiquetas que se añaden a TODAS las métricas de este proceso.
// Vienen de la Downward API (03-03), así que cada pod se identifica solo.
registro.setDefaultLabels({
  componente: 'api-reservas',
  entorno: process.env.ENTORNO || 'dev',
  version: process.env.APP_VERSION || 'desconocida',
});

// Métricas por defecto del runtime: memoria del heap, GC, event loop, etc.
// Muy útiles y gratis: una sola línea.
client.collectDefaultMetrics({ register: registro });

// ---------------------------------------------------------------------------
// 1. COUNTER: peticiones HTTP atendidas.
//    Etiquetas de BAJA cardinalidad: ruta normalizada (no la URL real),
//    método y código. Jamás el id de reserva ni el DNI del cliente.
// ---------------------------------------------------------------------------
const peticionesTotal = new client.Counter({
  name: 'rutasnorte_peticiones_total',
  help: 'Total de peticiones HTTP atendidas por api-reservas',
  labelNames: ['ruta', 'metodo', 'codigo'],
  registers: [registro],
});

// ---------------------------------------------------------------------------
// 2. HISTOGRAM: duración de las peticiones.
//    Las cubetas están elegidas para NUESTRA latencia real: la mayoría de
//    peticiones tardan entre 20 y 200 ms, y el SLO está en 500 ms.
//    Cubetas mal elegidas dan percentiles inútiles.
// ---------------------------------------------------------------------------
const duracionPeticion = new client.Histogram({
  name: 'rutasnorte_duracion_peticion_segundos',
  help: 'Duración de las peticiones HTTP en segundos',
  labelNames: ['ruta', 'metodo'],
  buckets: [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
  registers: [registro],
});

// ---------------------------------------------------------------------------
// 3. COUNTER de negocio: reservas confirmadas y pagadas.
//    Esta es LA métrica que mira el negocio. Si cae a cero, da igual que
//    todos los pods estén Running: la plataforma no está vendiendo.
// ---------------------------------------------------------------------------
const reservasConfirmadas = new client.Counter({
  name: 'rutasnorte_reservas_confirmadas_total',
  help: 'Reservas confirmadas y pagadas correctamente',
  labelNames: ['origen', 'trayecto_tipo'],   // 'web'|'movil', 'nacional'|'regional'
  registers: [registro],
});

// ---------------------------------------------------------------------------
// 4. GAUGE: estado del pool de conexiones a PostgreSQL.
//    Es exactamente el dato que en 07-01 provocaba el incidente invisible.
//    Ahora, además de que la readiness lo detecte, quedará registrado.
// ---------------------------------------------------------------------------
const conexionesPool = new client.Gauge({
  name: 'rutasnorte_conexiones_pool_activas',
  help: 'Conexiones actualmente en uso del pool de PostgreSQL',
  registers: [registro],
  collect() {
    // collect() se ejecuta en el momento del scrape: siempre valor fresco.
    this.set(poolPg.totalCount - poolPg.idleCount);
  },
});

// ---------------------------------------------------------------------------
// 5. COUNTER de la pasarela de pagos externa.
//    Nos permitirá distinguir "falla nuestro código" de "falla el proveedor".
// ---------------------------------------------------------------------------
const llamadasPasarela = new client.Counter({
  name: 'rutasnorte_pasarela_pagos_llamadas_total',
  help: 'Llamadas a la pasarela de pagos externa',
  labelNames: ['resultado'],   // 'ok' | 'error' | 'timeout'
  registers: [registro],
});

module.exports = {
  registro, peticionesTotal, duracionPeticion,
  reservasConfirmadas, conexionesPool, llamadasPasarela,
};

Y el middleware que lo conecta con Express:

// servidor.js
const express = require('express');
const m = require('./metricas');
const app = express();

// Middleware que mide TODAS las peticiones.
app.use((req, res, next) => {
  // IMPORTANTE: usamos la ruta del enrutador (/api/reservas/:id), no la URL
  // real (/api/reservas/48213). Si usáramos la URL real, cada reserva
  // crearía una serie nueva: explosión de cardinalidad garantizada.
  const finTemporizador = m.duracionPeticion.startTimer();

  res.on('finish', () => {
    const ruta = req.route ? req.baseUrl + req.route.path : 'desconocida';
    finTemporizador({ ruta, metodo: req.method });
    m.peticionesTotal.inc({ ruta, metodo: req.method, codigo: res.statusCode });
  });
  next();
});

// El endpoint que Prometheus escaneará.
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', m.registro.contentType);
  res.end(await m.registro.metrics());
});

// En la lógica de negocio, al confirmar una reserva:
async function confirmarReserva(reserva) {
  await guardarEnBaseDeDatos(reserva);
  m.reservasConfirmadas.inc({
    origen: reserva.origen,
    trayecto_tipo: reserva.esNacional ? 'nacional' : 'regional',
  });
}

app.listen(8080);

Dos decisiones a subrayar:

  1. Ruta normalizada, no URL real. req.route.path da /api/reservas/:id, con lo que todas las reservas comparten serie. Usar req.originalUrl daría /api/reservas/48213, /api/reservas/48214... una serie por reserva. Es el error de cardinalidad más frecuente del mundo.
  2. /metrics en el mismo puerto que la aplicación, o en uno aparte. Aquí lo dejamos en el 8080 por simplicidad. En producción es preferible exponerlo en un puerto distinto (por ejemplo 9090) que no esté publicado en el Ingress, para que las métricas no sean accesibles desde internet. Las NetworkPolicies de rutas-norte-pro (04-06) deberán permitir el tráfico desde el namespace de monitorización a ese puerto: recuerda que toda conversación nueva necesita su política.

  1. Desplegar el kube-prometheus-stack con Helm

Instalar Prometheus a mano en Kubernetes significa gestionar unos veinte objetos entre Deployments, ConfigMaps, Services, RBAC y almacenamiento. La comunidad ha empaquetado todo eso en un chart de Helm llamado kube-prometheus-stack.

Nota: Helm es el gestor de paquetes de Kubernetes y lo estudiaremos a fondo en 10-03. Aquí lo usamos como herramienta de instalación; por ahora basta con entender que un chart es una plantilla parametrizable de manifiestos y que helm install los genera y los aplica.

# Añadir el repositorio de charts de la comunidad
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Namespace dedicado para toda la monitorización
kubectl create namespace monitorizacion
kubectl label namespace monitorizacion app.kubernetes.io/part-of=rutas-norte

Fichero de valores adaptado a Rutas Norte:

# k8s/base/monitorizacion/valores-prometheus.yaml
prometheus:
  prometheusSpec:
    # Retención: 30 días o 45 GiB, lo que llegue antes. Un límite de tamaño
    # es imprescindible: sin él, el disco se llena y Prometheus muere.
    retention: 30d
    retentionSize: "45GiB"

    # CRÍTICO: por defecto, el operador SOLO recoge ServiceMonitors que
    # lleven la etiqueta release=<nombre-del-release>. Poniéndolo a false,
    # recogerá los de cualquier namespace sin etiqueta especial.
    # Es la causa número 1 de "mi ServiceMonitor no aparece".
    serviceMonitorSelectorNilUsesHelmValues: false
    podMonitorSelectorNilUsesHelmValues: false
    ruleSelectorNilUsesHelmValues: false

    # Almacenamiento persistente con la StorageClass rápida de 05-04.
    # Sin esto, Prometheus guarda en un emptyDir y pierde TODO al reiniciarse:
    # exactamente el problema que veníamos a resolver.
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: rutasnorte-rapida
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 50Gi

    resources:
      requests:
        cpu: "500m"
        memory: "3Gi"
      limits:
        memory: "6Gi"

    # Etiquetas externas: identifican a ESTE Prometheus. Imprescindibles
    # si algún día federamos varios clústeres (11-05).
    externalLabels:
      cluster: rutas-norte
      region: eu-west

grafana:
  enabled: true                    # lo configuramos en 07-04
  adminPassword: cambiar-en-produccion

alertmanager:
  enabled: true                    # lo configuramos en 07-04

# node-exporter: un pod por nodo, como el recolector de logs de 06-02
nodeExporter:
  enabled: true

kubeStateMetrics:
  enabled: true

# En minikube, algunos componentes del plano de control no son accesibles
# como en un clúster real; los desactivamos para evitar objetivos en rojo.
kubeControllerManager:
  enabled: false
kubeScheduler:
  enabled: false
kubeEtcd:
  enabled: false
kubeProxy:
  enabled: false
helm install monitorizacion prometheus-community/kube-prometheus-stack \
  --namespace monitorizacion \
  --values k8s/base/monitorizacion/valores-prometheus.yaml \
  --wait --timeout 10m

Qué acabamos de instalar:

Componente Tipo Función
Prometheus Operator Deployment Traduce los recursos personalizados a configuración de Prometheus
Prometheus StatefulSet (creado por el operador) El servidor: recolecta, almacena y evalúa reglas
Alertmanager StatefulSet (creado por el operador) Enruta y agrupa las alertas (07-04)
Grafana Deployment Visualización (07-04)
node-exporter DaemonSet Métricas del sistema operativo de cada nodo
kube-state-metrics Deployment Métricas del estado de los objetos de la API
Reglas y cuadros de mando PrometheusRule y ConfigMaps Un conjunto muy completo de alertas ya escritas
kubectl -n monitorizacion get pods
NAME                                                     READY   STATUS    RESTARTS   AGE
alertmanager-monitorizacion-kube-pr-alertmanager-0       2/2     Running   0          3m
monitorizacion-grafana-6d84b8c7f9-w2xnk                  3/3     Running   0          3m
monitorizacion-kube-pr-operator-7c9b4d5f68-hj4kp         1/1     Running   0          3m
monitorizacion-kube-state-metrics-59d7b8c644-pl3mv       1/1     Running   0          3m
monitorizacion-prometheus-node-exporter-4kx8n            1/1     Running   0          3m
monitorizacion-prometheus-node-exporter-9wq2t            1/1     Running   0          3m
monitorizacion-prometheus-node-exporter-mz7bd            1/1     Running   0          3m
prometheus-monitorizacion-kube-pr-prometheus-0           2/2     Running   0          3m

Acceso a la interfaz de Prometheus:

kubectl -n monitorizacion port-forward svc/monitorizacion-kube-pr-prometheus 9090:9090
# Abrir http://localhost:9090

  1. El Prometheus Operator y sus recursos personalizados

Aquí se materializa lo que anunciamos en 06-07. El Prometheus Operator es el ejemplo canónico del patrón operador: un controlador que observa recursos personalizados y reconcilia el estado real hasta que coincida.

Sin operador, añadir un objetivo nuevo a Prometheus significa editar un fichero prometheus.yml, meterlo en un ConfigMap, recargar la configuración y esperar. Un proceso manual, centralizado y propenso a errores.

Con operador, el equipo que despliega api-reservas crea un objeto ServiceMonitor junto a su propia aplicación, en su propio namespace, y el operador lo detecta y regenera la configuración de Prometheus automáticamente. La monitorización pasa a ser responsabilidad descentralizada, versionada en Git junto al resto de manifiestos.

flowchart LR
    DEV["Equipo de api-reservas<br/>k8s/base/api-reservas/servicemonitor.yaml"] -->|kubectl apply| API[(API Server)]
    API -->|watch| OP[Prometheus Operator]
    OP -->|genera y actualiza| SEC["Secret con<br/>prometheus.yaml"]
    SEC -->|montado en| PROM[Pod de Prometheus]
    OP -->|recarga| PROM
    PROM -->|"GET /metrics"| SVC[Service api-reservas]

Los cinco recursos personalizados

Recurso Para qué sirve
Prometheus Declara una instancia de Prometheus: réplicas, retención, almacenamiento, qué monitores recoge
ServiceMonitor "Escanea los pods que hay detrás de este Service" — la forma habitual
PodMonitor "Escanea estos pods directamente" — cuando no hay Service
PrometheusRule Reglas de alerta y reglas de grabación (las desarrollamos en 07-04)
Alertmanager Declara una instancia de Alertmanager (07-04)

Y AlertmanagerConfig, para configurar el enrutado de alertas por namespace. También en 07-04.

kubectl get crd | grep monitoring.coreos.com
alertmanagerconfigs.monitoring.coreos.com     2026-08-06T09:31:12Z
alertmanagers.monitoring.coreos.com           2026-08-06T09:31:12Z
podmonitors.monitoring.coreos.com             2026-08-06T09:31:13Z
prometheuses.monitoring.coreos.com            2026-08-06T09:31:13Z
prometheusrules.monitoring.coreos.com         2026-08-06T09:31:13Z
servicemonitors.monitoring.coreos.com         2026-08-06T09:31:14Z

Como vimos en 06-06, todos estos son CRDs. Y como Certificate de cert-manager o VolumeSnapshot, se manejan con kubectl exactamente igual que un Deployment.

  1. ServiceMonitor de Rutas Norte y diagnóstico de objetivos

El Service con puerto de métricas nombrado

Requisito imprescindible: el ServiceMonitor referencia el puerto por su nombre, así que el Service debe tenerlo nombrado.

# k8s/base/api-reservas/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: api-reservas
  namespace: rutas-norte-pro
  labels:
    app: api-reservas
    app.kubernetes.io/part-of: rutas-norte
    entorno: pro
spec:
  selector:
    app: api-reservas          # solo app y entorno en el selector,
    entorno: pro               # como marca nuestra convención
  ports:
    - name: http               # nombre obligatorio para el ServiceMonitor
      port: 80
      targetPort: 8080
    - name: metricas           # el puerto que escaneará Prometheus
      port: 9090
      targetPort: 9090

El ServiceMonitor de api-reservas

# k8s/base/api-reservas/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: api-reservas
  namespace: rutas-norte-pro     # vive junto a la aplicación, no con Prometheus
  labels:
    app: api-reservas
    app.kubernetes.io/part-of: rutas-norte
spec:
  # Qué Services selecciona. OJO: estas etiquetas se comparan con las del
  # objeto Service, NO con las de los pods. Es el error más frecuente.
  selector:
    matchLabels:
      app: api-reservas
      entorno: pro

  # En qué namespaces buscar esos Services.
  namespaceSelector:
    matchNames:
      - rutas-norte-pro

  endpoints:
    - port: metricas             # NOMBRE del puerto del Service, no el número
      path: /metrics
      interval: 30s              # cada cuánto se escanea
      scrapeTimeout: 10s         # siempre menor que el interval

      # Traslada etiquetas del pod a las métricas, para poder filtrar
      # después por entorno o por componente en PromQL.
      relabelings:
        - sourceLabels: [__meta_kubernetes_pod_label_entorno]
          targetLabel: entorno
        - sourceLabels: [__meta_kubernetes_pod_node_name]
          targetLabel: node

      # Descarta métricas que no nos interesan y ocupan espacio.
      # nodejs_gc_duration_seconds tiene mucha cardinalidad y poco valor.
      metricRelabelings:
        - sourceLabels: [__name__]
          regex: 'nodejs_gc_duration_seconds.*'
          action: drop

El ServiceMonitor del exportador de postgres-reservas

Como postgres-reservas es un StatefulSet con Service headless (06-01), necesitamos un Service adicional específico para métricas. Un Service headless (clusterIP: None) funciona igual de bien como origen de un ServiceMonitor, porque el operador usa los Endpoints, no la ClusterIP.

# k8s/base/postgres-reservas/service-metricas.yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres-reservas-metricas
  namespace: rutas-norte-pro
  labels:
    app: postgres-reservas
    entorno: pro
    tipo: metricas
spec:
  clusterIP: None                # headless: no necesitamos balanceo
  selector:
    app: postgres-reservas
    entorno: pro
  ports:
    - name: metricas
      port: 9187
      targetPort: 9187
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: postgres-reservas
  namespace: rutas-norte-pro
  labels:
    app: postgres-reservas
    app.kubernetes.io/part-of: rutas-norte
spec:
  selector:
    matchLabels:
      app: postgres-reservas
      entorno: pro
      tipo: metricas             # discrimina del Service de datos (5432)
  namespaceSelector:
    matchNames:
      - rutas-norte-pro
  endpoints:
    - port: metricas
      interval: 30s
      scrapeTimeout: 10s

Con esto, el sidecar que añadimos en 06-04 empieza a alimentar Prometheus. La promesa se cumple.

La NetworkPolicy que hace falta

Nuestra convención es clara: en rutas-norte-pro hay una deny-all y cada conversación se autoriza una a una. Prometheus vive en monitorizacion y quiere hablar con los puertos de métricas de rutas-norte-pro. Sin política, todos los objetivos aparecerán caídos.

# k8s/entornos/pro/netpol-permitir-prometheus.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permitir-scrape-prometheus
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/part-of: rutas-norte
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitorizacion
          podSelector:
            matchLabels:
              app.kubernetes.io/name: prometheus
      ports:
        - protocol: TCP
          port: 9090      # métricas de api-reservas
        - protocol: TCP
          port: 9187      # exportador de postgres-reservas

Verificar que el objetivo aparece

En la interfaz de Prometheus, Status → Targets. O por línea de comandos:

kubectl -n monitorizacion port-forward svc/monitorizacion-kube-pr-prometheus 9090:9090 &

curl -s localhost:9090/api/v1/targets | jq -r '
  .data.activeTargets[] |
  select(.labels.job | test("api-reservas|postgres")) |
  "\(.labels.job)\t\(.scrapeUrl)\t\(.health)\t\(.lastError)"'
api-reservas	http://10.244.2.17:9090/metrics	up
api-reservas	http://10.244.1.23:9090/metrics	up
postgres-reservas	http://10.244.2.31:9187/metrics	up

Diagnóstico: mi objetivo no aparece

Es la pregunta de esta lección. Checklist en orden de frecuencia:

# Causa Cómo confirmarlo
1 El selector del ServiceMonitor no casa con las etiquetas del Service kubectl get svc api-reservas --show-labels
2 El namespaceSelector no incluye el namespace del Service Revisar matchNames
3 El operador ignora el ServiceMonitor por falta de la etiqueta release Ver serviceMonitorSelector del recurso Prometheus
4 El nombre del puerto del endpoint no existe en el Service kubectl get svc api-reservas -o jsonpath='{.spec.ports}'
5 El Service no tiene Endpoints (readiness fallando, 07-01) kubectl get endpointslices -l kubernetes.io/service-name=api-reservas
6 Una NetworkPolicy bloquea el tráfico desde monitorizacion El objetivo aparece pero con health: down y error de conexión
7 La aplicación no expone /metrics en ese puerto kubectl exec -it <pod> -- curl -s localhost:9090/metrics

El punto 3 merece detalle porque es traicionero. El recurso Prometheus tiene un serviceMonitorSelector. Si el chart lo dejó con release: monitorizacion, solo recogerá ServiceMonitor con esa etiqueta, y el tuyo será ignorado en silencio: no hay evento, no hay error, simplemente no aparece.

# Ver qué está seleccionando el Prometheus desplegado
kubectl -n monitorizacion get prometheus -o yaml | grep -A 8 serviceMonitorSelector

Dos soluciones: poner serviceMonitorSelectorNilUsesHelmValues: false en los valores (lo que hicimos), o añadir la etiqueta release: monitorizacion a todos tus ServiceMonitor.

Comprobar directamente si el objetivo está siendo escaneado:

# ¿Existe la métrica sintética up para nuestro job?
curl -s 'localhost:9090/api/v1/query?query=up{job="api-reservas"}' | jq '.data.result'

Un array vacío significa que el objetivo no está configurado (problemas 1-4). Un resultado con value: ["...", "0"] significa que está configurado pero no responde (problemas 5-7).

  1. PromQL desde cero y con sentido

PromQL asusta al principio y es más sencillo de lo que parece si se aprende por capas.

Capa 1: selectores de series

La consulta más simple es el nombre de una métrica:

rutasnorte_peticiones_total

Devuelve todas las series con ese nombre: una por cada combinación de etiquetas de cada pod. Para filtrar, se usan las llaves:

rutasnorte_peticiones_total{entorno="pro"}

Los cuatro operadores de comparación de etiquetas:

Operador Significado Ejemplo
= Igual exacto {entorno="pro"}
!= Distinto {container!=""}
=~ Coincide con la expresión regular {codigo=~"5.."}
!~ No coincide con la expresión regular {ruta!~"/salud|/preparado"}

Combinables, con AND implícito:

rutasnorte_peticiones_total{entorno="pro", codigo=~"5..", ruta="/api/reservas"}

Lectura: peticiones a /api/reservas en producción que devolvieron un código 5xx.

Capa 2: selectores de rango

Añadiendo [5m] obtienes, en lugar de un valor por serie, todos los valores de los últimos 5 minutos:

rutasnorte_peticiones_total{entorno="pro"}[5m]

Esto se llama vector de rango y no se puede dibujar directamente: es materia prima para las funciones del siguiente nivel.

Capa 3: las funciones que se usan de verdad

rate() — la función más importante de PromQL.

Calcula el incremento por segundo de un contador dentro de una ventana. Es lo que convierte un contador inútil (184203) en un dato con sentido (12,4 peticiones por segundo).

rate(rutasnorte_peticiones_total{entorno="pro"}[5m])

Tres cosas que hay que saber sobre rate():

  • Solo funciona con contadores. Aplicarla a un gauge da resultados sin sentido.
  • Compensa automáticamente los reinicios del contador cuando un pod se recrea.
  • La ventana debe contener al menos 4 muestras. Con interval: 30s, la ventana mínima razonable es [2m]; lo habitual es [5m]. Con [1m] y escaneos de 30 s tendrías dos muestras y resultados erráticos.

increase() — igual que rate() pero da el incremento total de la ventana en vez de por segundo. Es literalmente rate() * segundos_de_la_ventana. Más legible para preguntas humanas:

increase(rutasnorte_reservas_confirmadas_total[1h])
# → "cuántas reservas se han confirmado en la última hora"

sum by y sum without — agregación.

rate() devuelve una serie por pod y por combinación de etiquetas. Casi nunca quieres eso: quieres el total del servicio.

# Peticiones por segundo del servicio entero, agregando todos los pods,
# pero manteniendo el desglose por código de respuesta
sum by (codigo) (rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))

sum by (a, b) conserva solo las etiquetas a y b. sum without (pod, instance) conserva todas menos esas. La segunda forma suele ser más robusta ante cambios.

Otras agregaciones: avg, min, max, count, stddev, topk, bottomk.

topk() — los N mayores. Perfecto para "¿quién se está comiendo el clúster?":

topk(5, sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="rutas-norte-pro"}[5m])))

histogram_quantile() — percentiles a partir de histogramas.

histogram_quantile(
  0.95,
  sum by (le, ruta) (rate(rutasnorte_duracion_peticion_segundos_bucket{entorno="pro"}[5m]))
)

Descomponiendo, de dentro afuera:

  1. rate(..._bucket[5m]) → tasa de incremento de cada cubeta.
  2. sum by (le, ruta) → suma las cubetas de todos los pods, conservando le (el límite de la cubeta) y ruta. La etiqueta le es obligatoria: sin ella, histogram_quantile no puede funcionar. Es el error más común con esta función.
  3. histogram_quantile(0.95, ...) → estima el percentil 95.

El resultado es en segundos: 0.412 significa que el 95 % de las peticiones se resuelven en menos de 412 ms.

Precisión: el resultado es una estimación por interpolación lineal dentro de la cubeta. Si tus cubetas son [0.1, 0.5, 1] y el p95 real está en 0,42 s, la estimación puede desviarse bastante. Por eso elegimos cubetas ajustadas a nuestra latencia real en el apartado 5.

Operadores aritméticos y comparación entre métricas.

# Uso de memoria como fracción del límite configurado
container_memory_working_set_bytes{namespace="rutas-norte-pro", container!=""}
  /
kube_pod_container_resource_limits{namespace="rutas-norte-pro", resource="memory"}

Prometheus empareja series de ambos lados por sus etiquetas comunes. Si las etiquetas no coinciden, el resultado es vacío: es la causa de la mitad de las consultas que "no devuelven nada". on() e ignoring() permiten controlar el emparejamiento.

Las consultas concretas de Rutas Norte

Peticiones por segundo por ruta:

sum by (ruta) (rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))

Tasa de error (proporción de 5xx sobre el total):

sum(rate(rutasnorte_peticiones_total{entorno="pro", codigo=~"5.."}[5m]))
  /
sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))

Devuelve un número entre 0 y 1. Multiplicado por 100 es el porcentaje. Advertencia: si el denominador es 0 (sin tráfico), el resultado es NaN y desaparece del gráfico. Es un comportamiento correcto: sin tráfico no hay tasa de error que medir.

Percentil 95 de latencia por ruta:

histogram_quantile(
  0.95,
  sum by (le, ruta) (rate(rutasnorte_duracion_peticion_segundos_bucket{entorno="pro"}[5m]))
)

Reservas confirmadas por minuto (la métrica del negocio):

sum(rate(rutasnorte_reservas_confirmadas_total{entorno="pro"}[5m])) * 60

Saturación de CPU respecto al límite configurado:

sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="rutas-norte-pro", container!=""}[5m]))
  /
sum by (pod) (kube_pod_container_resource_limits{namespace="rutas-norte-pro", resource="cpu"})

Un valor de 0.95 significa que el pod está usando el 95 % de su límite: candidato inmediato a throttling.

Confirmar el throttling que en 07-02 no podíamos ver:

sum by (pod) (rate(container_cpu_cfs_throttled_periods_total{namespace="rutas-norte-pro"}[5m]))
  /
sum by (pod) (rate(container_cpu_cfs_periods_total{namespace="rutas-norte-pro"}[5m]))

Es la fracción de periodos de planificación en los que el contenedor fue estrangulado. Por encima de 0.25 (25 %) hay un problema real de rendimiento. Esta es exactamente la consulta que habría diagnosticado el problema de api-reservas en 07-02 en diez segundos.

Comparar consumo real con las requests: la recalibración de 07-02, ahora automatizada:

# Cuánto se usa respecto a lo que se reserva. Valores muy por debajo de 1
# indican sobredimensionado; por encima de 1, infradimensionado.
sum by (pod, container) (
  container_memory_working_set_bytes{namespace="rutas-norte-pro", container!=""}
)
/
sum by (pod, container) (
  kube_pod_container_resource_requests{namespace="rutas-norte-pro", resource="memory"}
)

Con Prometheus, esta consulta sobre 30 días de histórico sustituye por completo al script de bash de 07-02, y además permite pedir el percentil real usando quantile_over_time.

Réplicas no disponibles (usa kube-state-metrics):

kube_deployment_status_replicas_unavailable{namespace="rutas-norte-pro"} > 0

Objetivo caído:

up{namespace="rutas-norte-pro"} == 0

Pods reiniciándose en bucle:

increase(kube_pod_container_status_restarts_total{namespace="rutas-norte-pro"}[1h]) > 3

Conexiones de PostgreSQL respecto al máximo (del sidecar de 06-04):

pg_stat_database_numbackends{datname="reservas"} / pg_settings_max_connections

Fallos de la pasarela de pagos externa:

sum(rate(rutasnorte_pasarela_pagos_llamadas_total{resultado=~"error|timeout"}[5m]))
  /
sum(rate(rutasnorte_pasarela_pagos_llamadas_total[5m]))

Esta consulta permite distinguir "nuestra plataforma falla" de "el proveedor externo falla", que es una distinción muy valiosa a las tres de la madrugada.

  1. Los cuatro indicadores dorados aplicados a Rutas Norte

Los cuatro indicadores dorados (golden signals), popularizados por el libro de SRE de Google, son el marco que evita el error de monitorizar todo y no entender nada.

Indicador Qué mide Pregunta que responde
Latencia Cuánto tarda una petición ¿Va lento?
Tráfico Cuánta demanda hay ¿Cuánto trabajo estamos haciendo?
Errores Qué fracción falla ¿Está fallando?
Saturación Cuán lleno está el recurso más escaso ¿Cuánto margen queda?

Un matiz esencial sobre la latencia: hay que medir por separado la de las peticiones que fallan. Un error 500 devuelto en 3 ms baja artificialmente la latencia media y puede hacer que un servicio que falla masivamente parezca rapidísimo.

Aplicación componente a componente

Componente Latencia Tráfico Errores Saturación
tienda-web p95 de nginx peticiones/s ratio 5xx CPU vs límite
api-reservas p95 y p99 por ruta peticiones/s ratio 5xx + fallos de pasarela CPU vs límite, conexiones del pool
postgres-reservas duración de transacción transacciones/s interbloqueos + errores conexiones/max, tamaño en disco
redis-cache latencia de comando operaciones/s ratio de fallos de caché memoria vs maxmemory
worker-notificaciones tiempo de proceso por correo correos/min correos fallidos profundidad de la cola
informes-ocupacion duración de la ejecución ejecuciones/día jobs fallidos (no aplica)

Las consultas concretas para api-reservas, que serán la base del cuadro de mando de 07-04:

# LATENCIA (excluyendo errores, para no falsear el dato)
histogram_quantile(0.95,
  sum by (le) (rate(rutasnorte_duracion_peticion_segundos_bucket{entorno="pro"}[5m])))

# TRÁFICO
sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))

# ERRORES
sum(rate(rutasnorte_peticiones_total{entorno="pro", codigo=~"5.."}[5m]))
  / sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))

# SATURACIÓN: dos ejes, CPU y pool de conexiones
sum by (pod) (rate(container_cpu_cfs_throttled_periods_total{namespace="rutas-norte-pro"}[5m]))
  / sum by (pod) (rate(container_cpu_cfs_periods_total{namespace="rutas-norte-pro"}[5m]))

max(rutasnorte_conexiones_pool_activas{entorno="pro"}) / 20

Esa última consulta cierra un círculo del módulo: es exactamente el fenómeno que en 07-01 dejaba a api-reservas viva pero inservible. Entonces solo teníamos una readiness que apartaba el pod; ahora tenemos además el dato registrado y consultable a posteriori.

Reglas de grabación

Consultas como el p95 son caras: recorren muchas series. Si un cuadro de mando la ejecuta cada 15 segundos con 8 paneles abiertos, Prometheus sufre.

Las reglas de grabación (recording rules) precalculan una consulta periódicamente y guardan el resultado como una métrica nueva:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: rutas-norte-grabacion
  namespace: monitorizacion
  labels:
    app.kubernetes.io/part-of: rutas-norte
spec:
  groups:
    - name: rutasnorte.indicadores-dorados
      interval: 30s
      rules:
        # Convención de nombres: nivel:metrica:operacion
        - record: apireservas:latencia_p95:5m
          expr: |
            histogram_quantile(0.95,
              sum by (le, ruta, entorno) (
                rate(rutasnorte_duracion_peticion_segundos_bucket[5m])))

        - record: apireservas:peticiones:rate5m
          expr: sum by (entorno, ruta) (rate(rutasnorte_peticiones_total[5m]))

        - record: apireservas:ratio_errores:5m
          expr: |
            sum by (entorno) (rate(rutasnorte_peticiones_total{codigo=~"5.."}[5m]))
              /
            sum by (entorno) (rate(rutasnorte_peticiones_total[5m]))

Ahora los cuadros de mando y las alertas consultan apireservas:latencia_p95:5m, que es una única serie ya calculada. En 07-04 las usaremos intensivamente.

  1. Almacenamiento, retención y los límites de Prometheus

Cómo guarda los datos

Prometheus escribe en su propia base de datos de series temporales (TSDB):

  1. Las muestras nuevas van a un bloque en memoria y a un registro de escritura anticipada (WAL) en disco, que protege ante caídas.
  2. Cada 2 horas, ese bloque se persiste como un directorio inmutable en disco.
  3. Un proceso de compactación va fusionando bloques pequeños en bloques mayores.
  4. Los bloques que superan la retención se borran enteros.

Consecuencia importante: la retención no borra muestras sueltas, borra bloques completos. Con retention: 30d puedes tener puntualmente algo más de 30 días.

Estimar el espacio

La regla práctica aceptada: entre 1 y 2 bytes por muestra tras la compresión.

Bytes ≈ retención_segundos × series_activas × bytes_por_muestra / intervalo_scrape

Rutas Norte, estimación:
  30 días = 2 592 000 s
  ~120 000 series activas (los tres entornos + el sistema)
  1,7 bytes por muestra
  intervalo 30 s

  2 592 000 × 120 000 × 1,7 / 30 ≈ 17,6 GB

Con 50 GiB de PVC vamos sobrados, incluso con margen para la compactación (que necesita espacio temporal). Por eso pusimos también retentionSize: 45GiB: es el freno de emergencia si la cardinalidad crece más de lo previsto.

Ver la cardinalidad real:

# Número total de series activas
prometheus_tsdb_head_series

# Las 10 métricas con más series: la lista de sospechosos de cardinalidad
topk(10, count by (__name__)({__name__=~".+"}))

Esta segunda consulta es la primera que hay que ejecutar cuando Prometheus empieza a consumir demasiada memoria.

Por qué Prometheus no es una base de datos eterna

Tres limitaciones de diseño, deliberadas:

  1. Almacenamiento local, no distribuido. Los datos viven en el disco de un pod. Sin replicación entre instancias.
  2. No escala horizontalmente por sí mismo. Dos Prometheus no comparten datos: cada uno tiene los suyos.
  3. La retención larga es cara. Guardar dos años en local significa cientos de GB en un disco rápido y un consumo de memoria considerable al consultar.

Para retención larga y visión multiclúster existen dos proyectos que extienden Prometheus:

Proyecto Enfoque
Thanos Un sidecar sube los bloques a almacenamiento de objetos (S3); un componente query consulta de forma transparente lo local y lo remoto
Mimir (Grafana) Recibe los datos por remote_write en un sistema distribuido y multiinquilino

Ambos permiten años de retención barata y consultar varios clústeres a la vez. Para Rutas Norte con un solo clúster, 30 días son suficientes hoy; cuando el módulo 11-05 aborde la gestión multiclúster, Thanos será la evolución natural.

Configurar remote_write desde el operador es sencillo:

prometheus:
  prometheusSpec:
    remoteWrite:
      - url: https://mimir.rutasnorte.example/api/v1/push
        writeRelabelConfigs:
          # Enviar solo lo que de verdad queremos conservar años:
          # las métricas de negocio, no las de infraestructura.
          - sourceLabels: [__name__]
            regex: 'rutasnorte_.*'
            action: keep

Errores Comunes y Consejos

1. Explosión de cardinalidad. El error más grave y el más caro. Etiquetar con identificadores de reserva, DNI, IP de cliente o URLs sin normalizar multiplica las series por miles y tumba Prometheus por falta de memoria. Antes de añadir una etiqueta, pregúntate cuántos valores distintos puede tener. Si la respuesta es "no lo sé", no la añadas.

2. Consultar un contador sin rate(). rutasnorte_peticiones_total a secas devuelve un valor acumulado desde que arrancó el pod. Es un gráfico que solo sube y no significa nada. Siempre rate() o increase().

3. Ventana de rate() demasiado corta. Con interval: 30s, un rate(...[1m]) tiene dos muestras y produce ruido o huecos. Regla: la ventana debe ser al menos 4 veces el intervalo de escaneo.

4. Olvidar le en sum by antes de histogram_quantile. Si agregas sin conservar la etiqueta le, la función no puede reconstruir el histograma y devuelve NaN o nada. Es el fallo número uno con percentiles.

5. El ServiceMonitor selecciona etiquetas de pod en vez de Service. El selector de un ServiceMonitor se compara con las etiquetas del objeto Service. Como en Rutas Norte los Service llevan las mismas etiquetas que los pods, es fácil no darse cuenta hasta que un Service tiene etiquetas distintas.

6. La etiqueta release del operador. Si el recurso Prometheus tiene serviceMonitorSelector: {matchLabels: {release: monitorizacion}}, tus ServiceMonitor sin esa etiqueta se ignoran sin ningún mensaje de error. Comprobarlo es lo primero cuando un objetivo no aparece.

7. Prometheus sin almacenamiento persistente. Sin storageSpec, el chart usa un emptyDir y cada reinicio del pod borra todo el histórico. Es exactamente el problema que veníamos a resolver.

8. Sin retentionSize. Solo con retention: 90d, si la cardinalidad crece, el disco se llena, Prometheus entra en CrashLoopBackOff y te quedas sin monitorización justo cuando más falta hace. Pon siempre un límite de tamaño, y que sea un 80-85 % del PVC.

9. Alertar sobre métricas de un summary. No son agregables entre pods. Si tu alerta usa el p99 de un summary con seis réplicas, el número no significa nada útil.

10. Monitorizar el nodo con métricas de contenedor. cAdvisor no ve el sistema de ficheros del nodo ni su carga media. Para eso está node-exporter. Confundirlos lleva a alertas que nunca disparan.

11. Olvidar las NetworkPolicies. En rutas-norte-pro hay una deny-all. Si Prometheus no puede alcanzar los puertos de métricas, todos los objetivos aparecen caídos y el error (context deadline exceeded) no es nada evidente.

12. Instrumentar demasiado tarde. La instrumentación de negocio se añade en el código, así que requiere un ciclo de desarrollo. Cuanto antes se incorpore como parte del "listo para producción", mejor. Improvisarla durante un incidente es imposible.

Ejercicios

Ejercicio 1 — Diagnosticar un ServiceMonitor que no funciona

El equipo ha desplegado redis-cache con un exportador y ha creado estos manifiestos. En la página de targets de Prometheus no aparece nada.

apiVersion: v1
kind: Service
metadata:
  name: redis-cache-metricas
  namespace: rutas-norte-pro
  labels:
    app.kubernetes.io/name: redis-cache
    app.kubernetes.io/part-of: rutas-norte
spec:
  selector:
    app: redis-cache
    entorno: pro
  ports:
    - port: 9121
      targetPort: 9121
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: redis-cache
  namespace: monitorizacion
spec:
  selector:
    matchLabels:
      app: redis-cache
      entorno: pro
  endpoints:
    - port: metrics
      interval: 30s
  1. Identifica tres errores distintos.
  2. Escribe los manifiestos corregidos.
  3. Escribe el comando PromQL o curl que confirma que el problema está resuelto.

Ejercicio 2 — Escribir las consultas de un incidente

El sábado del puente, entre las 12:00 y las 13:30, los clientes se quejaron de que la web iba lentísima y algunas compras fallaban. Ya pasó, nadie tomó notas, y el director pide explicaciones el lunes.

Escribe las consultas PromQL que responden a cada pregunta. Indica de qué fuente (cAdvisor, kube-state-metrics, node-exporter, exportador o instrumentación propia) sale cada métrica.

  1. ¿Cuántas peticiones por segundo atendía api-reservas en ese intervalo, comparado con el sábado anterior?
  2. ¿Cuál fue el percentil 99 de latencia de /api/reservas durante el incidente?
  3. ¿Estuvo api-reservas sufriendo throttling de CPU?
  4. ¿Se quedó postgres-reservas sin conexiones disponibles?
  5. ¿Falló la pasarela de pagos externa, o el fallo era nuestro?
  6. ¿Cuántas reservas se dejaron de confirmar respecto a lo esperado?

Ejercicio 3 — Instrumentar worker-notificaciones

worker-notificaciones consume de una cola de Redis y envía correos de confirmación. Actualmente no expone ninguna métrica. Diseña su instrumentación:

  1. Enumera las métricas que debe exponer, con su nombre completo, su tipo (counter, gauge, histogram) y sus etiquetas, justificando la cardinalidad de cada etiqueta.
  2. Escribe el ServiceMonitor correspondiente (el worker no tiene Service porque no recibe tráfico: resuelve ese problema).
  3. Escribe las consultas PromQL de los cuatro indicadores dorados para este componente.

Soluciones

Solución 1

1. Los tres errores.

Error A — El selector del ServiceMonitor no casa con las etiquetas del Service. El ServiceMonitor busca Services con app: redis-cache y entorno: pro, pero el Service tiene app.kubernetes.io/name: redis-cache y app.kubernetes.io/part-of: rutas-norte. No coincide ninguna. El spec.selector del Service (que sí usa app/entorno) selecciona pods, no es lo que mira el ServiceMonitor.

Error B — El puerto del Service no tiene nombre, y el endpoint referencia metrics. El ServiceMonitor busca un puerto llamado metrics en el Service; el Service define el puerto 9121 sin campo name. El operador no encuentra el puerto y descarta el endpoint.

Error C — El ServiceMonitor está en monitorizacion sin namespaceSelector. Por defecto, un ServiceMonitor busca Services en su propio namespace. Estando en monitorizacion y el Service en rutas-norte-pro, no se encontrarán nunca. Dos soluciones válidas: mover el ServiceMonitor al namespace de la aplicación (preferible, mantiene la monitorización junto al código) o añadir namespaceSelector.

2. Manifiestos corregidos.

apiVersion: v1
kind: Service
metadata:
  name: redis-cache-metricas
  namespace: rutas-norte-pro
  labels:
    app: redis-cache          # etiqueta que buscará el ServiceMonitor
    entorno: pro
    app.kubernetes.io/part-of: rutas-norte
    tipo: metricas            # distingue del Service de datos (6379)
spec:
  clusterIP: None
  selector:                   # esto selecciona PODS: solo app y entorno
    app: redis-cache
    entorno: pro
  ports:
    - name: metricas          # NOMBRE obligatorio
      port: 9121
      targetPort: 9121
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: redis-cache
  namespace: rutas-norte-pro  # junto a la aplicación
  labels:
    app: redis-cache
    app.kubernetes.io/part-of: rutas-norte
spec:
  selector:
    matchLabels:              # se comparan con las etiquetas del SERVICE
      app: redis-cache
      entorno: pro
      tipo: metricas
  namespaceSelector:
    matchNames:
      - rutas-norte-pro
  endpoints:
    - port: metricas          # coincide con el name del puerto
      path: /metrics
      interval: 30s
      scrapeTimeout: 10s

Y la NetworkPolicy, porque en rutas-norte-pro hay deny-all:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permitir-scrape-redis-cache
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      app: redis-cache
      entorno: pro
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitorizacion
      ports:
        - protocol: TCP
          port: 9121

3. Verificación.

kubectl -n monitorizacion port-forward svc/monitorizacion-kube-pr-prometheus 9090:9090 &

# ¿Existe el objetivo y responde?
curl -s 'localhost:9090/api/v1/query?query=up{job="redis-cache-metricas"}' | jq '.data.result'
[
  {
    "metric": {"__name__": "up", "job": "redis-cache-metricas",
               "namespace": "rutas-norte-pro", "pod": "redis-cache-0"},
    "value": [1754472000, "1"]
  }
]

"1" es la confirmación. Si diera "0", el objetivo está configurado pero no responde: revisa la NetworkPolicy y que el exportador escuche en el 9121.

Comprobación adicional de que el operador lo ha recogido:

kubectl -n monitorizacion get prometheus -o jsonpath='{.items[0].spec.serviceMonitorNamespaceSelector}'

Solución 2

Nota general: todas las consultas usan el modificador offset o un rango temporal explícito, ya que el incidente pasó. Aquí está la ventaja decisiva sobre kubectl top de 07-02: estos datos existen.

1. Tráfico durante el incidente, comparado con el sábado anterior.

Fuente: instrumentación propia de api-reservas.

# Tráfico actual (visualizado en el rango del incidente en Grafana)
sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))

# Comparación con hace exactamente una semana, superpuesta
sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m] offset 7d))

# Factor de multiplicación
sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))
  /
sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m] offset 7d))

Un resultado de 5.8 significa que el sábado del puente hubo casi seis veces más tráfico que el sábado normal: coherente con lo que sabemos de los picos de puentes.

2. Percentil 99 de latencia de /api/reservas.

Fuente: instrumentación propia (histograma).

histogram_quantile(0.99,
  sum by (le) (
    rate(rutasnorte_duracion_peticion_segundos_bucket{
      entorno="pro", ruta="/api/reservas"}[5m])))

Se compara con el p95 y con la media (_sum / _count) para ver si el problema afectaba a todas las peticiones o solo a la cola:

rate(rutasnorte_duracion_peticion_segundos_sum{entorno="pro", ruta="/api/reservas"}[5m])
  /
rate(rutasnorte_duracion_peticion_segundos_count{entorno="pro", ruta="/api/reservas"}[5m])

Si la media está bien y el p99 disparado, el problema afecta a un subconjunto (por ejemplo, las peticiones que tocan la pasarela de pagos). Si ambos suben, es sistémico.

3. Throttling de CPU.

Fuente: cAdvisor / kubelet.

sum by (pod) (
  rate(container_cpu_cfs_throttled_periods_total{
    namespace="rutas-norte-pro", pod=~"api-reservas-.*"}[5m]))
/
sum by (pod) (
  rate(container_cpu_cfs_periods_total{
    namespace="rutas-norte-pro", pod=~"api-reservas-.*"}[5m]))

Un valor sostenido por encima de 0.30 durante el incidente confirma que el límite de CPU estaba frenando la aplicación. Complementaria, para ver si estaba pegada al techo:

sum by (pod) (rate(container_cpu_usage_seconds_total{
  namespace="rutas-norte-pro", pod=~"api-reservas-.*", container="api"}[5m]))
/
sum by (pod) (kube_pod_container_resource_limits{
  namespace="rutas-norte-pro", pod=~"api-reservas-.*", resource="cpu"})

(La segunda parte viene de kube-state-metrics: es el ejemplo perfecto de por qué hacen falta las dos fuentes.)

4. Conexiones de PostgreSQL.

Fuente: exportador sidecar de 06-04, más las métricas propias del pool.

# Conexiones en uso frente al máximo configurado
pg_stat_database_numbackends{datname="reservas"} / pg_settings_max_connections

# El pool de la aplicación, que es el que se agotó en el caso de 07-01
max(rutasnorte_conexiones_pool_activas{entorno="pro"})

# Interbloqueos: síntoma de contención grave
increase(pg_stat_database_deadlocks{datname="reservas"}[5m])

Un numbackends / max_connections cercano a 1 confirma que la base de datos estaba saturada de conexiones.

5. ¿Falló la pasarela externa o fallamos nosotros?

Fuente: instrumentación propia.

# Tasa de fallo de la pasarela
sum(rate(rutasnorte_pasarela_pagos_llamadas_total{resultado=~"error|timeout"}[5m]))
  /
sum(rate(rutasnorte_pasarela_pagos_llamadas_total[5m]))

# Desglose: ¿error o timeout? El timeout apunta a lentitud del proveedor
sum by (resultado) (rate(rutasnorte_pasarela_pagos_llamadas_total[5m]))

Interpretación: si la tasa de fallo de la pasarela sube al 40 % mientras nuestro ratio_errores general también sube, el fallo es del proveedor y hay que reclamarle. Si la pasarela está al 0,1 % de fallos y nosotros devolvemos 5xx, el problema es nuestro. Esta distinción, imposible sin instrumentación, es lo que se lleva a la reunión del lunes.

6. Reservas dejadas de confirmar.

Fuente: instrumentación propia (métrica de negocio).

# Reservas confirmadas durante la hora y media del incidente
increase(rutasnorte_reservas_confirmadas_total{entorno="pro"}[90m])

# Lo mismo el sábado anterior a la misma hora
increase(rutasnorte_reservas_confirmadas_total{entorno="pro"}[90m] offset 7d)

# Pérdida estimada: reservas por minuto ahora vs. una semana antes
sum(rate(rutasnorte_reservas_confirmadas_total{entorno="pro"}[5m])) * 60

Y un cruce especialmente revelador: si el tráfico se multiplicó por 5,8 pero las reservas confirmadas solo por 1,3, la diferencia entre ambos factores es una estimación directa del negocio perdido. Ese número, en euros, es el que justifica el presupuesto de infraestructura.

Solución 3

1. Métricas del componente.

Nombre Tipo Etiquetas Cardinalidad Justificación
rutasnorte_correos_enviados_total counter tipo, resultado 3 × 3 = 9 Volumen y fallos. tipo: confirmacion, cancelacion, recordatorio. resultado: ok, error, rechazado
rutasnorte_correo_duracion_segundos histogram tipo 3 × 9 cubetas = 27 Latencia del envío, incluida la llamada al servidor SMTP
rutasnorte_cola_pendientes gauge ninguna 1 La métrica de saturación clave: si la cola crece sin parar, el worker no da abasto
rutasnorte_cola_espera_segundos gauge ninguna 1 Antigüedad del mensaje más viejo de la cola: mide el retraso percibido por el cliente
rutasnorte_reintentos_total counter motivo 4 Reintentos por tipo de fallo: smtp_timeout, smtp_rechazo, red, desconocido
rutasnorte_worker_ciclo_activo gauge ninguna 1 1 si el bucle de consumo está vivo. Alimenta también la liveness de 07-01

Cardinalidad total: unas 43 series por pod. Perfectamente asumible.

Etiquetas descartadas deliberadamente y por qué:

  • destinatario (correo del cliente): cardinalidad ilimitada y, sobre todo, dato personal en el sistema de monitorización. Prohibido. Volveremos sobre esto en 07-05.
  • id_reserva: cardinalidad ilimitada. Ese dato pertenece a los logs, no a las métricas. Las métricas agregan; los logs detallan.
  • mensaje_error completo: cardinalidad impredecible. Se sustituye por motivo, un conjunto cerrado de categorías.

2. ServiceMonitor para un componente sin Service.

worker-notificaciones no recibe tráfico, así que no tiene Service. Dos opciones válidas:

Opción A (recomendada): PodMonitor. Es exactamente el recurso diseñado para este caso.

apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: worker-notificaciones
  namespace: rutas-norte-pro
  labels:
    app: worker-notificaciones
    app.kubernetes.io/part-of: rutas-norte
spec:
  selector:
    matchLabels:            # aquí SÍ se comparan con etiquetas de POD
      app: worker-notificaciones
      entorno: pro
  namespaceSelector:
    matchNames:
      - rutas-norte-pro
  podMetricsEndpoints:
    - port: metricas        # nombre del puerto en el spec del CONTENEDOR
      path: /metrics
      interval: 30s
      scrapeTimeout: 10s

Con el puerto declarado en el Deployment:

containers:
  - name: worker
    image: registry.rutasnorte.example/worker-notificaciones:3.4.2
    ports:
      - name: metricas
        containerPort: 9100

Opción B: crear un Service headless solo para métricas y usar un ServiceMonitor normal. Funciona, pero crea un objeto que no aporta nada más. El PodMonitor es más limpio.

Y la NetworkPolicy correspondiente:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permitir-scrape-worker-notificaciones
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      app: worker-notificaciones
      entorno: pro
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitorizacion
      ports:
        - protocol: TCP
          port: 9100

3. Los cuatro indicadores dorados.

# ---- LATENCIA: p95 del tiempo de envío de un correo ----
histogram_quantile(0.95,
  sum by (le, tipo) (
    rate(rutasnorte_correo_duracion_segundos_bucket{entorno="pro"}[5m])))

# ---- TRÁFICO: correos enviados por minuto ----
sum by (tipo) (rate(rutasnorte_correos_enviados_total{entorno="pro"}[5m])) * 60

# ---- ERRORES: fracción de envíos fallidos ----
sum(rate(rutasnorte_correos_enviados_total{entorno="pro", resultado!="ok"}[5m]))
  /
sum(rate(rutasnorte_correos_enviados_total{entorno="pro"}[5m]))

# ---- SATURACIÓN: la cola, en tres ángulos complementarios ----

# a) Profundidad actual de la cola
max(rutasnorte_cola_pendientes{entorno="pro"})

# b) ¿Está creciendo? Derivada de la profundidad en la última media hora.
#    Positivo sostenido = el worker no da abasto y hay que escalar.
deriv(rutasnorte_cola_pendientes{entorno="pro"}[30m])

# c) El indicador que más importa al cliente: cuánto espera el correo más
#    antiguo. Un cliente que pagó hace 10 minutos y no ha recibido nada
#    empieza a dudar de si su compra se ha hecho.
max(rutasnorte_cola_espera_segundos{entorno="pro"})

Consulta adicional de cobertura, muy valiosa para el negocio: relaciona dos componentes distintos para detectar correos perdidos.

# Reservas confirmadas menos correos de confirmación enviados en la última
# hora. Debería ser prácticamente cero. Un valor positivo grande significa
# que hay clientes que han pagado y no han recibido nada.
increase(rutasnorte_reservas_confirmadas_total{entorno="pro"}[1h])
  -
increase(rutasnorte_correos_enviados_total{entorno="pro", tipo="confirmacion", resultado="ok"}[1h])

Este tipo de consulta —que cruza métricas de dos componentes para verificar una invariante del negocio— es de las más útiles que se pueden escribir, y en 07-04 la convertiremos en alerta.

Conclusión

Rutas Norte ha pasado de no recordar nada a tener memoria, lenguaje y capacidad de pregunta. En esta lección hemos:

  • Entendido el modelo de extracción y por qué es superior al de empuje en un entorno dinámico como Kubernetes.
  • Aprendido el modelo de datos: series identificadas por nombre y etiquetas, la amenaza permanente de la cardinalidad, y los cuatro tipos de métrica, con el histogram como el único que permite calcular percentiles agregados entre réplicas.
  • Aclarado la confusión clásica sobre de dónde salen las métricas: cAdvisor mide el consumo real de los contenedores, node-exporter el sistema operativo del nodo, kube-state-metrics el estado de los objetos de la API, los exportadores traducen software de terceros —incluido el sidecar de postgres-reservas que añadimos en 06-04, ahora por fin conectado— y la instrumentación propia es lo único que sabe de negocio.
  • Instrumentado api-reservas con peticiones por ruta y código, latencia en histograma, reservas confirmadas y el estado del pool de conexiones, cuidando la cardinalidad en cada decisión.
  • Desplegado el kube-prometheus-stack y trabajado con el Prometheus Operator que anunciamos en 06-07, creando ServiceMonitor que viven junto a cada aplicación y sabiendo diagnosticar por qué un objetivo no aparece.
  • Escrito PromQL de verdad: rate, increase, sum by, topk, histogram_quantile, y las consultas concretas de los cuatro indicadores dorados de cada componente. Entre ellas, la que confirma el throttling que en 07-02 solo podíamos sospechar.
  • Dimensionado el almacenamiento y la retención, y entendido por qué Prometheus no es un archivo eterno y cuándo tocará mirar hacia Thanos o Mimir.

Pero seguimos teniendo un problema práctico: todo esto vive en una interfaz web austera donde hay que escribir consultas a mano, y nadie está mirándola a las tres de la madrugada. Tener el dato no sirve de nada si nadie lo ve ni nadie recibe el aviso.

En 07-04 daremos ese salto: construiremos con Grafana el cuadro de mando de Rutas Norte, panel a panel, con variables que sirvan para los tres entornos; escribiremos el catálogo de alertas de la plataforma con PrometheusRule —incluida la que predice cuándo se llenará el disco de postgres-reservas antes de que ocurra—; y configuraremos Alertmanager para que la alerta correcta llegue a la persona correcta, agrupada, sin ruido y con un enlace al procedimiento que hay que seguir.

Curso de Kubernetes

Módulo 1: Introducción a Kubernetes

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados