Email copiado — support@tuurt.com
Cargando experiencia
spring · 09 de septiembre de 2026 · 6 min

Observabilidad en Spring Boot con Micrometer y OpenTelemetry: que metricas exportar antes de que el cliente pregunte por que esta lento

Un dashboard lleno de CPU y memoria no explica por que una request especifica fue lenta. Vemos como configuramos Micrometer con OpenTelemetry en Spring Boot 3 para exportar metricas de negocio y trazas que si responden esa pregunta.

Por Equipo Tuurt

Observabilidad en Spring Boot con Micrometer y OpenTelemetry: qué métricas exportar antes de que el cliente pregunte por qué está lento

Casi todos los servicios Spring Boot que heredamos ya tienen un dashboard de infraestructura: CPU, memoria heap, threads activos, conexiones del pool de base de datos. Ese dashboard casi nunca sirve para responder la pregunta que en realidad llega primero: "el checkout de un usuario tardó ocho segundos ayer a las tres de la tarde, ¿qué pasó". La CPU estaba en un nivel normal en ese momento. La memoria no tenía picos. El dashboard dice que el sistema estaba sano, y sin embargo una request concreta fue lenta. Esa desconexión es la que nos hizo cambiar el enfoque: en vez de partir de qué métricas de infraestructura son fáciles de exponer, partimos de qué pregunta necesitamos responder y trabajamos hacia atrás.

Por qué las métricas de infraestructura no alcanzan

Una métrica de infraestructura describe el estado del proceso o de la máquina: uso de CPU, threads del pool de Tomcat, conexiones abiertas en el pool de HikariCP. Son necesarias para saber si el sistema como un todo está bajo presión, pero están agregadas sobre todo el tráfico del servicio. Si el diez por ciento de las requests de un endpoint específico son lentas por una razón puntual -una fila bloqueada en una tabla, una llamada a un proveedor externo que empezó a degradarse-, esa señal se diluye entre el noventa por ciento de requests que sí van bien, y el promedio de CPU no se mueve lo suficiente como para disparar ninguna alerta.

Lo que responde a "por qué esta request fue lenta" no es una métrica agregada, es una traza distribuida de esa request en particular, con los tiempos de cada tramo: cuánto tardó la query a la base de datos, cuánto la llamada al proveedor externo, cuánto el propio procesamiento del controlador. Y lo que responde a "cuántas requests de este tipo están teniendo este problema" no es CPU, es una métrica de negocio con las etiquetas correctas: duración del endpoint de checkout, separada por resultado, no duración genérica de todas las requests HTTP.

Micrometer y OpenTelemetry no compiten, se complementan

Micrometer es la fachada de instrumentación que ya trae Spring Boot: expone un MeterRegistry y anota automáticamente cosas como duración de requests HTTP, queries JPA y llamadas salientes con RestClient. OpenTelemetry es el estándar de exportación y el modelo de trazas distribuidas. En Spring Boot 3 no hace falta elegir entre uno u otro: Micrometer Tracing usa OpenTelemetry como implementación por debajo, y el mismo micrometer-registry-otlp exporta tanto métricas como trazas al mismo colector, con el mismo formato.

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-otlp</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
management:
  otlp:
    metrics:
      export:
        url: http://otel-collector:4318/v1/metrics
        step: 15s
    tracing:
      endpoint: http://otel-collector:4318/v1/traces
  tracing:
    sampling:
      probability: 1.0

El step: 15s define cada cuánto se agregan y envían las métricas al colector, no cada cuánto se miden: Micrometer sigue registrando cada evento en el momento en que ocurre, y agrega internamente antes de exportar. La probabilidad de muestreo de trazas en 1.0 tiene sentido mientras se está calibrando qué exportar; en un servicio con volumen alto conviene bajarla, pero eso es una decisión posterior a saber qué se necesita capturar, no antes.

Métricas de negocio: el Timer no es gratis, hay que decidir las etiquetas

Spring Boot ya expone http.server.requests para cada endpoint automáticamente, con el método HTTP, la ruta y el código de estado como etiquetas. Eso alcanza para saber que un endpoint en general responde lento, pero no distingue por qué. Cuando el motivo de lentitud depende de una condición de negocio -por ejemplo, si el checkout requiere o no verificación adicional de fraude-, esa condición tiene que quedar como etiqueta propia, no mezclada dentro de la duración genérica del endpoint.

@Service
public class CheckoutService {

    private final MeterRegistry registry;

    public CheckoutService(MeterRegistry registry) {
        this.registry = registry;
    }

    public CheckoutResult process(Order order) {
        Timer.Sample sample = Timer.start(registry);
        String fraudPath = order.requiresFraudCheck() ? "with_fraud_check" : "standard";
        try {
            CheckoutResult result = doProcess(order);
            sample.stop(Timer.builder("checkout.process.duration")
                .tag("fraud_path", fraudPath)
                .tag("outcome", result.outcome().name())
                .register(registry));
            return result;
        } catch (Exception ex) {
            sample.stop(Timer.builder("checkout.process.duration")
                .tag("fraud_path", fraudPath)
                .tag("outcome", "error")
                .register(registry));
            throw ex;
        }
    }
}

Con esa métrica, un dashboard puede separar el percentil de duración de los checkouts que pasan por verificación de fraude de los que no, en vez de ver un único percentil mezclado que no dice a cuál de los dos caminos corresponde la cola lenta. Esa separación es la diferencia entre un dashboard que confirma que algo anda mal en general y uno que apunta a qué parte del flujo revisar primero.

Lo que no funcionó: etiquetas con cardinalidad sin control

La primera versión de esta métrica incluía el identificador del cliente como etiqueta, con la idea de poder filtrar por cliente específico si alguien reportaba un problema puntual:

Timer.builder("checkout.process.duration")
    .tag("customer_id", order.customerId())
    .tag("fraud_path", fraudPath)
    .register(registry);

Cada valor distinto de una etiqueta en Micrometer crea una serie temporal nueva en el backend de métricas. Con miles de clientes distintos, esa única línea multiplicó la cantidad de series activas por un factor enorme, y el colector empezó a descartar métricas y a consumir memoria de forma creciente hasta que lo reiniciamos. Revertimos esa etiqueta a los pocos días. El identificador de cliente sigue existiendo, pero como atributo de la traza distribuida, no como etiqueta de métrica: OpenTelemetry no agrega series por atributo de traza de la misma forma, así que ahí el costo de cardinalidad alta no aplica igual, y para encontrar el caso puntual de un cliente alcanza con buscar sus trazas, no con tener una métrica separada por cada uno.

Span currentSpan = tracer.currentSpan();
if (currentSpan != null) {
    currentSpan.tag("customer.id", order.customerId());
}

El criterio que quedó es simple de enunciar aunque a veces cueste aplicarlo: una etiqueta de métrica solo puede tener un conjunto acotado y conocido de valores posibles -un código de resultado, un booleano, un nombre de proveedor externo de una lista fija-. Cualquier valor que crezca con el número de usuarios, pedidos o entidades de negocio va a una traza, nunca a una etiqueta de métrica.

Conectar métricas y trazas con exemplars

El paso que cierra el ciclo es que, al ver un pico en el percentil 99 de checkout.process.duration, se pueda llegar directamente a una traza real que cayó en ese pico, sin tener que buscarla a mano por rango de tiempo. Eso es lo que hace un exemplar: un identificador de traza adjunto a un punto de la métrica exportada.

management:
  metrics:
    distribution:
      percentiles-histogram:
        checkout.process.duration: true

Con el histograma habilitado y el mismo colector recibiendo métricas y trazas, el backend puede asociar un bucket del histograma con trazas concretas que cayeron en ese rango de duración, siempre que el trace ID esté disponible en el contexto en el momento de registrar la métrica -lo cual ocurre automáticamente si la métrica se registra dentro del mismo hilo que procesa la request, dentro del span activo.

El criterio que aplicamos ahora

Antes de agregar una métrica nueva nos hacemos una pregunta: si esta métrica sube o baja de forma anómala, ¿alguien sabría qué mirar después? Una métrica de CPU alta no responde eso por sí sola; una métrica de duración de checkout separada por camino de fraude sí, porque indica directamente dónde seguir. Las métricas de infraestructura siguen ahí, porque hacen falta para saber si el sistema completo está bajo presión, pero dejaron de ser el primer lugar donde miramos cuando alguien pregunta por una request lenta en particular. El primer lugar ahora es la traza de esa request y la métrica de negocio que la agrupa con las que tuvieron el mismo problema.

spring-boot observabilidad micrometer opentelemetry
← Volver al blog