En 07-01 abrimos CicloUrbana por dentro con Actuator: sondas de salud, información de versión, niveles de log en caliente, un endpoint propio y una cadena de seguridad que exige ADMIN en el puerto 8081. Cerramos aquella lección dejando deliberadamente un endpoint sin abrir, /actuator/metrics, con una promesa: la instrumentación quedaba montada y aquí se leería.

Ha llegado el momento, y con una necesidad concreta detrás. En 09-01 ajustamos el rendimiento a base de pruebas de carga y EXPLAIN ANALYZE, es decir, mirando una vez y en un entorno controlado. En 09-02 montamos cachés cuya utilidad depende por completo de una tasa de aciertos que todavía nadie observa. Las dos lecciones dejaron la misma deuda: CicloUrbana no se mide sola, continuamente y en producción.

Esta lección la salda. No repetiremos qué es Actuator ni cómo se expone: usaremos la infraestructura ya montada para instrumentar de verdad. Veremos Micrometer como fachada de métricas, los cinco tipos de medidor y cuándo usar cada uno, el catálogo de métricas que existen sin escribir una línea, la regla de oro de la cardinalidad —la que separa un sistema de monitorización sano de uno que se cae por su propio peso—, las métricas de negocio de la red de Ribalta, la diferencia crucial entre percentiles calculados en el cliente e histogramas agregables, y el modelo unificado de observación de Spring Boot 3 que produce a la vez métrica y traza.

Contenido

  1. Los tres pilares de la observabilidad
  2. Micrometer: el SLF4J de las métricas
  3. Los tipos de medidor
  4. Las métricas que ya existen sin escribir código
  5. Consultar métricas desde /actuator/metrics
  6. Etiquetas y la regla de oro de la cardinalidad
  7. Instrumentar CicloUrbana: MetricasCicloUrbana
  8. El Gauge, la referencia débil y su error clásico
  9. @Timed y @Counted
  10. Percentiles frente a histogramas
  11. MeterFilter y MeterRegistryCustomizer
  12. El pool, la base de datos y la caché: qué números vigilar
  13. La Observation API: una instrumentación, dos señales
  14. De las métricas a los SLO: RED y USE
  15. Probar que una métrica se registra
  16. Errores Comunes y Consejos
  17. Ejercicios

  1. Los tres pilares de la observabilidad

Monitorizar es vigilar un conjunto conocido de indicadores; observar es poder responder preguntas que no habías previsto. La diferencia se nota a las tres de la mañana, cuando el problema nunca es el que esperabas. Para lograrlo hacen falta tres señales, y ninguna sustituye a las otras:

Métricas Logs Trazas
Qué son Números agregados en el tiempo Eventos con texto y contexto El recorrido de una petición
Qué responden ¿Está pasando algo? ¿Cuánto? ¿Desde cuándo? ¿Qué pasó exactamente? ¿Dónde se fue el tiempo?
Coste Muy bajo y constante con el tráfico Proporcional al tráfico; caro Alto: se muestrea
Cardinalidad Debe ser baja Alta: cada línea es única Alta por definición
Retención típica Meses o años Días o semanas Días
Sirven para alertar Sí, es su función Mal (excepto tasas de error) No
En este módulo Esta lección y 09-04 09-05 09-06

El flujo de trabajo real de una incidencia en Ribalta usa las tres en este orden: una alerta basada en métricas avisa de que el p99 de los alquileres se ha disparado; una traza muestra que el 80 % del tiempo está en el span de la pasarela de pagos; los logs de esa traza concreta, filtrados por su identificador, dicen qué error devolvió. Métricas para detectar, trazas para localizar, logs para entender.

Esta lección construye el primer pilar dentro de la aplicación. En 09-04 esas métricas saldrán del proceso hacia Prometheus y Grafana, porque —y conviene decirlo ya— todo lo que instrumentemos aquí vive en memoria y muere con el proceso.

  1. Micrometer: el SLF4J de las métricas

La comparación es literal y explica el diseño entero. Igual que SLF4J te deja escribir log.info(...) sin saber si detrás hay Logback o Log4j2, Micrometer te deja registrar un contador sin saber si detrás hay Prometheus, Datadog, New Relic o CloudWatch. Tu código depende de una fachada; el sistema de destino es una dependencia intercambiable.

flowchart LR
    A[Codigo de CicloUrbana<br/>Counter, Timer, Gauge] --> B[MeterRegistry<br/>Micrometer]
    C[Spring Boot<br/>metricas automaticas] --> B
    B --> D[Actuator<br/>/actuator/metrics]
    B --> E[PrometheusMeterRegistry<br/>/actuator/prometheus]
    E --> F[(Prometheus)]
    F --> G[Grafana · 09-04]

MeterRegistry es la pieza central: la fábrica y el registro de todos los medidores. Spring Boot crea uno automáticamente al detectar Actuator, y se inyecta como cualquier otro bean. Sin ninguna implementación concreta en el classpath se usa un SimpleMeterRegistry en memoria, que es justo lo que hace útil /actuator/metrics en desarrollo y en pruebas (apartado 15).

Un detalle importante de vocabulario: en Micrometer un medidor (meter) es la abstracción genérica, y Counter, Gauge, Timer, DistributionSummary y LongTaskTimer son sus tipos. Cada medidor se identifica por su nombre más su conjunto de etiquetas, y cada combinación distinta es una serie temporal independiente. Esa frase es la clave del apartado 6.

  1. Los tipos de medidor

Tipo Qué mide Puede bajar Ejemplo en CicloUrbana
Counter Un valor acumulado que solo sube No Alquileres iniciados, errores de cobro
Gauge Un valor instantáneo que sube y baja Sí Bicicletas disponibles, tamaño de una cola
Timer Duración y recuento de eventos cortos — Tiempo del cálculo de tarifa, latencia HTTP
DistributionSummary Distribución de valores sin unidad de tiempo — Duración en minutos de un alquiler, importe
LongTaskTimer Duración de tareas largas en curso — La importación nocturna del ayuntamiento
FunctionCounter Contador leído de un objeto externo No Un total que ya lleva otra biblioteca

Cuatro criterios de elección que resuelven casi todas las dudas:

Counter frente a Gauge. Si la pregunta es «¿cuántas veces ha ocurrido?», es un contador; si es «¿cuánto hay ahora mismo?», es un indicador. Un contador nunca se decrementa: para «alquileres en curso» no se usa un contador que sube al iniciar y baja al finalizar —eso es un Gauge—, porque el valor de un contador es su ritmo de crecimiento, que es lo que rate() sabrá explotar en 09-04.

Timer frente a DistributionSummary. El Timer mide tiempo y lleva unidades; el DistributionSummary mide cualquier magnitud. La duración de un trayecto de bicicleta en minutos parece tiempo, pero no es la duración de una operación del programa: es un dato de negocio, y va en un DistributionSummary.

Timer frente a LongTaskTimer. Un Timer solo registra la operación cuando termina: si una importación tarda cuarenta minutos, durante esos cuarenta minutos el Timer no dice nada. El LongTaskTimer informa de las tareas activas y de cuánto llevan, que es justo lo que se quiere vigilar de una tarea @Scheduled de 07-03.

Todo Timer es también un contador. Publica _count, _sum y _max, así que temporizar una operación da gratis el número de veces que ocurrió: no hace falta un Counter aparte.

  1. Las métricas que ya existen sin escribir código

Antes de instrumentar nada, conviene saber qué hay. Con Actuator y las autoconfiguraciones activas, CicloUrbana ya publica:

Métrica Tipo Qué mide Por qué importa
http.server.requests Timer Latencia y recuento por uri, method, status, outcome, exception La métrica más valiosa del sistema: RED completo del apartado 14
http.client.requests Timer Lo mismo, para el RestClient hacia la pasarela (07-06) Separa «somos lentos» de «el remoto es lento»
jvm.memory.used / .max Gauge Memoria por área (heap, nonheap) y por pool La memoria que sube y no baja de 09-01
jvm.gc.pause Timer Duración de las pausas de GC, por causa Explica picos de p99
jvm.threads.live / .states Gauge Hilos vivos y su estado Hilos bloqueados esperando conexión
hikaricp.connections.* Gauge/Timer active, idle, pending, acquire, usage, timeout La señal del apartado 11 de 09-01
system.cpu.usage / process.cpu.usage Gauge CPU de la máquina y del proceso Saturación
tomcat.threads.busy / .config.max Gauge Hilos del contenedor web ocupados Cuánto margen queda
spring.data.repository.invocations Timer Llamadas a cada método de repositorio Qué consulta domina el tiempo
cache.gets / cache.puts / cache.evictions / cache.size Counter/Gauge Aciertos y fallos por caché (09-02) Si la caché sirve para algo
resilience4j.circuitbreaker.* Gauge/Timer Estado y llamadas del cortacircuitos (07-06) Circuito abierto = degradación
application.ready.time / started.time Gauge Cuánto tardó en arrancar Regresiones de arranque en Kubernetes
logback.events Counter Eventos de log por nivel Un pico de level="error" es una alerta barata

Dos observaciones. La primera: http.server.requests sola responde a casi todo, porque tiene recuento (tráfico), etiqueta status (errores) y distribución (latencia), que son las tres señales del método RED. La segunda: spring.data.repository.invocations requiere activarse (management.metrics.data.repository.autotime.enabled: true) y es el complemento natural del contador de consultas de 09-01, ahora en producción y de forma continua.

  1. Consultar métricas desde /actuator/metrics

Recordando la configuración de 07-01 —puerto de gestión 8081 y ADMIN por httpBasic—, basta con añadir el endpoint a la lista expuesta:

management:
  endpoints.web.exposure.include: health,info,loggers,metrics,caches,prometheus
  metrics.tags:                      # etiquetas comunes a TODAS las metricas
    application: ciclourbana
    entorno: ${SPRING_PROFILES_ACTIVE:local}
    instancia: ${HOSTNAME:local}

Esas tres etiquetas comunes son la diferencia entre «la latencia está alta» y «la latencia está alta en la instancia 3 de producción». Se aplican a todos los medidores, incluidos los automáticos.

curl -su admin:*** http://localhost:8081/actuator/metrics            # el catalogo
curl -su admin:*** http://localhost:8081/actuator/metrics/http.server.requests
{
  "name": "http.server.requests",
  "measurements": [
    { "statistic": "COUNT", "value": 128431 },
    { "statistic": "TOTAL_TIME", "value": 4021.83 },
    { "statistic": "MAX", "value": 2.41 }
  ],
  "availableTags": [
    { "tag": "uri", "values": ["/api/v1/estaciones", "/api/v1/alquileres", "/api/v1/alquileres/{id}/finalizar"] },
    { "tag": "status", "values": ["200", "201", "404", "409", "500"] },
    { "tag": "outcome", "values": ["SUCCESS", "CLIENT_ERROR", "SERVER_ERROR"] }
  ]
}

Lo importante es cómo se lee: TOTAL_TIME / COUNT es la latencia media —4021,83 s entre 128.431 peticiones = 31 ms—, y ya sabemos de 09-01 lo poco que vale una media. Para acercarse a lo que importa se filtra con ?tag=, encadenando varios:

# Latencia y recuento solo del endpoint de alquileres
.../metrics/http.server.requests?tag=uri:/api/v1/alquileres

# Solo los errores de servidor de ese endpoint
.../metrics/http.server.requests?tag=uri:/api/v1/alquileres&tag=outcome:SERVER_ERROR

Fíjate en la etiqueta uri: dice /api/v1/alquileres/{id}/finalizar, con la plantilla y no con el identificador real. Esa decisión de Spring es la que hace utilizable la métrica, y es exactamente el tema del apartado siguiente.

Y una limitación que conviene interiorizar: este endpoint da el valor acumulado ahora. No hay historia, no hay gráfica, no hay «hace dos horas». Sirve para comprobar que una métrica existe y para una consulta puntual de urgencia; para todo lo demás hace falta 09-04.

  1. Etiquetas y la regla de oro de la cardinalidad

Una etiqueta (tag, o label en Prometheus) es una dimensión: permite desglosar alquileres.iniciados por tipo de tarifa o http.server.requests por endpoint. Sin etiquetas, una métrica solo dice «pasan cosas»; con ellas, dice qué cosas.

Pero cada combinación distinta de nombre y valores de etiqueta es una serie temporal independiente, con su propia memoria en la aplicación, su propia serie en Prometheus y su propio coste en cada consulta. De ahí la regla de oro:

La cardinalidad de una etiqueta debe ser baja y acotada. Si el número de valores posibles crece con el número de usuarios, de peticiones o de filas, no es una etiqueta.

Aplicado a CicloUrbana:

Etiqueta Valores posibles ¿Correcta? Motivo
tipoTarifa 3 Sí Fijo y conocido
estacion ~40 Sí Acotado y crece muy despacio
estado del alquiler 4 Sí Fijo
uri con plantilla ~25 endpoints Sí Acotado por el código
instancia 3-10 Sí Acotado por el despliegue
idUsuario Decenas de miles No Explosión: una serie por ciudadano
uri sin plantilla Infinitos (/alquileres/48213) No Una serie por alquiler
trazaId Uno por petición Nunca Cardinalidad infinita por definición
Mensaje de excepción Ilimitado No Puede llevar datos variables

Qué pasa exactamente cuando se incumple. Supón Counter.builder("alquileres.iniciados").tag("usuario", idUsuario). Con 50.000 ciudadanos aparecen 50.000 series: la aplicación retiene 50.000 objetos medidor en memoria y su MeterRegistry crece sin parar; /actuator/prometheus pasa de devolver 200 KB a devolver decenas de megabytes en cada recogida; Prometheus multiplica su consumo de memoria e índice; y las consultas de Grafana se vuelven lentas o directamente fallan. Es la causa número uno de caídas de sistemas de monitorización, tiene nombre propio —cardinality explosion— y lo peor es que no falla el día que se despliega, sino tres semanas después, cuando ya nadie relaciona una cosa con la otra.

La regla práctica que evita el error: si un dato identifica a un individuo o a una petición concreta, va en un log (09-05) o en una traza (09-06), nunca en una etiqueta de métrica. Las métricas responden «cuántos» y «cuánto»; el «cuál» es trabajo de las otras dos señales.

Un caso intermedio que conviene conocer: en Ribalta, 40 estaciones son una etiqueta perfectamente sana. Si CicloUrbana se extendiera a una red metropolitana de 4.000 estaciones habría que revisar la decisión —4.000 series por métrica multiplicadas por cada métrica de estación empieza a ser mucho— y probablemente pasar a etiquetar por distrito y dejar la estación concreta para las trazas.

  1. Instrumentar CicloUrbana: MetricasCicloUrbana

Las métricas automáticas dicen si el sistema está sano; las de negocio dicen si el servicio está sano. Son preguntas distintas: una API que responde 200 en 30 ms con cero alquileres iniciados a las ocho de la mañana está técnicamente perfecta y funcionalmente rota.

Todo se concentra en un componente, que es la práctica que mejor envejece:

package com.ciclourbana.comun.metricas;

@Component
public class MetricasCicloUrbana {

    private final MeterRegistry registro;
    private final Timer temporizadorTarifa;
    private final DistributionSummary duracionAlquileres;
    // La referencia FUERTE que mantiene vivos los gauges (apartado 8)
    private final Map<Long, AtomicInteger> disponiblesPorEstacion = new ConcurrentHashMap<>();

    public MetricasCicloUrbana(MeterRegistry registro) {
        this.registro = registro;

        this.temporizadorTarifa = Timer.builder("ciclourbana.tarifa.calculo")
                .description("Tiempo de calculo del importe de un alquiler")
                .publishPercentileHistogram()          // apartado 10
                .register(registro);

        this.duracionAlquileres = DistributionSummary.builder("ciclourbana.alquiler.duracion")
                .description("Duracion de los alquileres finalizados")
                .baseUnit("minutos")
                .publishPercentileHistogram()
                .register(registro);
    }

    /** Contador con etiqueta de baja cardinalidad: 3 valores posibles. */
    public void alquilerIniciado(TipoTarifa tarifa, Long idEstacion) {
        Counter.builder("ciclourbana.alquileres.iniciados")
                .description("Alquileres iniciados")
                .tag("tarifa", tarifa.name())
                .tag("estacion", String.valueOf(idEstacion))
                .register(registro)
                .increment();
    }

    public void alquilerFinalizado(TipoTarifa tarifa, Duration duracion) {
        Counter.builder("ciclourbana.alquileres.finalizados")
                .tag("tarifa", tarifa.name())
                .register(registro)
                .increment();
        duracionAlquileres.record(duracion.toMinutes());
    }

    /** Timer envolviendo el calculo: mide y devuelve el resultado. */
    public BigDecimal medirCalculoTarifa(Supplier<BigDecimal> calculo) {
        return temporizadorTarifa.record(calculo);
    }

    /** Gauge por estacion: se registra una vez y despues solo se actualiza el valor. */
    public void actualizarDisponibles(Long idEstacion, String nombre, int disponibles) {
        disponiblesPorEstacion.computeIfAbsent(idEstacion, id -> {
            AtomicInteger valor = new AtomicInteger();
            Gauge.builder("ciclourbana.bicicletas.disponibles", valor, AtomicInteger::get)
                    .description("Bicicletas disponibles por estacion")
                    .tag("estacion", nombre)
                    .register(registro);
            return valor;
        }).set(disponibles);
    }
}

Y su uso desde el servicio de dominio, que no conoce a Micrometer:

@Transactional
public AlquilerResponse iniciar(IniciarAlquilerRequest peticion) {
    Alquiler alquiler = /* ... logica de dominio ... */;
    BigDecimal estimado = metricas.medirCalculoTarifa(
            () -> calculadora.estimar(peticion.tipoTarifa(), DURACION_MEDIA));
    metricas.alquilerIniciado(peticion.tipoTarifa(), peticion.estacionOrigenId());
    return mapeador.aRespuesta(alquiler);
}

Tres decisiones de diseño merecen comentario. Un solo componente concentra los nombres de las métricas: sin él, las cadenas "ciclourbana.alquileres.iniciados" se dispersan por el código y aparecen las variantes (alquileres_iniciados, alquiler.iniciado) que rompen los cuadros de mando. Los nombres siguen la convención de Micrometer: minúsculas separadas por puntos y jerarquía de lo general a lo particular (ciclourbana.alquileres.iniciados), que cada sistema traduce a su propio estilo —Prometheus lo convertirá a ciclourbana_alquileres_iniciados_total—. Y la instrumentación no cambia el comportamiento: si registro fuera un SimpleMeterRegistry de pruebas, todo sigue funcionando igual.

Una advertencia sobre el Counter dentro del método: Counter.builder(...).register(registro) en cada invocación no crea un contador nuevo, porque register devuelve el existente si el nombre y las etiquetas coinciden. Es correcto, aunque tiene un pequeño coste de búsqueda; en un camino muy caliente conviene cachear los contadores en un Map por etiqueta.

  1. El Gauge, la referencia débil y su error clásico

Un Gauge no almacena valores: guarda una referencia al objeto que los tiene y una función para leerlo, y esa referencia es débil. Micrometer lo hace a propósito, para no impedir que el recolector de basura libere objetos que la aplicación ya no usa: una métrica nunca debe provocar una fuga de memoria.

La consecuencia es el error más desconcertante de todo Micrometer:

// MAL: el AtomicInteger no tiene ninguna referencia fuerte
public void publicarDisponibles(Long idEstacion, int valor) {
    Gauge.builder("ciclourbana.bicicletas.disponibles", new AtomicInteger(valor),
                  AtomicInteger::get)
         .tag("estacion", String.valueOf(idEstacion))
         .register(registro);
}

Esto funciona perfectamente durante minutos y luego la métrica empieza a devolver NaN. Cuando el GC pasa, el AtomicInteger —al que nadie apunta salvo la referencia débil del gauge— desaparece, y el medidor se queda sin fuente. Y el fallo es intermitente y depende de la presión de memoria, lo que lo convierte en el clásico «en desarrollo funcionaba».

La solución es la del apartado 7: el Map de campo mantiene la referencia fuerte mientras el componente viva. Otras dos formas correctas: usar registro.gauge("nombre", tags, objetoQueYaVive, funcion) sobre un objeto de larga vida, o Gauge.builder("cola.pendientes", laCola, Collection::size) sobre una colección que ya es un campo del bean.

Tres reglas más sobre gauges. Se registra una vez y luego solo se actualiza el valor: volver a registrar el mismo nombre con las mismas etiquetas devuelve el existente, pero registrarlo en un bucle es un olor a diseño. La función se invoca en el momento de la recogida, no cuando tú llamas: por eso debe ser barata y no debe lanzar excepciones —jamás un Gauge que ejecute una consulta a la base de datos, porque Prometheus la lanzaría cada 15 segundos—. Y un gauge que no cambia nunca no aporta nada: es una constante disfrazada de métrica.

Para la disponibilidad de Ribalta, el gauge se refresca desde la tarea programada de 07-03, que ya recorre las estaciones cada quince segundos, en lugar de calcularse en cada recogida.

  1. @Timed y @Counted

Para instrumentar sin escribir código, Micrometer ofrece dos anotaciones basadas en aspectos. Requieren la dependencia de AOP y registrar los aspectos:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Bean TimedAspect timedAspect(MeterRegistry registro) { return new TimedAspect(registro); }
@Bean CountedAspect countedAspect(MeterRegistry registro) { return new CountedAspect(registro); }
@Timed(value = "ciclourbana.tarifa.calculo",
       description = "Tiempo de calculo del importe",
       extraTags = {"componente", "tarifas"},
       percentiles = {0.5, 0.95, 0.99})
public BigDecimal calcular(TipoTarifa tipo, Duration duracion) { ... }

@Counted(value = "ciclourbana.cobros.intentos", recordFailuresOnly = false)
public ResultadoCobro cobrar(Long idAlquiler, BigDecimal importe) { ... }

@Timed genera un Timer con etiquetas class, method y exception —esta última vale none cuando no hubo error, lo que permite separar la latencia de los éxitos de la de los fallos—. @Counted cuenta invocaciones con las mismas etiquetas.

Sus límites, que conviene tener claros antes de usarlas por todas partes. Sufren la trampa del proxy por cuarta vez en el curso: una llamada interna no se mide. No pueden etiquetar por datos del negocio: extraTags solo admite constantes, así que no hay forma de etiquetar por tipoTarifa recibido como argumento —para eso hace falta el código explícito del apartado 7—. Y añaden un aspecto por método anotado, con su coste de invocación.

El criterio de CicloUrbana: @Timed para instrumentación transversal y rápida —temporizar un servicio entero mientras se investiga algo—, y código explícito para las métricas de negocio que llevan etiquetas y que van a alimentar cuadros de mando permanentes.

  1. Percentiles frente a histogramas

Este apartado es el más técnico de la lección y el que más consecuencias tiene al escalar. En 09-01 quedó claro que el SLO se escribe en p95 y p99. La pregunta ahora es dónde se calculan esos percentiles, y hay dos respuestas incompatibles.

management.metrics.distribution:
  percentiles:
    ciclourbana.tarifa.calculo: 0.5, 0.95, 0.99      # opcion A: en el cliente
  percentiles-histogram:
    http.server.requests: true                        # opcion B: histograma
    ciclourbana.alquiler.duracion: true
  slo:
    http.server.requests: 100ms, 300ms, 500ms, 1s, 3s
  minimum-expected-value:
    http.server.requests: 10ms
  maximum-expected-value:
    http.server.requests: 10s
percentiles (calculados en el cliente) percentiles-histogram (cubetas)
Qué exporta Tres números ya calculados: quantile="0.95" Muchas series _bucket{le="..."}
Dónde se calcula En la JVM, con un estimador aproximado En el sistema de monitorización, con histogram_quantile
Agregable entre instancias No Sí
Series generadas Pocas Decenas por métrica
Configurable a posteriori No: hay que redesplegar Sí: cualquier percentil, sin tocar la aplicación
Cuándo usarlo Una instancia, o un vistazo local Producción con varias instancias

Por qué los percentiles del cliente no se pueden agregar. Es la consecuencia de aquella regla de 09-01: los percentiles no se promedian. Si la instancia 1 informa de p95 = 200 ms y la instancia 2 de p95 = 400 ms, el p95 real del servicio no es 300 ms, y no hay forma de calcularlo a partir de esos dos números: la información necesaria se perdió al calcularlos. Con tres réplicas en Kubernetes (08-04), esa gráfica de «p95» es sencillamente falsa.

Cómo lo resuelve el histograma. En lugar de un percentil, cada instancia exporta cuántas observaciones cayeron por debajo de cada umbral: 1.200 por debajo de 100 ms, 1.380 por debajo de 300 ms, y así. Esos recuentos sí se suman entre instancias, y a partir de la suma se interpola cualquier percentil con histogram_quantile (09-04). Se decide el percentil al consultar, no al instrumentar.

El precio es la cardinalidad: un histograma genera una serie por cubeta. Por eso existen los tres ajustes de arriba: slo añade cubetas exactamente en los umbrales que te importan —muy útil, porque permite después calcular directamente «qué porcentaje de peticiones bajó de 300 ms», que es la formulación literal de un objetivo de servicio—, y minimum-expected-value / maximum-expected-value acotan el rango, eliminando las cubetas absurdas por debajo de 10 ms o por encima de 10 s y reduciendo mucho el número de series.

La decisión de CicloUrbana: histogramas en las pocas métricas que sostienen los SLO (http.server.requests y la duración de alquileres) y nada de distribución en el resto. Activar percentiles-histogram para todo es una de las formas más rápidas de provocar el problema del apartado 6.

  1. MeterFilter y MeterRegistryCustomizer

Cuando hay que corregir la instrumentación —propia o de una biblioteca que no controlas—, MeterFilter interviene en el registro de cada medidor:

@Bean
MeterRegistryCustomizer<MeterRegistry> personalizarRegistro() {
    return registro -> registro.config()
            // 1. Denegar metricas caras que nadie usa
            .meterFilter(MeterFilter.denyNameStartsWith("jvm.buffer"))
            // 2. Limitar la cardinalidad de una etiqueta: red de seguridad del apartado 6
            .meterFilter(MeterFilter.maximumAllowableTags(
                    "ciclourbana.alquileres.iniciados", "estacion", 100,
                    MeterFilter.deny()))
            // 3. Techo global de series por metrica
            .meterFilter(MeterFilter.maximumAllowableMetrics(5_000))
            // 4. Renombrar una metrica heredada sin tocar el codigo que la publica
            .meterFilter(MeterFilter.renameTag("ciclourbana.alquileres.iniciados",
                    "tipo", "tarifa"))
            // 5. Reglas de distribucion por codigo
            .meterFilter(new MeterFilter() {
                @Override
                public DistributionStatisticConfig configure(Meter.Id id,
                                                             DistributionStatisticConfig config) {
                    return id.getName().startsWith("ciclourbana.")
                            ? DistributionStatisticConfig.builder()
                                  .percentilesHistogram(true).build().merge(config)
                            : config;
                }
            });
}

Los cinco usos, en orden de utilidad real. Denegar métricas que no se usan reduce el tamaño de cada recogida —jvm.buffer y algunas de tomcat rara vez se miran—. maximumAllowableTags es la red de seguridad contra la explosión de cardinalidad: pasadas 100 estaciones distintas, deja de registrar nuevas series en lugar de tumbar Prometheus; es un salvavidas, no una excusa para etiquetar mal. maximumAllowableMetrics pone un techo global. renameTag y MeterFilter.commonTags permiten adaptar métricas de terceros a tu convención. Y la configuración de distribución por código aplica reglas a familias enteras de métricas por prefijo, algo que el YAML no permite.

Conviene distinguir los dos beans: MeterRegistryCustomizer se ejecuta una vez sobre el registro, mientras que el MeterFilter que instala se ejecuta para cada medidor que se registre. Y hay un orden que sorprende: los filtros se aplican en el orden en que se añaden, y un deny posterior no revoca un medidor ya registrado, así que los filtros deben instalarse antes de que la aplicación empiece a instrumentar —de ahí que se hagan en un @Bean y no en un @PostConstruct cualquiera—.

  1. El pool, la base de datos y la caché: qué números vigilar

Con lo aprendido en 09-01 y 09-02, estas son las métricas que traducen aquellos diagnósticos puntuales en vigilancia continua:

Métrica Valor sano Qué significa si se degrada
hikaricp.connections.pending 0 casi siempre Hilos esperando conexión: consultas lentas o pool corto
hikaricp.connections.acquire (p99) < 10 ms Lo mismo, con magnitud
hikaricp.connections.usage (p99) < duración de la transacción esperada Conexión retenida: llamada remota dentro de la transacción
hikaricp.connections.timeout 0 Se está agotando el connection-timeout: incidente en curso
spring.data.repository.invocations (_count por método) Estable por petición Si crece con el volumen de datos: N+1
cache.gets{result="hit"} / total > 0,9 Caché inútil, clave mal elegida o autoinvocación (09-02)
cache.evictions Cerca de 0 maximumSize demasiado pequeño
jvm.gc.pause (_max) < 200 ms con G1 Pausas que explican el p99
jvm.memory.used{area="heap"} tras GC Baja tras cada ciclo Si solo sube: fuga
tomcat.threads.busy / tomcat.threads.config.max < 0,8 Saturación del contenedor web

La más rentable de todas es la penúltima combinada con jvm.gc.pause: memoria que sube y no baja tras las pausas es la firma de una fuga, y verla en una gráfica de un mes es infinitamente más fácil que en un volcado de heap. Y la primera, pending, es la que convierte el diagnóstico manual del ejercicio 1 de 09-01 en una alerta automática.

  1. La Observation API: una instrumentación, dos señales

Hasta aquí hemos registrado métricas. Cuando en 09-06 haya que registrar trazas, aparecería el problema de instrumentar dos veces lo mismo. Spring Boot 3 lo resuelve con la Micrometer Observation API: se declara una observación y la infraestructura produce a la vez una métrica, un span de traza y, si se configura, una entrada de log.

@Service
public class CalculadoraTarifaService {

    private final ObservationRegistry observaciones;

    public BigDecimal calcular(TipoTarifa tipo, Duration duracion) {
        return Observation.createNotStarted("ciclourbana.tarifa.calculo", observaciones)
                .contextualName("calculo-tarifa")
                .lowCardinalityKeyValue("tarifa", tipo.name())      // -> etiqueta de metrica
                .highCardinalityKeyValue("minutos", String.valueOf(duracion.toMinutes()))
                .observe(() -> calculadora.importe(tipo, duracion));   // -> solo en la traza
    }
}

La distinción entre lowCardinalityKeyValue y highCardinalityKeyValue es la regla del apartado 6 convertida en API, y es una de las mejores ideas de Micrometer: las claves de baja cardinalidad se convierten en etiquetas de la métrica y en atributos del span; las de alta cardinalidad van solo al span, donde no hacen daño. El modelo obliga a pensar en la cardinalidad al escribir el código, que es justo donde hay que pensarlo.

La versión declarativa necesita el aspecto correspondiente:

@Bean ObservedAspect observedAspect(ObservationRegistry registro) {
    return new ObservedAspect(registro);
}

@Observed(name = "ciclourbana.tarifa.calculo", contextualName = "calculo-tarifa")
public BigDecimal calcular(TipoTarifa tipo, Duration duracion) { ... }

Hoy, con solo Micrometer Metrics en el classpath, esto produce un Timer idéntico al de @Timed. En 09-06, al añadir Micrometer Tracing, el mismo código empezará a producir spans sin tocar una línea. Esa es la razón para conocerlo ya: lo que se instrumente con la Observation API estará listo para la trazabilidad distribuida; lo que se instrumente con Timer a mano, no.

Como complemento, un ObservationHandler propio permite reaccionar a cada observación —por ejemplo, escribir una línea de log con su duración— y ObservationPredicate permite filtrar cuáles se registran, típicamente para ignorar /actuator/** y no medirse a uno mismo.

  1. De las métricas a los SLO: RED y USE

Instrumentar sin un método produce cientos de gráficas que nadie mira. Los dos métodos de referencia dicen qué mirar y son complementarios:

RED, para servicios (lo que ve el usuario): Rate (peticiones por segundo), Errors (proporción que falla) y Duration (distribución de la latencia).

USE, para recursos (lo que consume el sistema): Utilization (porcentaje de uso), Saturation (trabajo encolado esperando) y Errors (fallos del recurso).

Método Aplicado a Señal Métrica de CicloUrbana SLO propuesto
RED API de alquileres Rate http.server.requests _count sobre /api/v1/alquileres Informativo
RED API de alquileres Errors Proporción con outcome="SERVER_ERROR" < 0,5 % en 5 min
RED API de alquileres Duration http.server.requests p99 < 800 ms
RED Pasarela de pagos Errors resilience4j.circuitbreaker.state Circuito cerrado > 99 %
USE Pool de conexiones Utilization hikaricp.connections.active / max < 0,8
USE Pool de conexiones Saturation hikaricp.connections.pending = 0
USE Hilos de Tomcat Utilization tomcat.threads.busy / max < 0,8
USE JVM Saturation jvm.gc.pause _max < 200 ms
Negocio Servicio de Ribalta — ciclourbana.alquileres.iniciados > 0 en hora punta

La última fila es la más valiosa y la que casi nadie pone. Todos los indicadores técnicos pueden estar verdes mientras el servicio está roto: un despliegue que rompió el botón de alquilar en la app deja la API contestando 200 a las consultas de estaciones y cero alquileres iniciados. Una alerta sobre esa métrica de negocio detecta en cinco minutos lo que las técnicas no detectan nunca.

La implementación de estas alertas es trabajo de 09-04, con reglas en Prometheus y Alertmanager. Lo que se decide aquí es qué se mide y qué umbral se considera aceptable, y esa decisión es de producto tanto como de ingeniería.

  1. Probar que una métrica se registra

Una métrica de negocio es comportamiento observable: si un refactor deja de incrementar el contador de alquileres, el cuadro de mando miente y nadie se entera. Se prueba con SimpleMeterRegistry, la implementación en memoria de Micrometer.

class MetricasCicloUrbanaTest {

    private final MeterRegistry registro = new SimpleMeterRegistry();
    private final MetricasCicloUrbana metricas = new MetricasCicloUrbana(registro);

    @Test
    void cuentaLosAlquileresPorTipoDeTarifa() {
        metricas.alquilerIniciado(TipoTarifa.ESTANDAR, 1L);
        metricas.alquilerIniciado(TipoTarifa.ESTANDAR, 1L);
        metricas.alquilerIniciado(TipoTarifa.ESTUDIANTE, 2L);

        assertThat(registro.get("ciclourbana.alquileres.iniciados")
                .tag("tarifa", "ESTANDAR").counter().count()).isEqualTo(2.0);
        assertThat(registro.get("ciclourbana.alquileres.iniciados")
                .tag("tarifa", "ESTUDIANTE").counter().count()).isEqualTo(1.0);
    }

    @Test
    void elGaugeSobreviveAlRecolectorDeBasura() {
        metricas.actualizarDisponibles(1L, "Plaza Mayor", 12);
        System.gc();                                  // fuerza el escenario del apartado 8

        assertThat(registro.get("ciclourbana.bicicletas.disponibles")
                .tag("estacion", "Plaza Mayor").gauge().value()).isEqualTo(12.0);
    }
}

La segunda prueba es la joya: verifica que la referencia fuerte existe, que es precisamente el error que se manifiesta solo en producción y tras horas de ejecución. System.gc() es una sugerencia y no una garantía, pero en la práctica basta para que la prueba falle con la versión incorrecta del apartado 8.

En integración, @SpringBootTest con @AutoConfigureObservability levanta el registro real y permite comprobar que las métricas HTTP automáticas aparecen —por defecto, las pruebas desactivan la exportación—:

@SpringBootTest
@AutoConfigureObservability
class MetricasHttpIT {

    @Autowired MeterRegistry registro;
    @Autowired TestRestTemplate cliente;

    @Test
    void laPeticionQuedaRegistradaConSuUriPlantilla() {
        cliente.getForEntity("/api/v1/estaciones/1", EstacionResponse.class);

        assertThat(registro.get("http.server.requests")
                .tag("uri", "/api/v1/estaciones/{id}")     // plantilla, no el 1
                .timer().count()).isEqualTo(1L);
    }
}

Errores Comunes y Consejos

Etiquetar por identificador de usuario, por URI con identificadores o por identificador de traza. Es el error grave de la lección: explosión de cardinalidad que tumba primero a Prometheus y después a la aplicación, semanas después de haberse introducido. Lo que identifica a un individuo va al log o a la traza.

Perder el Gauge por la referencia débil. Funciona un rato y luego devuelve NaN. Guarda siempre una referencia fuerte en un campo del componente, y escribe la prueba con System.gc().

Usar un Counter para algo que baja. «Alquileres en curso» no es un contador: es un Gauge. Un contador que se decrementa rompe rate() y produce gráficas absurdas.

Creer que percentiles sirve con varias instancias. Los percentiles calculados en la JVM no se pueden agregar; con tres réplicas, esa gráfica es falsa. Se usan histogramas.

Activar percentiles-histogram para todas las métricas. Multiplica las series por diez. Solo en las que sostienen un SLO, y acotadas con minimum/maximum-expected-value.

Hacer trabajo dentro de la función de un Gauge. Se invoca en cada recogida, cada 15 segundos, para siempre. Una consulta a la base de datos ahí es una carga permanente que nadie relaciona con nada.

Medir solo lo técnico. Con todos los indicadores en verde el servicio puede estar roto. La métrica que avisa de un despliegue que rompió el alquiler es ciclourbana.alquileres.iniciados cayendo a cero.

Consejo: centraliza los nombres de métricas en un componente o en constantes. Los nombres son un contrato con los cuadros de mando y las alertas de 09-04; un cambio de nombre rompe paneles en silencio. Y sigue la convención de Micrometer —minúsculas con puntos, prefijo de aplicación—, dejando que cada exportador la traduzca.

Consejo: usa la Observation API para lo nuevo. Cuesta lo mismo que un Timer y en 09-06 producirá spans sin tocar el código.

Consejo: pon las etiquetas comunes (application, entorno, instancia) desde el primer día. Añadirlas después obliga a reescribir todas las consultas y a perder la continuidad de las gráficas.

Ejercicios

Ejercicio 1: elegir el medidor y las etiquetas

Para cada necesidad del ayuntamiento de Ribalta, indica el tipo de medidor, el nombre, las etiquetas —justificando su cardinalidad— y si necesita histograma: (a) cuántas bicicletas hay ahora mismo en mantenimiento, por taller (hay 2 talleres); (b) cuántos cobros ha rechazado la pasarela, distinguiendo el motivo (SALDO, TARJETA_CADUCADA, TECNICO); (c) cuánto tarda la importación nocturna del ayuntamiento, sabiendo que dura entre 20 y 50 minutos y que se quiere vigilar mientras se ejecuta; (d) el importe de cada alquiler facturado, para poder responder «¿cuál es el importe mediano y el p95?»; (e) cuántas veces cada ciudadano ha alquilado este mes.

Ejercicio 2: diagnosticar una instrumentación rota

Un compañero ha añadido esta instrumentación. Tres semanas después, Prometheus consume 12 GB de memoria, /actuator/prometheus tarda 8 segundos en responder y la gráfica de bicicletas disponibles muestra huecos. Encuentra los tres problemas, explica el síntoma de cada uno y escribe la versión corregida.

@RestController
public class AlquilerController {

    @PostMapping("/api/v1/alquileres")
    public ResponseEntity<AlquilerResponse> iniciar(@RequestBody IniciarAlquilerRequest p,
                                                    Authentication auth) {
        AlquilerResponse r = alquilerService.iniciar(p);
        Counter.builder("alquileres")
               .tag("usuario", auth.getName())
               .tag("uri", "/api/v1/alquileres/" + r.id())
               .register(registro).increment();
        Gauge.builder("bicis.libres", new AtomicInteger(contarLibres()), AtomicInteger::get)
             .register(registro);
        return ResponseEntity.status(201).body(r);
    }
}

Ejercicio 3: definir los SLO de CicloUrbana

El ayuntamiento pide un acuerdo de nivel de servicio. Define los SLI y SLO de CicloUrbana siguiendo RED y USE: elige cuatro indicadores de servicio y tres de recursos, di con qué métrica de Micrometer se calcula cada uno, propón un umbral y un periodo de evaluación, e indica cuáles de esos umbrales deberían despertar a alguien de madrugada y cuáles no. Justifica la configuración de management.metrics.distribution que hace posible medirlos.

Soluciones

Solución 1.

(a) Bicicletas en mantenimiento por taller: Gauge, ciclourbana.bicicletas.mantenimiento, etiqueta taller (2 valores: cardinalidad mínima). Es un valor que sube y baja, luego no es un contador. Sin histograma —los gauges no tienen distribución—. Cuidado con la referencia fuerte del apartado 8: un Map<String, AtomicInteger> como campo, refrescado desde la tarea programada, y nunca una consulta a la base de datos dentro de la función del gauge.

(b) Cobros rechazados: Counter, ciclourbana.cobros.rechazados, etiqueta motivo con 3 valores fijos del enumerado. Solo sube y lo que interesa es su ritmo, que rate() extraerá en 09-04. La etiqueta debe salir de un enumerado, nunca del mensaje de error de la pasarela: un texto libre devuelto por un tercero es cardinalidad ilimitada disfrazada.

(c) Importación nocturna: LongTaskTimer, ciclourbana.importacion.duracion. Es el caso que justifica su existencia: un Timer normal no publicaría nada durante los 40 minutos que dura y solo registraría el resultado al terminar, con lo que sería imposible saber si está en marcha o colgada. El LongTaskTimer publica active_tasks y duration de las tareas en curso, permitiendo alertar de «lleva más de 60 minutos». Complemento útil: un Gauge con la marca de tiempo de la última ejecución correcta, para detectar que no se ha ejecutado.

(d) Importe facturado: DistributionSummary, ciclourbana.alquiler.importe, baseUnit("euros"), con publishPercentileHistogram() porque la pregunta pide explícitamente mediana y p95 y hay varias instancias. No es un Timer porque no mide tiempo. Etiqueta razonable: tarifa (3 valores). Conviene acotar con minimum-expected-value: 0.5 y maximum-expected-value: 50 para no generar cubetas inútiles.

(e) Alquileres por ciudadano y mes: ninguna métrica. Es cardinalidad ilimitada —una serie por ciudadano— y además no es una pregunta de monitorización sino de negocio, con la fila del alquiler ya guardada en PostgreSQL: se responde con SELECT usuario_id, count(*) ... GROUP BY usuario_id. La regla del apartado 6 en su forma más pura: las métricas responden «cuántos» agregado, no «quién».

Solución 2.

Problema 1 — etiqueta usuario de cardinalidad ilimitada. Una serie por ciudadano; con 50.000 usuarios, 50.000 series de un solo contador. Síntoma: memoria de Prometheus disparada y consultas lentas. Es la causa principal de los 12 GB.

Problema 2 — etiqueta uri con el identificador del alquiler. Cardinalidad infinita: una serie nueva por cada alquiler creado, para siempre, y ninguna de ellas se reutiliza jamás. Síntoma: /actuator/prometheus devolviendo megabytes y tardando 8 segundos, porque su tamaño crece linealmente con el número de alquileres históricos. Es incluso peor que el anterior, porque no tiene techo.

Problema 3 — Gauge registrado en cada petición sobre un objeto temporal. Dos fallos en una línea: se registra dentro del método, así que se intenta crear un medidor por petición, y el AtomicInteger no tiene referencia fuerte, así que el GC lo libera y el gauge devuelve NaN. Síntoma: los huecos en la gráfica. Además, contarLibres() en el camino de la petición añade una consulta a cada alquiler.

Versión corregida:

@RestController
public class AlquilerController {

    private final MetricasCicloUrbana metricas;      // el componente del apartado 7

    @PostMapping("/api/v1/alquileres")
    public ResponseEntity<AlquilerResponse> iniciar(@RequestBody IniciarAlquilerRequest p) {
        AlquilerResponse r = alquilerService.iniciar(p);   // el servicio ya instrumenta
        return ResponseEntity.status(201).body(r);
    }
}

Las decisiones: la instrumentación se va al servicio, con metricas.alquilerIniciado(tipoTarifa, idEstacion) y sus dos etiquetas de baja cardinalidad; la URI y el estado ya los aporta http.server.requests con la plantilla, de modo que el contador manual de peticiones sobraba por completo; y el gauge de bicicletas libres se registra una vez en MetricasCicloUrbana, con su referencia fuerte y actualizado desde la tarea programada de 07-03. Como red de seguridad, se añade el MeterFilter.maximumAllowableTags(...) del apartado 11, que habría convertido esta caída en una métrica truncada.

Solución 3.

Indicadores de servicio (RED), medidos sobre http.server.requests:

SLI Cálculo SLO Periodo ¿Despierta?
Disponibilidad de la API 1 − (outcome="SERVER_ERROR" / total) ≥ 99,5 % 30 días Sí si > 5 % en 5 min
Latencia de inicio de alquiler p99 de uri="/api/v1/alquileres" < 800 ms 5 min Sí, sostenido 15 min
Latencia de consulta de estaciones p95 de uri="/api/v1/estaciones" < 300 ms 5 min No: aviso en horario laboral
Alquileres iniciados en hora punta rate de ciclourbana.alquileres.iniciados > 0 entre 7 y 10 h 10 min Sí: el servicio está roto

Indicadores de recursos (USE):

SLI Métrica Umbral ¿Despierta?
Saturación del pool hikaricp.connections.pending 0; alerta si > 5 durante 5 min Sí: precede a una caída
Utilización de hilos de Tomcat tomcat.threads.busy / config.max < 0,8 No: aviso
Pausas de GC jvm.gc.pause _max < 200 ms No: aviso, salvo con latencia degradada

Qué despierta y qué no. El criterio es doble: el usuario lo está sufriendo ahora y hay algo que hacer. Por eso despiertan la tasa de error, la latencia del endpoint crítico sostenida, la ausencia de alquileres en hora punta y el pool saturado —que es el aviso temprano de una caída completa—. No despiertan las pausas de GC ni la utilización de hilos: son causas, no síntomas, y su lugar es un aviso en horario laboral. Es el principio que se desarrollará en 09-04: alertar sobre síntomas, no sobre causas, porque cada alerta que no exige acción inmediata erosiona la credibilidad de todas las demás.

Configuración necesaria:

management.metrics.distribution:
  percentiles-histogram:
    http.server.requests: true          # agregable entre las 3 instancias
  slo:
    http.server.requests: 300ms, 800ms  # cubetas justo en los umbrales de los SLO
  minimum-expected-value.http.server.requests: 10ms
  maximum-expected-value.http.server.requests: 5s

Histograma y no percentiles, porque con tres réplicas los percentiles calculados en cada JVM no se pueden agregar y darían una cifra falsa (apartado 10). Las cubetas de slo en 300 ms y 800 ms permiten responder directamente «qué porcentaje de peticiones cumplió el objetivo», que es la formulación literal del SLO y la que se llevará al informe mensual del ayuntamiento. Y el acotado entre 10 ms y 5 s evita decenas de cubetas inútiles, manteniendo la cardinalidad bajo control.

Conclusión

CicloUrbana ya no solo se deja preguntar: se mide sola y continuamente. Tienes claros los tres pilares de la observabilidad y qué responde cada uno —métricas para detectar, trazas para localizar, logs para entender—, y entiendes Micrometer como la fachada que hace que tu código registre contadores sin saber quién los recogerá, con MeterRegistry inyectable como un bean más. Sabes elegir entre los seis tipos de medidor con criterios que resuelven las dudas reales: contador para «cuántas veces» y gauge para «cuánto hay ahora», Timer para operaciones del programa y DistributionSummary para magnitudes de negocio, LongTaskTimer para lo que hay que vigilar mientras ocurre.

Conoces el catálogo de métricas que existen sin escribir código —con http.server.requests como la más valiosa del sistema— y sabes consultarlas por /actuator/metrics con ?tag=, con la limitación de que ahí no hay historia. Y tienes grabada la regla de oro de esta lección: la cardinalidad de una etiqueta debe ser baja y acotada; tarifa y estacion sí, idUsuario, la URI con identificadores y el trazaId jamás, porque lo que identifica a un individuo pertenece a un log o a una traza y no a una métrica.

Has instrumentado la red de Ribalta con MetricasCicloUrbana: contadores de alquileres iniciados y finalizados por tarifa, un gauge de bicicletas disponibles por estación —con la referencia fuerte que evita el NaN de la referencia débil, y su prueba con System.gc()—, un Timer del cálculo de tarifa y un DistributionSummary de la duración de los trayectos, todo tras una fachada que deja al dominio sin conocer Micrometer. Sabes cuándo @Timed y @Counted bastan y dónde están sus tres límites. Entiendes la diferencia que más importa al escalar: los percentiles calculados en la JVM no se agregan entre instancias y los histogramas sí, con slo, minimum-expected-value y maximum-expected-value para que esa potencia no se pague con una explosión de series. Y sabes corregir y proteger la instrumentación con MeterFilter, incluida la red de seguridad de maximumAllowableTags.

Por último, tienes los números que vigilar en el pool, la base de datos y las cachés de 09-02; la Observation API como el modelo unificado que convierte la cardinalidad en API (lowCardinalityKeyValue frente a highCardinalityKeyValue) y que en 09-06 producirá spans sin tocar el código; y los métodos RED y USE convertidos en SLI y SLO concretos, incluida la fila que casi nadie pone y que detecta un servicio roto con todo en verde: alquileres iniciados = 0 en hora punta.

Queda el problema que ha estado presente todo el rato: todo esto vive en memoria y muere con el proceso. Cada despliegue de 08-05 borra las métricas; /actuator/metrics no tiene historia; no hay gráficas, no hay comparación con la semana pasada y, sobre todo, no hay una sola alerta. Los SLO del apartado 14 están definidos pero no vigilados por nadie. La siguiente lección, Uso de Prometheus y Grafana, cierra ese hueco: el modelo de recogida por consulta, el formato de /actuator/prometheus, PromQL de verdad —rate, sum by, histogram_quantile sobre las cubetas que acabamos de configurar—, el cuadro de mando de CicloUrbana panel a panel y las reglas de alerta que convierten estos números en una llamada de teléfono cuando de verdad hace falta.

Curso de Spring Boot

Módulo 1: Introducción a Spring Boot

Módulo 2: Conceptos Básicos de Spring Boot

Módulo 3: Construyendo Servicios Web RESTful

Módulo 4: Acceso a Datos con Spring Boot

Módulo 5: Seguridad en Spring Boot

Módulo 6: Pruebas en Spring Boot

Módulo 7: Funciones Avanzadas de Spring Boot

Módulo 8: Despliegue de Aplicaciones Spring Boot

Módulo 9: Rendimiento y Monitoreo

Módulo 10: Mejores Prácticas y Consejos

© Copyright 2026. Todos los derechos reservados