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.

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 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.
<!-- 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:
# 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: unrestrictedExposició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:
// 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.
// 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.
// 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
}
}# application.yml
# Enables TimedAspect, CountedAspect and the other Micrometer annotation aspects
management:
observations:
annotations:
enabled: trueDesde 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".
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.
// 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:
# 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-authorizedEl 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:
# 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: 3Con 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.
# 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 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 * 100jvm_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:
// 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:
{
"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.
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.
# 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.
# 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:
// 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:
# 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: 1800000Spring 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:
<!-- 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.
// 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();
}
}// 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
# 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íntoma | Causa | Solución |
|---|---|---|
/actuator/prometheus responde 404 | El endpoint no está en la lista de exposición, o falta el registry | Agregar 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 health | Abrir prometheus a la red del scraper o configurar basic auth en el scrape config |
@Timed no produce ninguna métrica | management.observations.annotations.enabled vale false por defecto, o falta AspectJ | Poner la propiedad en true y agregar spring-boot-starter-aspectj |
spring-boot-starter-aop no se encuentra después de actualizar | Renombrado en Spring Boot 4 | Usar spring-boot-starter-aspectj |
El import de Health o HealthIndicator falla en Boot 4 | Paquete movido a org.springframework.boot.health.contributor | Actualizar los imports |
Advertencias de deprecación en management.endpoint.*.enabled | Modelo de encendido y apagado reemplazado en Spring Boot 3.4 | Usar management.endpoints.access.default y management.endpoint.<id>.access |
| Los percentiles del dashboard difieren mucho de la realidad | Valores de publishPercentiles promediados entre pods | Usar 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
CounteryTimerexporta 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
SecurityFilterChaindedicado.
Más preguntas de este tipo están reunidas en el módulo de preguntas de entrevista sobre Spring Boot Actuator.
Fuentes
- Spring Boot 4.0 Migration Guide:
spring-boot-starter-aoprenombrado aspring-boot-starter-aspectj, probes habilitadas por defecto, nuevo módulospring-boot-health - Spring Boot 3.4 Release Notes:
enabled-by-defaultyendpoint.<id>.enableddeprecados en favor del modelo de acceso - Referencia de endpoints de Spring Boot Actuator: propiedades de acceso, paquete de
EndpointRequest, health indicators enorg.springframework.boot.health.contributor, grupos de probes y rutas adicionales - Referencia de métricas de Spring Boot:
DefaultServerRequestObservationConvention, métricas de HikariCP yjdbc.connections, deprecación de simpleclient - Referencia de observabilidad de Spring Boot:
management.observations.annotations.enabledy el requisito de AspectJ - Documentación de Micrometer para Prometheus: registry sobre el cliente Java de Prometheus 1.x y el módulo heredado simpleclient
- datasource-micrometer en GitHub: 2.x para Spring Boot 4, 1.x para Spring Boot 3
- Grafana: eliminación de AngularJS en Grafana 12: paneles Graph y Singlestat reemplazados por Time series y Stat
- Releases de Spring Boot: 4.1.2 como versión GA actual, 4.2.0-RC1 en pre-release
Lo que Hay que Retener de Spring Boot Micrometer y Prometheus
- Tres pasos para un scraping funcional: starter de Actuator,
micrometer-registry-prometheus,prometheusen la lista de exposición - Modelo de acceso desde la 3.4:
management.endpoints.access.default: nonemásread-onlypor endpoint reemplaza los flagsenableddeprecados - Cambios de Spring Boot 4:
spring-boot-starter-aspectj, API de salud enorg.springframework.boot.health.contributor,EndpointRequestenorg.springframework.boot.security.autoconfigure.actuate.web.servlet, probes activas por defecto - Las anotaciones necesitan un interruptor:
management.observations.annotations.enabled=true, sin beanTimedAspectmanual - Tags HTTP mediante la Observation API: extender
DefaultServerRequestObservationConvention, mantener labels de baja cardinalidad - Histogramas antes que percentiles del cliente:
publishPercentileHistogram()ysum by (le)en cadahistogram_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.
¿Sabrías detectar el bug en Spring Boot?
Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Escrito por
Anthony Fillion-MailletFundador 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
Compartir
Artículos relacionados

Observabilidad en Spring Boot 2026: OpenTelemetry, Tracing Distribuido y Preguntas de Entrevista
Guía completa sobre observabilidad en Spring Boot con OpenTelemetry y tracing distribuido. Configuración OTLP, Observation API, propagación de contexto y preguntas técnicas para entrevistas backend.

Logs estructurados en Spring Boot 2026: JSON para producción con Logback y OpenTelemetry
Guía completa de logs estructurados en Spring Boot 4.x. Soporte JSON nativo, starter OpenTelemetry, tracing MDC e integración ELK Stack para observabilidad en producción.

Trabajos freelance de desarrollador Java Spring Boot en 2026: dónde encontrar contratos, tarifas diarias y preparación
Guía completa sobre trabajos freelance de Java Spring Boot en 2026: plataformas para encontrar contratos, tarifas diarias en Europa, habilidades requeridas y preparación para entrevistas técnicas.