Volver a Spring Boot Intermedio

Actuator y Observabilidad en Spring Boot

Spring Boot Actuator expone endpoints listos para producción: verificaciones de salud, métricas e información de ejecución.

Dependencias

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>

Configuración de Actuator

management: endpoints: web: exposure: include: health,info,metrics,prometheus,loggers,caches,env base-path: /actuator endpoint: health: show-details: when-authorized show-components: always info: env: enabled: true

Endpoints Incorporados

EndpointDescripción
/actuator/healthEstado de salud de la aplicación
/actuator/metricsLista de métricas disponibles
/actuator/metrics/{nombre}Valor de una métrica específica
/actuator/envPropiedades del entorno
/actuator/loggersNiveles de log (soporte de cambio en tiempo real)
/actuator/prometheusMétricas en formato Prometheus
/actuator/infoInformación de la aplicación
/actuator/cachesEstadísticas de caché

Indicador de Salud Personalizado

@Component public class IndicadorSaludApiExterna implements HealthIndicator { private final RestTemplate restTemplate; @Override public Health health() { try { ResponseEntity<String> response = restTemplate .getForEntity("http://api-externa/ping", String.class); if (response.getStatusCode().is2xxSuccessful()) { return Health.up() .withDetail("url", "http://api-externa") .withDetail("estado", response.getStatusCode()) .build(); } return Health.down() .withDetail("estado", response.getStatusCode()) .build(); } catch (Exception e) { return Health.down() .withException(e) .withDetail("error", e.getMessage()) .build(); } } }

Métricas Personalizadas con Micrometer

@Service public class ServicioPedidos { private final Counter contadorPedidos; private final Timer timerProcesamiento; public ServicioPedidos(MeterRegistry registry, RepositorioPedidos repo) { this.contadorPedidos = Counter.builder("pedidos.creados.total") .description("Total de pedidos creados") .tag("entorno", "produccion") .register(registry); this.timerProcesamiento = Timer.builder("pedido.procesamiento.duracion") .description("Tiempo de procesamiento de pedidos") .register(registry); // Gauge refleja valor en vivo Gauge.builder("pedidos.pendientes.cantidad", repo, r -> r.contarPorEstado(Estado.PENDIENTE)) .description("Pedidos pendientes actuales") .register(registry); } public Pedido crearPedido(CrearPedidoRequest request) { contadorPedidos.increment(); return timerProcesamiento.record(() -> procesarPedido(request)); } }

Logging Estructurado

<dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.4</version> </dependency>
<!-- logback-spring.xml --> <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <includeMdc>true</includeMdc> <customFields>{"servicio":"servicio-productos","entorno":"prod"}</customFields> </encoder> </appender>
// MDC para correlación de peticiones @Component public class FiltroLoggingMdc extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Exception { String traceId = Optional.ofNullable(request.getHeader("X-Trace-ID")) .orElse(UUID.randomUUID().toString()); MDC.put("traceId", traceId); MDC.put("usuarioId", extraerUsuarioId(request)); try { chain.doFilter(request, response); } finally { MDC.clear(); } } }

Configuración Prometheus + Grafana

# docker-compose.yml services: prometheus: image: prom/prometheus volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml ports: - "9090:9090" grafana: image: grafana/grafana ports: - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=admin
# prometheus.yml scrape_configs: - job_name: 'spring-boot' metrics_path: '/actuator/prometheus' static_configs: - targets: ['host.docker.internal:8080'] scrape_interval: 15s

Información de la Aplicación

info: app: nombre: Servicio de Productos version: '@project.version@' descripcion: Gestiona el catálogo de productos contacto: equipo: equipo-backend email: [email protected]

Seguridad para Actuator

@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/health", "/actuator/info").permitAll() .requestMatchers("/actuator/**").hasRole("ADMIN") .anyRequest().authenticated()); return http.build(); }