Spring Boot Micrometer e Prometheus: Métricas do Actuator em Produção

Integração do Spring Boot Micrometer com Prometheus no Spring Boot 4: modelo de acesso do Actuator, scrape config, métricas HTTP, health probes e regras de alerta.

Monitoramento Spring Boot Actuator com Micrometer e Prometheus

O Spring Boot Actuator expõe endpoints de saúde, métricas e diagnóstico, e o Micrometer é a fachada de métricas por trás deles. Adicionar a dependência micrometer-registry-prometheus disponibiliza um endpoint /actuator/prometheus que o Prometheus coleta em um intervalo fixo. Os exemplos abaixo usam Spring Boot 4.1 (a versão GA atual, com a 4.2 em release candidate), Micrometer 1.16 e o cliente Java do Prometheus 1.x.

A resposta curta

A integração do Spring Boot Micrometer com o Prometheus leva três passos: adicionar spring-boot-starter-actuator e micrometer-registry-prometheus, incluir prometheus em management.endpoints.web.exposure.include e apontar um scrape_config do Prometheus para /actuator/prometheus. As métricas de JVM, HTTP, HikariCP e sistema são publicadas sem código extra.

Configuração do Spring Boot Micrometer: Actuator e o Registry do Prometheus

Dependências Maven Necessárias

O starter do Actuator habilita os endpoints e traz o Micrometer. O registry do Prometheus formata cada meter no formato de exposição do Prometheus. O starter do AspectJ só é necessário para as anotações @Timed e @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>

Duas armadilhas de versão aparecem em tutoriais antigos. Desde o Micrometer 1.13 (Spring Boot 3.3), o micrometer-registry-prometheus é construído sobre o cliente Java do Prometheus 1.x; o código 0.x antigo fica no módulo depreciado micrometer-registry-prometheus-simpleclient, que a documentação do Micrometer para Prometheus apresenta apenas como solução provisória. E no Spring Boot 4, um pom.xml que ainda declara spring-boot-starter-aop deixa de resolver: o guia de migração do Spring Boot 4.0 o renomeia para spring-boot-starter-aspectj.

Configuração dos Endpoints do Actuator

Por padrão, apenas o endpoint health é exposto via HTTP. O endpoint /actuator/prometheus existe assim que o registry está no classpath, mas retorna 404 até ser adicionado à lista de exposição.

O Spring Boot 3.4 substituiu as chaves liga/desliga (management.endpoints.enabled-by-default, management.endpoint.<id>.enabled) por um modelo de acesso, e as propriedades antigas foram depreciadas. O padrão opt-in define o acesso padrão como none e concede 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

Exposição e acesso são dois controles separados. exposure.include decide se um endpoint é mapeado via HTTP; access decide o que o endpoint permite depois de mapeado. Um arquivo de propriedades que ainda usa enabled: true continua funcionando com um aviso de depreciação, e esse é o momento de migrar.

A opção show-details: when-authorized mostra os detalhes de saúde apenas para usuários autenticados com o papel adequado. Os endpoints em si são protegidos por um 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();
    }
}

No Spring Boot 4, o EndpointRequest foi movido para org.springframework.boot.security.autoconfigure.actuate.web.servlet, como mostra a referência de endpoints do Actuator. A DSL Java AuthorizedUrl de authorizeHttpRequests não tem método hasIpAddress: a regra por IP passa por IpAddressAuthorizationManager.hasIpAddress, disponível desde o Spring Security 6.3.

Métricas Customizadas com Micrometer

Counters, Gauges e Timers Aplicacionais

O Micrometer oferece diversos tipos de métricas adequados a casos de uso distintos. Counters medem eventos cumulativos, gauges medem valores instantâneos e timers medem a duração de operações.

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);
    }
}

Dois detalhes de nomenclatura importam com o registry do Prometheus. Os nomes de meters do Micrometer usam pontos e são convertidos para snake case (orders.processing.duration vira orders_processing_duration_seconds). Os counters recebem o sufixo _total automaticamente, então nomear um counter como orders.created.total só adiciona ruído.

publishPercentiles(0.5, 0.95, 0.99) calcula os percentis dentro de cada JVM, e esses valores não podem ser agregados entre instâncias. publishPercentileHistogram() publica buckets que o histogram_quantile() agrega sobre toda a frota, e é a opção certa quando o backend é o Prometheus.

Anotações @Timed e @Counted

Para evitar código repetitivo, o Micrometer disponibiliza anotações que instrumentam métodos via 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 o Spring Boot 3.2, declarar manualmente os beans TimedAspect e CountedAspect não é mais necessário: definir management.observations.annotations.enabled=true com spring-boot-starter-aspectj no classpath faz a autoconfiguração deles. A propriedade tem false como padrão, o que explica a maioria dos relatos do tipo "meu @Timed não faz nada".

Limitação do AOP

As anotações @Timed e @Counted só funcionam em métodos públicos de beans Spring chamados através do proxy. Uma chamada de um método para outro na mesma classe ignora o proxy e não é instrumentada, e uma anotação em método privado é ignorada. Anotar um controller Spring MVC que já é instrumentado gera observações duplicadas.

Pronto para mandar bem nas entrevistas de Spring Boot?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Endpoints de Saúde Customizados

Health Indicators de Negócio

Os Health Indicators verificam o estado das dependências externas e dos componentes críticos do negócio. O Spring Boot já oferece indicadores padrão para bancos de dados, Redis e outros serviços comuns. No Spring Boot 4, a API de saúde fica em um módulo próprio, spring-boot-health, e Health e HealthIndicator passaram de org.springframework.boot.actuate.health para 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();
        }
    }
}

Esse indicador aparece em /actuator/health sob o nome paymentGateway: o nome do bean sem o sufixo HealthIndicator.

Grupos de Saúde para Kubernetes

O Spring Boot 4 habilita as probes de liveness e readiness por padrão, em todas as plataformas e não apenas quando detecta o Kubernetes. Os grupos /actuator/health/liveness e /actuator/health/readiness existem sem nenhuma configuração, e declarar manualmente beans LivenessStateHealthIndicator ou ReadinessStateHealthIndicator é desnecessário. A configuração só é necessária para adicionar componentes a um grupo ou definir grupos customizados:

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

O grupo de liveness deve ficar limitado a livenessState. Colocar um banco de dados ou um gateway externo na liveness faz o Kubernetes reiniciar todos os pods quando essa dependência cai, o que transforma uma indisponibilidade parcial em total. Dependências externas ficam na readiness, e só quando a aplicação não consegue atender nenhuma requisição sem elas.

As probes do Kubernetes então apontam para os 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

Com add-additional-paths: true, as probes continuam na porta da aplicação mesmo quando o Actuator roda em uma porta de gerenciamento separada, de modo que a probe detecta um servidor principal com falha.

Integração do Spring Boot com Prometheus e Grafana

Configuração do Scraping no Prometheus

O Prometheus coleta as métricas consultando periodicamente o endpoint /actuator/prometheus. A configuração de scraping define os alvos e o caminho das métricas, já que o Prometheus usa /metrics por padrão.

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: (.+)

Os alvos usam a porta 9090 porque a configuração de produção mais abaixo move o Actuator para uma porta de gerenciamento separada. Sem management.server.port, o alvo do scraping é a porta da aplicação.

Métricas do Spring Boot Actuator Publicadas por Padrão

O Actuator com Micrometer expõe métricas de JVM, processo, HikariCP e HTTP sem nenhum código. Estas consultas PromQL cobrem as que são monitoradas primeiro em produção:

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"} retorna uma série por pool de memória (Eden, Survivor, Old Gen com G1). Dividir pelo máximo sem sum by (instance) produz razões por pool que parecem erradas em um dashboard.

Um bean MeterBinder adiciona gauges de nível de aplicação que as métricas padrão não cobrem:

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 e process.uptime já são publicados pelos binders nativos do Micrometer. O exemplo mostra o padrão, e um binder real deve registrar apenas o que falta nas métricas padrão.

Dashboards Grafana Prontos

O Grafana oferece dashboards da comunidade para Spring Boot que funcionam com as métricas padrão do Actuator. Um dashboard escrito à mão usa os tipos de painel timeseries e 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"
        }
      ]
    }
  ]
}

Muitos dashboards copiados de tutoriais antigos ainda usam os tipos de painel graph e singlestat. O Singlestat foi removido no Grafana 8, e o Grafana 12 removeu totalmente o suporte ao AngularJS, então esses painéis só são renderizados após a migração para timeseries e stat.

Importação no Grafana

Para importar um dashboard da comunidade: Grafana > Dashboards > New > Import, e então informar o ID 4701 (JVM Micrometer) ou 12900 (SpringBoot APM). Vale conferir os tipos de painel após a importação: revisões antigas dos dashboards da comunidade ainda podem conter painéis Angular que o Grafana 12 precisa migrar.

Alertas com Prometheus

Regras de Alerta Essenciais

As regras de alerta do Prometheus disparam notificações quando as métricas ultrapassam limites 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 }}"

Esses alertas cobrem os problemas mais comuns em produção: disponibilidade, performance e recursos. A regra HighLatency precisa de sum by (le, ...): sem ele, o histogram_quantile calcula um P99 por série (por URI, método e status), e um único endpoint lento com três chamadas dispara o alerta.

Métricas HTTP e de Banco de Dados no Spring Boot

Instrumentação Automática de Requisições HTTP

O Spring Boot instrumenta toda requisição de entrada do Spring MVC e do WebFlux por meio da Observation API e publica o timer http.server.requests, com as tags method, uri, status, outcome e 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}

As interfaces WebMvcTagsContributor e WebMvcTagsProvider encontradas em muitos exemplos foram removidas no Spring Boot 3.0. Tags customizadas agora são adicionadas estendendo DefaultServerRequestObservationConvention, como documenta a referência de métricas do 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")
        );
    }
}

Apenas key-values de baixa cardinalidade viram labels do Prometheus. Uma tag tenant é aceitável com algumas dezenas de tenants e vira um problema de cardinalidade com milhares. Valores de alta cardinalidade (ID de usuário, ID de pedido) pertencem a getHighCardinalityKeyValues, que alimenta apenas os traces. O lado de tracing da Observation API é detalhado em observabilidade do Spring Boot com OpenTelemetry.

Métricas HikariCP e de Consultas SQL

As métricas do pool do HikariCP são expostas automaticamente com o prefixo hikaricp, com tag pelo nome do pool, ao lado dos gauges genéricos jdbc.connections. Definir o nome do pool deixa a tag legível:

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

O tempo por consulta não é coberto pelo Spring Boot. O projeto datasource-micrometer envolve o DataSource e cria uma observação por conexão, consulta e result set, o que gera tanto métricas quanto spans de trace. A linha 2.x é voltada ao Spring Boot 4, a linha 1.x ao 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>

Isso substitui a abordagem manual de envolver o DataSource em um ProxyDataSourceBuilder com um listener escrito à mão, que contornava a autoconfiguração de DataSource do Spring Boot e precisava ser mantida em sincronia a cada atualização do Boot. Consultas lentas costumam indicar um padrão N+1, tratado em soluções para o problema N+1 no Spring Data JPA.

Boas Práticas para Produção

Cardinalidade das Métricas

Uma cardinalidade excessiva degrada o desempenho do Prometheus. Cada combinação única de tags gera uma série 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));
    }
}

Um Timer com buckets de histograma responde melhor às perguntas de latência do que um counter com uma tag bucket feita à mão: o PromQL consegue calcular qualquer percentil a partir dos buckets, e os limites dos buckets permanecem consistentes entre serviços.

Configuração Pronta para Produção

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}

Com access.default: none, endpoints como env, beans, configprops e mappings ficam inacessíveis sem precisar listá-los um a um. management.prometheus.metrics.export.enabled tem true como padrão, e uma configuração step não tem efeito no Prometheus: é um registry baseado em pull, e o intervalo de scraping é definido no prometheus.yml.

Erros Comuns com Actuator e Prometheus

SintomaCausaCorreção
/actuator/prometheus retorna 404Endpoint fora da lista de exposição, ou registry ausenteAdicionar prometheus em management.endpoints.web.exposure.include e verificar se micrometer-registry-prometheus está no classpath
O target do Prometheus mostra "server returned HTTP status 401"O Spring Security protege todos os endpoints exceto healthLiberar prometheus para a rede do scraper ou configurar basic auth no scrape config
@Timed não gera nenhuma métricamanagement.observations.annotations.enabled é false por padrão, ou o AspectJ está ausenteDefinir a propriedade como true e adicionar spring-boot-starter-aspectj
spring-boot-starter-aop não encontrado após o upgradeRenomeado no Spring Boot 4Usar spring-boot-starter-aspectj
O import de Health ou HealthIndicator falha no Boot 4Pacote movido para org.springframework.boot.health.contributorAtualizar os imports
Avisos de depreciação em management.endpoint.*.enabledModelo liga/desliga substituído no Spring Boot 3.4Usar management.endpoints.access.default e management.endpoint.<id>.access
Percentis do dashboard muito diferentes da realidadeValores de publishPercentiles com média entre podsUsar publishPercentileHistogram() e histogram_quantile()

Perguntas de Entrevista sobre Spring Boot Micrometer

O Actuator aparece na maioria das entrevistas de Spring Boot para vagas pleno e sênior. As respostas que separam um perfil experiente de um júnior:

  • Diferença entre liveness e readiness: a liveness diz ao Kubernetes para reiniciar o pod, a readiness diz para parar de rotear tráfego para ele. Um candidato que coloca o banco de dados no grupo de liveness nunca passou por uma indisponibilidade com essa configuração.
  • Por que Micrometer e não o cliente do Prometheus diretamente: o Micrometer é uma fachada. O mesmo código de Counter e Timer exporta para Prometheus, Datadog ou OTLP com a troca de uma dependência, e a instrumentação do próprio Spring (HTTP, pool JDBC, caches) é escrita sobre ele.
  • Percentis no cliente versus histogramas: percentis calculados no cliente não podem ser agregados entre instâncias, histogramas podem. É a pegadinha mais comum sobre o assunto.
  • O que é cardinalidade e como ela derruba o Prometheus: uma série temporal por combinação única de labels; uma tag com ID de usuário multiplica o uso de memória pelo número de usuários.
  • Como proteger o Actuator: limitar a exposição, definir níveis de acesso, rodar o gerenciamento em uma porta separada e protegê-lo com um SecurityFilterChain dedicado.

Mais perguntas desse tipo estão reunidas no módulo de perguntas de entrevista sobre Spring Boot Actuator.

Fontes

O que Levar de Spring Boot Micrometer e Prometheus

  • Três passos para um scraping funcional: starter do Actuator, micrometer-registry-prometheus, prometheus na lista de exposição
  • Modelo de acesso desde a 3.4: management.endpoints.access.default: none mais read-only por endpoint substitui as flags enabled depreciadas
  • Mudanças do Spring Boot 4: spring-boot-starter-aspectj, API de saúde em org.springframework.boot.health.contributor, EndpointRequest em org.springframework.boot.security.autoconfigure.actuate.web.servlet, probes ativas por padrão
  • Anotações precisam de uma chave: management.observations.annotations.enabled=true, sem bean TimedAspect manual
  • Tags HTTP via Observation API: estender DefaultServerRequestObservationConvention, manter labels de baixa cardinalidade
  • Histogramas em vez de percentis no cliente: publishPercentileHistogram() e sum by (le) em todo histogram_quantile
  • A liveness fica local: dependências externas vão para a readiness ou para um grupo customizado, nunca para a liveness

Essas configurações dão a um serviço Spring Boot 4 métricas que agregam corretamente entre pods, endpoints que não vazam nada para a rede pública e alertas que disparam em incidentes reais, e não por causa de uma única URI lenta.

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Desafio do dia

Você saberia encontrar o bug em Spring Boot?

Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador da SharpSkill

Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.

Atualizado em 26 de outubro de 2026

Tags

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

Compartilhar

Artigos relacionados