Spring Boot Actuator: health check e metrics
1. Visão geral
Um serviço rodando em produção precisa responder duas perguntas: “ele ainda está vivo?” e “como está o desempenho?”. Spring Boot Actuator fornece respostas para ambas através de endpoints prontos, principalmente /actuator/health e /actuator/metrics.
Neste artigo, você vai habilitar o Actuator, escrever um HealthIndicator customizado e configurar probes de liveness/readiness para Kubernetes. Depois, você vai registrar métricas de negócio usando Micrometer, exportar para Prometheus e escrever testes para ambas as partes.
2. Instalação
Você precisa adicionar o starter do Actuator. O exemplo deste artigo também usa spring-boot-starter-web para que os endpoints sejam servidos via HTTP:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Inicie a aplicação e tente chamar:
curl http://localhost:8080/actuator
{"_links":{"self":{...},"health":{...},"health-path":{...}}}
Por padrão, Spring Boot 3.x expõe apenas o endpoint health via HTTP. Os outros endpoints ainda existem, mas não são acessíveis de fora. Esta é uma escolha de segurança deliberada.
2.1. Expor endpoints adicionais
Você lista explicitamente quais endpoints deseja abrir em application.properties:
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
A primeira linha abre três endpoints: health, info e metrics. A segunda linha permite que /actuator/health retorne detalhes de cada componente em vez de apenas um status geral.
Atenção: Evite usar
management.endpoints.web.exposure.include=*em produção. Endpoints comoenv,heapdumpouconfigpropspodem expor senhas, configurações internas e dados na memória.
3. Health check
3.1. Endpoint padrão
Com a configuração acima, chamar /actuator/health retorna:
{
"status": "UP",
"components": {
"diskSpace": {
"status": "UP",
"details": { "total": 499963174912, "free": 213456789504, "threshold": 10485760 }
},
"ping": { "status": "UP" }
}
}
Spring Boot registra automaticamente HealthIndicators com base no classpath. Se houver um DataSource, você verá um componente db. Se houver Redis, verá redis, e assim por diante com outros componentes. O status geral é a agregação: se qualquer componente estiver DOWN, o status geral é DOWN, e o HTTP status retornado é 503.
3.2. Escrevendo um HealthIndicator customizado
Suponha que seu serviço de pedidos dependa de um gateway de pagamento externo. Primeiro, você tem um cliente simples:
public interface PaymentGatewayClient {
boolean ping();
}
Depois, escreva um indicador que verifica este cliente:
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final PaymentGatewayClient client;
public PaymentGatewayHealthIndicator(PaymentGatewayClient client) {
this.client = client;
}
@Override
public Health health() {
try {
if (client.ping()) {
return Health.up().withDetail("provider", "VNPay").build();
}
return Health.down().withDetail("reason", "ping returned false").build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
Alguns pontos a observar:
- O nome do bean é
paymentGatewayHealthIndicator. Spring Boot remove o sufixoHealthIndicator, então a chave no JSON épaymentGateway. withDetail(...)adiciona informações extras, mostradas apenas quandoshow-detailspermite.Health.down(e)automaticamente coloca a mensagem de erro na seçãodetails.error.
O resultado quando o gateway de pagamento falha:
{
"status": "DOWN",
"components": {
"diskSpace": { "status": "UP", ... },
"paymentGateway": {
"status": "DOWN",
"details": { "error": "java.net.ConnectException: Connection refused" }
},
"ping": { "status": "UP" }
}
}
Atenção: O método
health()é executado cada vez que o endpoint é chamado. Se ele fazer uma chamada de rede sem timeout, uma dependência lenta vai fazer/actuator/healthtravar. Você deve sempre definir um timeout curto (por exemplo, 1–2 segundos) para clientes usados em health checks.
3.3. Probes de liveness e readiness
Kubernetes diferencia dois tipos de probe:
- Liveness: a aplicação travou? Se falhar, o pod é reiniciado.
- Readiness: a aplicação está pronta para receber requisições? Se falhar, o pod é removido do load balancer, mas não é reiniciado.
Spring Boot ativa automaticamente esses dois grupos quando detecta que está rodando no Kubernetes. Quando roda localmente, você habilita manualmente:
management.endpoint.health.probes.enabled=true
management.endpoint.health.group.readiness.include=readinessState,paymentGateway
A primeira linha cria dois endpoints: /actuator/health/liveness e /actuator/health/readiness. A segunda linha inclui paymentGateway no grupo readiness. Quando o gateway de pagamento cai, o pod apenas para de receber tráfego, não é reiniciado desnecessariamente.
curl http://localhost:8080/actuator/health/liveness
{"status":"UP"}
A configuração correspondente no manifest Kubernetes:
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
Atenção: Não inclua dependências externas (banco de dados, APIs externas) no grupo liveness. Quando o banco cai, todos os pods falham liveness e são reiniciados simultaneamente. Reiniciar não vai corrigir o banco, apenas adiciona mais carga na inicialização.
4. Métricas com Micrometer
O Actuator usa Micrometer como uma camada de abstração para métricas. Nenhuma configuração adicional é necessária; você já tem métricas de JVM, CPU, requisições HTTP, connection pools e muito mais.
curl http://localhost:8080/actuator/metrics
{"names":["application.ready.time","http.server.requests","jvm.gc.pause","jvm.memory.used","process.cpu.usage", ...]}
Para ver os detalhes de uma métrica, você adiciona seu nome à URL. Você pode filtrar por tags usando o parâmetro tag:
curl "http://localhost:8080/actuator/metrics/http.server.requests?tag=uri:/orders"
4.1. Counter para número de pedidos
As métricas do sistema não são suficientes; você geralmente precisa de métricas de negócio também. O exemplo abaixo conta o número de pedidos criados, divididos por canal de vendas:
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final MeterRegistry registry;
public OrderService(MeterRegistry registry) {
this.registry = registry;
}
public Order createOrder(Customer customer, String channel) {
Order order = new Order(customer.id(), channel);
// ... salvar pedido
Counter.builder("orders.created")
.description("Número de pedidos criados")
.tag("channel", channel)
.register(registry)
.increment();
return order;
}
}
public record Customer(long id, String name) {}
public record Order(long customerId, String channel) {}
MeterRegistry é configurado automaticamente pelo Spring Boot e injetado. Chamar register(registry) múltiplas vezes com o mesmo nome e tags é seguro, pois Micrometer retorna o counter já existente. Depois de criar três pedidos através do canal web:
curl http://localhost:8080/actuator/metrics/orders.created
{
"name": "orders.created",
"description": "Número de pedidos criados",
"measurements": [ { "statistic": "COUNT", "value": 3.0 } ],
"availableTags": [ { "tag": "channel", "values": ["web"] } ]
}
4.2. Timer para o tempo de processamento de pagamento
Timer conta o número de invocações e também mede o tempo total e máximo:
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final Timer paymentTimer;
public PaymentService(MeterRegistry registry) {
this.paymentTimer = Timer.builder("payment.process")
.description("Tempo de processamento de pagamento")
.register(registry);
}
public boolean pay(Order order) {
return paymentTimer.record(() -> {
// ... chamar gateway de pagamento
return true;
});
}
}
O Timer é criado uma vez no construtor e reutilizado, pois não há tags que mudam entre invocações. record(Supplier) mede o tempo de execução do lambda e retorna seu resultado. O endpoint /actuator/metrics/payment.process exibirá três estatísticas: COUNT, TOTAL_TIME e MAX.
4.3. Tags comuns para todas as métricas
Quando múltiplos serviços enviam métricas para um único lugar, você deve adicionar o nome da aplicação a todas as métricas:
management.metrics.tags.application=order-service
Atenção: Não use valores com cardinalidade muito alta ou ilimitada como tags, por exemplo
orderId,customerIdou email. Cada combinação única de tags cria uma time series separada. Milhões de pedidos criarão milhões de series, inchando a memória da aplicação e do sistema de monitoramento. Tags devem conter apenas um conjunto pequeno e fixo de valores, comochannel,statusouregion.
5. Exportar para Prometheus
O endpoint /actuator/metrics é útil para visualização rápida. Para armazenamento de longo prazo e construção de gráficos, você precisa de um sistema como Prometheus. Adicione o registry correspondente:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Depois exponha o endpoint prometheus:
management.endpoints.web.exposure.include=health,info,metrics,prometheus
curl http://localhost:8080/actuator/prometheus | grep -E "orders|payment"
# HELP orders_created_total Número de pedidos criados
# TYPE orders_created_total counter
orders_created_total{application="order-service",channel="web"} 3.0
payment_process_seconds_count{application="order-service"} 5.0
payment_process_seconds_sum{application="order-service"} 0.412
payment_process_seconds_max{application="order-service"} 0.153
Micrometer automaticamente renomeia de acordo com as convenções de Prometheus: pontos viram underscores, counters ganham o sufixo _total, timers são medidos em segundos. O resto é apenas configurar Prometheus para fazer scrape do endpoint /actuator/prometheus.
6. Testes
6.1. Testando HealthIndicator
HealthIndicator é uma classe comum, então você pode testá-la sem precisar inicializar o contexto Spring:
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import static org.assertj.core.api.Assertions.assertThat;
class PaymentGatewayHealthIndicatorTest {
@Test
void whenPingSucceeds_thenStatusIsUp() {
var indicator = new PaymentGatewayHealthIndicator(() -> true);
Health health = indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsEntry("provider", "VNPay");
}
@Test
void whenPingThrows_thenStatusIsDownWithError() {
var indicator = new PaymentGatewayHealthIndicator(() -> {
throw new IllegalStateException("timeout");
});
Health health = indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails().get("error").toString()).contains("timeout");
}
}
Como PaymentGatewayClient tem apenas um método, você passa um lambda em vez de um mock.
6.2. Testando métricas
Micrometer fornece SimpleMeterRegistry, que armazena métricas na memória, muito útil para testes unitários:
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class OrderServiceTest {
@Test
void whenOrdersCreated_thenCounterIncrementsPerChannel() {
var registry = new SimpleMeterRegistry();
var service = new OrderService(registry);
var customer = new Customer(1L, "An");
service.createOrder(customer, "web");
service.createOrder(customer, "web");
service.createOrder(customer, "mobile");
assertThat(registry.get("orders.created").tag("channel", "web").counter().count())
.isEqualTo(2.0);
assertThat(registry.get("orders.created").tag("channel", "mobile").counter().count())
.isEqualTo(1.0);
}
}
Se a métrica não existir, registry.get(...) lança MeterNotFoundException. Desta forma, o teste também detecta nomes de métricas digitados incorretamente.
7. Segurança e erros comuns
Separar porta de administração. Você pode fazer o Actuator rodar em uma porta separada, aberta apenas na rede interna:
management.server.port=8081
Neste caso, a aplicação é servida em 8080, e /actuator/** está disponível apenas em 8081. Você bloqueia esta porta no firewall ou ingress e pronto. No Kubernetes, lembre-se de alterar a porta na configuração do probe para 8081.
Limitar detalhes de health. Em produção, use show-details=when-authorized combinado com Spring Security em vez de always. Usuários anônimos verão apenas {"status":"UP"}.
Alguns erros comuns:
- Chamando
/actuator/metricsrecebe404: você ainda não adicionoumetricsaexposure.include. - Health check falha aleatoriamente: um indicador faz uma chamada de rede sem timeout ou tem um tempo de resposta mais lento que o timeout da probe.
- Pod reinicia continuamente quando o banco cai intermitentemente: o banco de dados está incluído no grupo liveness.
- Memória aumenta gradualmente com o tempo: uma métrica usa tags com cardinalidade alta ou ilimitada.
- Não vê o endpoint
/actuator/prometheus: faltamicrometer-registry-prometheusou o endpoint não foi exposto.
8. Conclusão
Spring Boot Actuator oferece health checks e métricas quase gratuitamente. Seu trabalho é configurar corretamente: exponha apenas os endpoints necessários, escreva HealthIndicators com timeout para dependências críticas, e separe liveness de readiness para que Kubernetes reaja apropriadamente.
Com métricas, Micrometer permite registrar Counters e Timers para negócio em apenas algumas linhas, depois exportar para Prometheus com uma simples dependência. Mantenha o número de valores únicos de tags pequeno e escreva testes usando SimpleMeterRegistry para que as métricas não falhem silenciosamente após cada refatoração.