Spring Boot Micrometer y Prometheus: Métricas de Actuator en Producción

Integración de Spring Boot Micrometer y Prometheus en Spring Boot 4: modelo de acceso de Actuator, scrape config, métricas HTTP, health probes y reglas de alerta.

Monitoreo Spring Boot Actuator con Micrometer y Prometheus

Spring Boot Actuator expone endpoints de salud, métricas y diagnóstico, y Micrometer es la fachada de métricas que los respalda. Agregar la dependencia micrometer-registry-prometheus habilita un endpoint /actuator/prometheus que Prometheus scrapea en un intervalo fijo. Los ejemplos de abajo apuntan a Spring Boot 4.1 (la versión GA actual, con la 4.2 en release candidate), Micrometer 1.16 y el cliente Java de Prometheus 1.x.

La respuesta corta

La integración de Spring Boot Micrometer con Prometheus se resuelve en tres pasos: agregar spring-boot-starter-actuator y micrometer-registry-prometheus, agregar prometheus a management.endpoints.web.exposure.include y apuntar un scrape_config de Prometheus a /actuator/prometheus. Las métricas de JVM, HTTP, HikariCP y del sistema se publican sin código adicional.

Configuración de Spring Boot Micrometer: Actuator y el registry de Prometheus

Dependencias Maven Requeridas

El starter de Actuator habilita los endpoints y trae Micrometer. El registry de Prometheus formatea cada meter en el formato de exposición de Prometheus. El starter de AspectJ solo hace falta para las anotaciones @Timed y @Counted.

xml
<!-- pom.xml -->
<!-- Actuator + Micrometer + Prometheus (Spring Boot 4.x) -->
<dependencies>
    <!-- Spring Boot Actuator: monitoring endpoints, includes Micrometer -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!-- Micrometer Registry Prometheus (Prometheus Java client 1.x) -->
    <!-- Exposes metrics in Prometheus format at /actuator/prometheus -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>

    <!-- AspectJ for @Timed and @Counted -->
    <!-- Spring Boot 4 renamed spring-boot-starter-aop to spring-boot-starter-aspectj -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aspectj</artifactId>
    </dependency>
</dependencies>

Hay dos trampas de versión que aparecen en tutoriales viejos. Desde Micrometer 1.13 (Spring Boot 3.3), micrometer-registry-prometheus está construido sobre el cliente Java de Prometheus 1.x; el código 0.x anterior vive en el módulo deprecado micrometer-registry-prometheus-simpleclient, que la documentación de Micrometer para Prometheus presenta solo como una solución transitoria. Y en Spring Boot 4, un pom.xml que todavía declara spring-boot-starter-aop ya no resuelve: la guía de migración de Spring Boot 4.0 lo renombra a spring-boot-starter-aspectj.

Configuración de Endpoints Actuator

Por defecto, solo el endpoint health se expone por HTTP. El endpoint /actuator/prometheus existe en cuanto el registry está en el classpath, pero responde 404 hasta que se agrega a la lista de exposición.

Spring Boot 3.4 reemplazó los interruptores de encendido y apagado (management.endpoints.enabled-by-default, management.endpoint.<id>.enabled) por un modelo de acceso, y las propiedades anteriores quedaron deprecadas. El patrón opt-in fija el acceso por defecto en none y otorga read-only endpoint por endpoint:

yaml
# application.yml
# Actuator configuration for production (Spring Boot 3.4+ access model)
management:
  endpoints:
    # Every endpoint is inaccessible unless granted below
    access:
      default: none
    web:
      exposure:
        # Endpoints exposed over HTTP
        # health, info, prometheus are the minimum for monitoring
        include: health,info,prometheus,metrics,env,loggers
      base-path: /actuator
  endpoint:
    health:
      access: read-only
      show-details: when-authorized
      show-components: when-authorized
    info:
      access: read-only
    prometheus:
      access: read-only
    metrics:
      access: read-only
    env:
      access: read-only
      # Mask sensitive values for anonymous callers
      show-values: when-authorized
    loggers:
      # unrestricted allows POST to change a log level at runtime
      access: unrestricted

Exposición y acceso son dos controles separados. exposure.include decide si un endpoint se mapea por HTTP; access decide qué permite el endpoint una vez mapeado. Un archivo de propiedades que todavía usa enabled: true sigue funcionando con una advertencia de deprecación, y ese es el momento de migrar.

La opción show-details: when-authorized muestra los detalles de salud solo a usuarios autenticados con el rol adecuado. Los endpoints en sí se protegen con un SecurityFilterChain dedicado:

ActuatorSecurityConfig.javajava
// Securing Actuator endpoints (Spring Boot 4, Spring Security 7)
package com.example.monitoring.config;

import static org.springframework.security.web.access.IpAddressAuthorizationManager.hasIpAddress;

import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration(proxyBeanMethods = false)
public class ActuatorSecurityConfig {

    @Bean
    SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
            .securityMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests(auth -> auth
                // Health and info public for load balancers
                .requestMatchers(EndpointRequest.to("health", "info")).permitAll()
                // Prometheus accessible from the internal network only
                .requestMatchers(EndpointRequest.to("prometheus")).access(hasIpAddress("10.0.0.0/8"))
                // Other endpoints restricted to admins
                .anyRequest().hasRole("ACTUATOR_ADMIN")
            )
            .httpBasic(basic -> {})
            .build();
    }
}

En Spring Boot 4, EndpointRequest se movió a org.springframework.boot.security.autoconfigure.actuate.web.servlet, como muestra la referencia de endpoints de Actuator. El DSL Java AuthorizedUrl de authorizeHttpRequests no tiene un método hasIpAddress: la regla por IP pasa por IpAddressAuthorizationManager.hasIpAddress, disponible desde Spring Security 6.3.

Métricas Personalizadas con Micrometer

Contadores, Gauges y Timers Aplicativos

Micrometer ofrece varios tipos de métricas adaptados a casos de uso distintos. Los contadores miden eventos acumulativos, los gauges valores instantáneos y los timers la duración de una operación.

OrderMetricsService.javajava
// Custom business metrics service
package com.example.monitoring.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

@Service
public class OrderMetricsService {

    // Counter for orders created
    private final Counter ordersCreatedCounter;
    // Timer to measure processing duration
    private final Timer orderProcessingTimer;
    // Atomic value for pending orders gauge
    private final AtomicInteger pendingOrdersCount = new AtomicInteger(0);

    public OrderMetricsService(MeterRegistry registry) {
        // Exported as orders_created_total: the _total suffix is added by the registry
        this.ordersCreatedCounter = Counter.builder("orders.created")
            .description("Total number of orders created")
            .tag("channel", "web")
            .register(registry);

        // Timer with histogram buckets: percentiles computed server-side in PromQL
        this.orderProcessingTimer = Timer.builder("orders.processing.duration")
            .description("Order processing duration")
            .publishPercentileHistogram()
            .register(registry);

        // Gauge linked to atomic value
        // Read on each scrape
        Gauge.builder("orders.pending.count", pendingOrdersCount, AtomicInteger::get)
            .description("Number of orders pending processing")
            .register(registry);
    }

    public void recordOrderCreated() {
        ordersCreatedCounter.increment();
        pendingOrdersCount.incrementAndGet();
    }

    public void recordOrderProcessed(Runnable processingLogic) {
        // Measures execution duration
        orderProcessingTimer.record(processingLogic);
        pendingOrdersCount.decrementAndGet();
    }

    public <T> T recordOrderProcessedWithResult(Supplier<T> processingLogic) {
        return orderProcessingTimer.record(processingLogic);
    }
}

Con el registry de Prometheus importan dos detalles de nomenclatura. Los nombres de meters de Micrometer usan puntos y se convierten a snake case (orders.processing.duration pasa a ser orders_processing_duration_seconds). Los contadores reciben el sufijo _total automáticamente, así que nombrar un contador orders.created.total solo agrega ruido.

publishPercentiles(0.5, 0.95, 0.99) calcula los percentiles dentro de cada JVM, y esos valores no se pueden agregar entre instancias. publishPercentileHistogram() publica buckets que histogram_quantile() agrega sobre toda la flota, y es la opción correcta detrás de Prometheus.

Anotaciones @Timed y @Counted

Para evitar código repetitivo, Micrometer ofrece anotaciones que instrumentan los métodos a través de AspectJ.

PaymentService.javajava
// Automatic instrumentation with annotations
package com.example.monitoring.service;

import io.micrometer.core.annotation.Counted;
import io.micrometer.core.annotation.Timed;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private final PaymentValidator paymentValidator;

    public PaymentService(PaymentValidator paymentValidator) {
        this.paymentValidator = paymentValidator;
    }

    // @Timed creates a Timer
    // Measures each call and publishes count, sum, max and histogram buckets
    @Timed(
        value = "payment.process.duration",
        description = "Payment processing duration",
        histogram = true
    )
    public PaymentResult processPayment(PaymentRequest request) {
        // Call through another bean: the proxy intercepts it
        paymentValidator.validate(request);
        return executePayment(request);
    }

    // @Counted increments a counter on each call
    // Useful for discrete events
    @Counted(
        value = "payment.refunds",
        description = "Total number of refunds"
    )
    public void refundPayment(String transactionId) {
        // Refund logic
    }
}
yaml
# application.yml
# Enables TimedAspect, CountedAspect and the other Micrometer annotation aspects
management:
  observations:
    annotations:
      enabled: true

Desde Spring Boot 3.2 ya no hace falta declarar a mano los beans TimedAspect y CountedAspect: configurar management.observations.annotations.enabled=true con spring-boot-starter-aspectj en el classpath los autoconfigura. La propiedad vale false por defecto, lo que explica la mayoría de los reportes del tipo "mi @Timed no hace nada".

Limitación AOP

Las anotaciones @Timed y @Counted solo funcionan en métodos públicos de beans de Spring invocados a través del proxy. Una llamada de un método a otro dentro de la misma clase evita el proxy y no se instrumenta, y una anotación sobre un método privado se ignora. Anotar un controlador de Spring MVC que ya está instrumentado produce observaciones duplicadas.

¿Listo para aprobar tus entrevistas de Spring Boot?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Endpoints de Salud Personalizados

Health Indicators de Negocio

Los Health Indicators verifican el estado de las dependencias externas y de los componentes críticos del negocio. Spring Boot ya incluye indicadores predeterminados para bases de datos, Redis y otros servicios habituales. En Spring Boot 4, la API de salud vive en su propio módulo spring-boot-health, y Health y HealthIndicator pasaron de org.springframework.boot.actuate.health a org.springframework.boot.health.contributor.

PaymentGatewayHealthIndicator.javajava
// Health indicator for payment gateway (Spring Boot 4 packages)
package com.example.monitoring.health;

import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

import java.time.Duration;
import java.time.Instant;

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final RestClient restClient;
    private final String gatewayHealthUrl;

    public PaymentGatewayHealthIndicator(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.build();
        this.gatewayHealthUrl = "https://api.payment-gateway.com/health";
    }

    @Override
    public Health health() {
        Instant start = Instant.now();

        try {
            // Call gateway health endpoint
            var response = restClient.get()
                .uri(gatewayHealthUrl)
                .retrieve()
                .toBodilessEntity();

            Duration responseTime = Duration.between(start, Instant.now());

            if (response.getStatusCode().is2xxSuccessful()) {
                return Health.up()
                    .withDetail("responseTime", responseTime.toMillis() + "ms")
                    .withDetail("statusCode", response.getStatusCode().value())
                    .build();
            } else {
                return Health.down()
                    .withDetail("statusCode", response.getStatusCode().value())
                    .withDetail("reason", "Unexpected status code")
                    .build();
            }
        } catch (Exception e) {
            Duration responseTime = Duration.between(start, Instant.now());

            return Health.down()
                .withDetail("error", e.getClass().getSimpleName())
                .withDetail("message", e.getMessage())
                .withDetail("responseTime", responseTime.toMillis() + "ms")
                .build();
        }
    }
}

Este indicador aparece en /actuator/health bajo el nombre paymentGateway: el nombre del bean sin el sufijo HealthIndicator.

Grupos de Salud para Kubernetes

Spring Boot 4 habilita las probes de liveness y readiness por defecto, en todas las plataformas y no solo cuando detecta Kubernetes. Los grupos /actuator/health/liveness y /actuator/health/readiness existen sin ninguna configuración, y declarar a mano beans LivenessStateHealthIndicator o ReadinessStateHealthIndicator es innecesario. La configuración solo hace falta para agregar componentes a un grupo o para definir grupos personalizados:

yaml
# application.yml
# Health groups configuration for Kubernetes
management:
  endpoint:
    health:
      probes:
        # true by default in Spring Boot 4
        enabled: true
        # Also serve the groups at /livez and /readyz on the main server port
        add-additional-paths: true
      group:
        # Readiness: can the application receive traffic?
        readiness:
          include: readinessState,db
        # Custom group for critical dependencies
        critical:
          include: db,paymentGateway
          show-details: when-authorized

El grupo de liveness debe limitarse a livenessState. Agregar una base de datos o un gateway externo a liveness hace que Kubernetes reinicie todos los pods cuando esa dependencia cae, lo que convierte una caída parcial en una caída total. Las dependencias externas van en readiness, y solo cuando la aplicación no puede atender ninguna solicitud sin ellas.

Las probes de Kubernetes apuntan entonces a los endpoints dedicados:

yaml
# kubernetes-deployment.yml
# Kubernetes probes configuration
spec:
  containers:
    - name: order-service
      livenessProbe:
        httpGet:
          path: /livez
          port: 8080
        initialDelaySeconds: 30
        periodSeconds: 10
        failureThreshold: 3
      readinessProbe:
        httpGet:
          path: /readyz
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 5
        failureThreshold: 3

Con add-additional-paths: true, las probes siguen en el puerto de la aplicación incluso cuando Actuator corre en un puerto de administración separado, así que la probe detecta un servidor principal caído.

Integración de Spring Boot con Prometheus y Grafana

Configuración de Scraping Prometheus

Prometheus recolecta las métricas consultando periódicamente el endpoint /actuator/prometheus. La configuración de scraping define los objetivos y la ruta de métricas, ya que Prometheus usa /metrics por defecto.

yaml
# prometheus.yml
# Prometheus configuration for Spring Boot
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'spring-boot-apps'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 10s
    static_configs:
      - targets:
          - 'order-service:9090'
          - 'payment-service:9090'
          - 'inventory-service:9090'
    # Relabeling to strip the port from the instance label
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
        regex: '([^:]+):\d+'
        replacement: '${1}'

  # Kubernetes service discovery
  - job_name: 'kubernetes-pods'
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      # Only scrape pods with annotation
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__
        regex: (.+)

Los objetivos usan el puerto 9090 porque la configuración de producción más abajo mueve Actuator a un puerto de administración separado. Sin management.server.port, el objetivo del scraping es el puerto de la aplicación.

Métricas de Spring Boot Actuator Publicadas por Defecto

Actuator con Micrometer expone métricas de JVM, proceso, HikariCP y HTTP sin escribir código. Estas consultas PromQL cubren las que se vigilan primero en producción:

promql
# PromQL queries for JVM monitoring

# Heap memory usage, summed across heap pools
sum by (instance) (jvm_memory_used_bytes{area="heap"})

# Heap usage percentage
sum by (instance) (jvm_memory_used_bytes{area="heap"})
  / sum by (instance) (jvm_memory_max_bytes{area="heap"}) * 100

# Live threads
jvm_threads_live_threads

# Garbage collection: seconds spent paused per second
rate(jvm_gc_pause_seconds_sum[5m])

# GC pauses per minute
rate(jvm_gc_pause_seconds_count[1m]) * 60

# CPU used by the JVM process
process_cpu_usage

# Active database connections
hikaricp_connections_active

# Connection pool utilization
hikaricp_connections_active / hikaricp_connections_max * 100

jvm_memory_used_bytes{area="heap"} devuelve una serie por pool de memoria (Eden, Survivor, Old Gen con G1). Dividirla por el máximo sin sum by (instance) produce ratios por pool que se ven mal en un dashboard.

Un bean MeterBinder agrega gauges a nivel de aplicación que las métricas por defecto no cubren:

CustomJvmMetrics.javajava
// Additional JVM metrics
package com.example.monitoring.metrics;

import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.stereotype.Component;

import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;

@Component
public class CustomJvmMetrics implements MeterBinder {

    @Override
    public void bindTo(MeterRegistry registry) {
        OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();

        // System load average
        Gauge.builder("system.load.average", osBean, OperatingSystemMXBean::getSystemLoadAverage)
            .description("System load average over 1 minute")
            .register(registry);

        // Available processors count
        Gauge.builder("system.cpu.count", osBean, OperatingSystemMXBean::getAvailableProcessors)
            .description("Number of available processors")
            .register(registry);

        // Application uptime
        Gauge.builder("application.uptime.seconds",
                ManagementFactory.getRuntimeMXBean(),
                bean -> bean.getUptime() / 1000.0)
            .description("Application uptime in seconds")
            .register(registry);
    }
}

system.cpu.count y process.uptime ya los publican los binders integrados de Micrometer. El ejemplo muestra el patrón, y un binder real solo debería registrar lo que falta en las métricas por defecto.

Dashboards Grafana Listos para Usar

Grafana ofrece dashboards de la comunidad para Spring Boot que funcionan con las métricas estándar de Actuator. Un dashboard escrito a mano usa los tipos de panel timeseries y stat:

json
{
  "annotations": {
    "list": []
  },
  "panels": [
    {
      "title": "Request Rate",
      "type": "timeseries",
      "targets": [
        {
          "expr": "sum by (method, uri, status) (rate(http_server_requests_seconds_count{application=\"$application\"}[5m]))",
          "legendFormat": "{{method}} {{uri}} - {{status}}"
        }
      ]
    },
    {
      "title": "Response Time P99",
      "type": "timeseries",
      "targets": [
        {
          "expr": "histogram_quantile(0.99, sum by (le, method, uri) (rate(http_server_requests_seconds_bucket{application=\"$application\"}[5m])))",
          "legendFormat": "{{method}} {{uri}}"
        }
      ]
    },
    {
      "title": "Error Rate",
      "type": "stat",
      "targets": [
        {
          "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\",status=~\"5..\"}[5m])) / sum(rate(http_server_requests_seconds_count{application=\"$application\"}[5m])) * 100"
        }
      ]
    }
  ]
}

Muchos dashboards copiados de tutoriales viejos todavía usan los tipos de panel graph y singlestat. Singlestat se eliminó en Grafana 8, y Grafana 12 eliminó por completo el soporte de AngularJS, por lo que esos paneles solo se renderizan después de migrarlos a timeseries y stat.

Importación en Grafana

Para importar un dashboard de la comunidad: Grafana > Dashboards > New > Import, y luego ingresar el ID 4701 (JVM Micrometer) o 12900 (SpringBoot APM). Conviene revisar los tipos de panel después de importar: las revisiones antiguas de los dashboards de la comunidad pueden contener todavía paneles Angular que Grafana 12 tiene que migrar.

Alertas con Prometheus

Reglas de Alerta Esenciales

Las reglas de alerta de Prometheus disparan notificaciones cuando las métricas superan umbrales críticos.

yaml
# alerting-rules.yml
# Alert rules for Spring Boot applications
groups:
  - name: spring-boot-alerts
    rules:
      # Alert if application is down
      - alert: ApplicationDown
        expr: up{job="spring-boot-apps"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Application {{ $labels.instance }} is down"
          description: "{{ $labels.instance }} has been down for more than 1 minute"

      # Alert on HTTP error rate
      - alert: HighErrorRate
        expr: |
          sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) by (application)
          /
          sum(rate(http_server_requests_seconds_count[5m])) by (application)
          > 0.05
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High error rate on {{ $labels.application }}"
          description: "Error rate is {{ $value | humanizePercentage }}"

      # Alert on P99 latency, aggregated per application
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            sum by (le, application) (rate(http_server_requests_seconds_bucket[5m]))
          ) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High latency on {{ $labels.application }}"
          description: "P99 latency is {{ $value | humanizeDuration }}"

      # Heap memory alert
      - alert: HighHeapUsage
        expr: |
          sum by (instance) (jvm_memory_used_bytes{area="heap"})
          / sum by (instance) (jvm_memory_max_bytes{area="heap"})
          > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High heap memory usage on {{ $labels.instance }}"
          description: "Heap usage is at {{ $value | humanizePercentage }}"

      # Database connection pool exhausted alert
      - alert: DatabaseConnectionPoolExhausted
        expr: |
          hikaricp_connections_active
          / hikaricp_connections_max
          > 0.9
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Database connection pool nearly exhausted"
          description: "{{ $value | humanizePercentage }} of connections in use"

      # Excessive GC alert
      - alert: HighGCPause
        expr: |
          rate(jvm_gc_pause_seconds_sum[5m])
          / rate(jvm_gc_pause_seconds_count[5m])
          > 0.5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High GC pause time"
          description: "Average GC pause is {{ $value | humanizeDuration }}"

Estas alertas cubren los problemas de producción más comunes: disponibilidad, rendimiento y recursos. La regla HighLatency necesita sum by (le, ...): sin él, histogram_quantile calcula un P99 por serie (por URI, método y status), y un solo endpoint lento con tres llamadas dispara la alerta.

Métricas HTTP y de Base de Datos en Spring Boot

Instrumentación Automática de Solicitudes HTTP

Spring Boot instrumenta cada solicitud entrante de Spring MVC y WebFlux mediante la Observation API y publica el timer http.server.requests, etiquetado con method, uri, status, outcome y exception.

yaml
# application.yml
# HTTP metrics configuration
management:
  metrics:
    distribution:
      # Histogram buckets for server-side percentiles
      percentiles-histogram:
        http.server.requests: true
      # SLO buckets: count requests faster than each boundary
      slo:
        http.server.requests: 100ms, 500ms, 1s, 2s
    tags:
      # Common tags added to every meter
      application: ${spring.application.name}
      environment: ${spring.profiles.active:default}

Las interfaces WebMvcTagsContributor y WebMvcTagsProvider que aparecen en muchos ejemplos se eliminaron en Spring Boot 3.0. Los tags personalizados ahora se agregan extendiendo DefaultServerRequestObservationConvention, como documenta la referencia de métricas de Spring Boot:

TenantObservationConvention.javajava
// Adds custom tags to http.server.requests (Spring Boot 3.0+)
package com.example.monitoring.config;

import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.observation.ServerRequestObservationContext;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;

@Component
public class TenantObservationConvention extends DefaultServerRequestObservationConvention {

    @Override
    public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
        HttpServletRequest request = context.getCarrier();
        String userId = request.getHeader("X-User-Id");
        String tenantId = request.getHeader("X-Tenant-Id");

        // Keep the default tags and add two bounded ones
        return super.getLowCardinalityKeyValues(context).and(
            KeyValue.of("user.type", userId != null ? "authenticated" : "anonymous"),
            KeyValue.of("tenant", tenantId != null ? tenantId : "default")
        );
    }
}

Solo los key-values de baja cardinalidad se convierten en labels de Prometheus. Un tag tenant es aceptable con unas pocas decenas de tenants y se vuelve un problema de cardinalidad con miles. Los valores de alta cardinalidad (ID de usuario, ID de pedido) van en getHighCardinalityKeyValues, que solo alimenta las trazas. El lado de tracing de la Observation API se detalla en observabilidad de Spring Boot con OpenTelemetry.

Métricas HikariCP y Consultas SQL

Las métricas del pool de HikariCP se exponen automáticamente con el prefijo hikaricp, etiquetadas por nombre de pool, junto con los gauges genéricos jdbc.connections. Definir el nombre del pool hace que el tag sea legible:

yaml
# application.yml
# HikariCP pool configuration
spring:
  datasource:
    hikari:
      # Becomes the "pool" tag on hikaricp_* metrics
      pool-name: OrderServicePool
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

Spring Boot no cubre el tiempo a nivel de consulta. El proyecto datasource-micrometer envuelve el DataSource y crea una observación por conexión, consulta y result set, lo que produce tanto métricas como spans de traza. Su línea 2.x apunta a Spring Boot 4 y la línea 1.x a Spring Boot 3:

xml
<!-- pom.xml -->
<!-- JDBC observations: 2.x for Spring Boot 4, 1.x for Spring Boot 3 -->
<dependency>
    <groupId>net.ttddyy.observation</groupId>
    <artifactId>datasource-micrometer-spring-boot</artifactId>
    <version>2.0.1</version>
</dependency>

Esto reemplaza el enfoque manual de envolver el DataSource en un ProxyDataSourceBuilder con un listener escrito a mano, que evitaba la autoconfiguración de DataSource de Spring Boot y había que mantener sincronizado con cada actualización de Boot. Las consultas lentas suelen indicar un patrón N+1, tratado en soluciones al problema N+1 en Spring Data JPA.

Mejores Prácticas para Producción

Cardinalidad de las Métricas

Una cardinalidad excesiva degrada el rendimiento de Prometheus. Cada combinación única de tags genera una serie temporal distinta.

AntiPatternHighCardinality.javajava
// AVOID: unbounded cardinality
package com.example.monitoring.antipattern;

@Service
public class AntiPatternHighCardinality {

    private final MeterRegistry registry;

    // BAD: userId creates one series per user
    public void trackUserAction(String userId, String action) {
        Counter.builder("user.actions")
            .tag("userId", userId)  // Millions of possible values
            .tag("action", action)
            .register(registry)
            .increment();
    }
}
GoodPracticeCardinality.javajava
// Controlled cardinality
package com.example.monitoring.bestpractice;

@Service
public class GoodPracticeCardinality {

    private final MeterRegistry registry;

    // GOOD: user category instead of ID
    public void trackUserAction(User user, String action) {
        Counter.builder("user.actions")
            .tag("userType", user.getSubscriptionType())  // FREE, PREMIUM, ENTERPRISE
            .tag("action", action)
            .register(registry)
            .increment();
    }

    // GOOD: let a timer histogram bucket durations
    public void trackResponseTime(long responseTimeMs) {
        Timer.builder("response.time")
            .publishPercentileHistogram()
            .register(registry)
            .record(Duration.ofMillis(responseTimeMs));
    }
}

Un Timer con buckets de histograma responde mejor las preguntas de latencia que un contador con un tag bucket hecho a mano: PromQL puede calcular cualquier percentil a partir de los buckets, y los límites de los buckets se mantienen consistentes entre servicios.

Configuración Lista para Producción

yaml
# application-production.yml
# Production configuration (Spring Boot 4)
management:
  server:
    # Separate port for management endpoints, not exposed publicly
    port: 9090
  endpoints:
    access:
      default: none
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      access: read-only
      show-details: when-authorized
      probes:
        add-additional-paths: true
    info:
      access: read-only
    prometheus:
      access: read-only
  observations:
    annotations:
      enabled: true
  metrics:
    distribution:
      percentiles-histogram:
        http.server.requests: true
      minimum-expected-value:
        http.server.requests: 1ms
      maximum-expected-value:
        http.server.requests: 30s
    tags:
      application: ${spring.application.name}
      environment: production
      version: ${app.version:unknown}

Con access.default: none, endpoints como env, beans, configprops y mappings quedan inaccesibles sin tener que listarlos uno por uno. management.prometheus.metrics.export.enabled vale true por defecto, y un ajuste step no tiene efecto en Prometheus: es un registry basado en pull, y el intervalo de scraping se define en prometheus.yml.

Errores Comunes con Actuator y Prometheus

SíntomaCausaSolución
/actuator/prometheus responde 404El endpoint no está en la lista de exposición, o falta el registryAgregar prometheus a management.endpoints.web.exposure.include y verificar que micrometer-registry-prometheus esté en el classpath
El target de Prometheus muestra "server returned HTTP status 401"Spring Security protege todos los endpoints excepto healthAbrir prometheus a la red del scraper o configurar basic auth en el scrape config
@Timed no produce ninguna métricamanagement.observations.annotations.enabled vale false por defecto, o falta AspectJPoner la propiedad en true y agregar spring-boot-starter-aspectj
spring-boot-starter-aop no se encuentra después de actualizarRenombrado en Spring Boot 4Usar spring-boot-starter-aspectj
El import de Health o HealthIndicator falla en Boot 4Paquete movido a org.springframework.boot.health.contributorActualizar los imports
Advertencias de deprecación en management.endpoint.*.enabledModelo de encendido y apagado reemplazado en Spring Boot 3.4Usar management.endpoints.access.default y management.endpoint.<id>.access
Los percentiles del dashboard difieren mucho de la realidadValores de publishPercentiles promediados entre podsUsar publishPercentileHistogram() y histogram_quantile()

Preguntas de Entrevista sobre Spring Boot Micrometer

Actuator aparece en la mayoría de las entrevistas de Spring Boot para puestos semi-senior y senior. Las respuestas que separan a un perfil con experiencia de uno junior:

  • Diferencia entre liveness y readiness: liveness le indica a Kubernetes que reinicie el pod, readiness le indica que deje de enrutarle tráfico. Un candidato que pone la base de datos en el grupo de liveness nunca lo vivió durante una caída.
  • Por qué Micrometer y no el cliente de Prometheus directamente: Micrometer es una fachada. El mismo código de Counter y Timer exporta a Prometheus, Datadog u OTLP cambiando una dependencia, y la instrumentación propia de Spring (HTTP, pool JDBC, cachés) está escrita sobre él.
  • Percentiles del lado del cliente frente a histogramas: los percentiles del lado del cliente no se pueden agregar entre instancias, los histogramas sí. Es la pregunta trampa más común sobre el tema.
  • Qué significa la cardinalidad y cómo rompe Prometheus: una serie temporal por cada combinación única de labels; un tag con el ID de usuario multiplica el uso de memoria por la cantidad de usuarios.
  • Cómo proteger Actuator: limitar la exposición, fijar niveles de acceso, correr la administración en un puerto separado y protegerla con un SecurityFilterChain dedicado.

Más preguntas de este tipo están reunidas en el módulo de preguntas de entrevista sobre Spring Boot Actuator.

Fuentes

Lo que Hay que Retener de Spring Boot Micrometer y Prometheus

  • Tres pasos para un scraping funcional: starter de Actuator, micrometer-registry-prometheus, prometheus en la lista de exposición
  • Modelo de acceso desde la 3.4: management.endpoints.access.default: none más read-only por endpoint reemplaza los flags enabled deprecados
  • Cambios de Spring Boot 4: spring-boot-starter-aspectj, API de salud en org.springframework.boot.health.contributor, EndpointRequest en org.springframework.boot.security.autoconfigure.actuate.web.servlet, probes activas por defecto
  • Las anotaciones necesitan un interruptor: management.observations.annotations.enabled=true, sin bean TimedAspect manual
  • Tags HTTP mediante la Observation API: extender DefaultServerRequestObservationConvention, mantener labels de baja cardinalidad
  • Histogramas antes que percentiles del cliente: publishPercentileHistogram() y sum by (le) en cada histogram_quantile
  • Liveness se queda local: las dependencias externas van en readiness o en un grupo personalizado, nunca en liveness

Estos ajustes le dan a un servicio Spring Boot 4 métricas que se agregan correctamente entre pods, endpoints que no filtran nada a la red pública y alertas que se disparan ante incidentes reales y no por una única URI lenta.

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Reto diario

¿Sabrías detectar el bug en Spring Boot?

Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador de SharpSkill

Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.

Actualizado el 26 de octubre de 2026

Etiquetas

#spring boot actuator
#micrometer
#prometheus
#monitoring
#observability

Compartir

Artículos relacionados