Todos os artigos

// Knowledge.log — 技術記事

Spring Boot Actuator em produção: o que vale a pena expor

Configure endpoints, health checks, readiness, segurança e métricas do Spring Boot Actuator sem abrir informações sensíveis em produção.

/actuator/health responder UP prova apenas que os indicadores daquele grupo estão saudáveis. Não prova que a aplicação consegue consumir a fila, atender pelo conector principal ou chamar uma dependência externa.

Esse detalhe importa porque o health check padrão pode continuar verde enquanto a fila orders acumula mensagens sem nenhum consumidor. A saída não é publicar todos os endpoints e procurar a resposta no susto. É separar acesso, exposição, liveness, readiness e métricas, cada um com uma responsabilidade verificável.

A base usada aqui é Java 25 LTS com Spring Boot 4.1.0. Essa combinação está dentro da matriz oficial de requisitos do Spring Boot: a versão 4.1.0 requer Java 17 ou superior e aceita até Java 26. O Spring Boot 3.5.16, compatível com Java 17 a 25, aparece apenas como contexto de migração; os exemplos seguem a API do Boot 4.

Acesso e exposição resolvem problemas diferentes

No Actuator, um endpoint precisa passar por duas decisões:

  • Acesso define se ele pode existir como unrestricted, read-only ou none, por meio de management.endpoint.<id>.access. Com none, o endpoint é removido do contexto.
  • Exposição define se um endpoint existente será publicado por HTTP ou JMX, por meio de management.endpoints.web.exposure e management.endpoints.jmx.exposure. Se um id estiver em include e exclude, o exclude vence.

Por padrão, somente health é exposto por HTTP e JMX. No HTTP, isso disponibiliza /actuator/health; /actuator funciona como rota de descoberta dos endpoints expostos.

Logo, conceder acesso não publica automaticamente um endpoint. E colocar um endpoint no include não supera um acesso none. O asterisco continua sendo uma estratégia de configuração, só não costuma ser uma estratégia de produção.

Uma seleção conservadora para o ambiente produtivo é:

EndpointDecisãoMotivo
healthExporAlimenta probes e permite verificar grupos de saúde.
infoExporEntrega informações operacionais escolhidas pela aplicação.
metricsExpor com autenticaçãoPermite explorar métricas em JSON; não é a rota de scrape.
prometheusExpor para a rede de monitoramentoEntrega o formato de scrape e exige o registry do Prometheus.
loggersOpcional e somente leituraAjuda no diagnóstico, sem permitir alteração por POST.
heapdump, env, configprops, threaddumpNão exporPodem revelar heap, configuração, valores ou stacks.
shutdown, beans, mappings, conditions, httpexchanges, logfile, sessionsNão exporAmpliam desnecessariamente a superfície operacional.

O heapdump já tem acesso restrito por padrão no Boot 4.1, assim como shutdown. Ainda vale fixá-lo como none para registrar a intenção. Para env e configprops, mantenha show-values=never; a máscara padrão não é convite para publicar o endpoint.

Uma configuração de produção com defesa em camadas

As dependências abaixo partem de um projeto gerenciado pelo parent ou BOM do Spring Boot 4.1.0:

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

Separe o tráfego de gestão em uma porta privada e libere apenas os endpoints necessários:

management:
  server:
    port: 8081
  endpoints:
    access:
      max-permitted: read-only
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when-authorized
      roles: ENDPOINT_ADMIN
      probes:
        add-additional-paths: true
      group:
        readiness:
          include: readinessState,rabbit,ordersQueue
    env:
      show-values: never
    heapdump:
      access: none

O teto max-permitted: read-only prevalece sobre permissões mais amplas. Ele também impede que uma futura inclusão de loggers habilite a escrita por acidente. Se a equipe realmente precisar alterar loggers em execução, terá de tratar autenticação e CSRF de forma deliberada; desabilitar CSRF globalmente porque um POST devolveu 403 apenas troca um diagnóstico por outro problema.

Com Spring Security no classpath e sem uma SecurityFilterChain própria, o Boot protege todos os endpoints do Actuator, exceto health. Quando a aplicação declara uma chain, essa configuração automática recua. Nesse caso, crie duas chains: uma para a aplicação e outra para os endpoints selecionados com EndpointRequest.toAnyEndpoint(), HTTP Basic e a autoridade ENDPOINT_ADMIN. No Boot 4, o matcher fica em org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest.

Libere sem autenticação somente as rotas de probe que o kubelet precisa acessar. A porta 8081 deve permanecer alcançável apenas pela rede operacional ou pelo cluster, não pela entrada pública da aplicação.

O health verde do RabbitMQ não mede a fila

No Boot 4, indicadores customizados implementam org.springframework.boot.health.contributor.HealthIndicator. Essa mudança de pacote importa para quem vem do Boot 3.5, cuja interface ficava sob org.springframework.boot.actuate.health.

O indicador automático do RabbitMQ também merece uma leitura precisa. O RabbitHealthIndicator do Spring Boot 4.1.0 abre uma operação pelo RabbitTemplate e lê a propriedade version do broker. Se o broker responde, o componente fica UP.

Ele não verifica quantidade de mensagens nem consumidores. Portanto, um broker acessível, zero consumidores e uma fila parada há 20 minutos ainda produzem um rabbit verde. É um verde educado, mas não especialmente útil para o pedido que continua esperando.

Para verificar a condição operacional da fila orders, adicione um indicador específico:

package academy.devdojo.orders;

import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class OrdersQueueHealthIndicator implements HealthIndicator {

    private static final int MAX_PENDING_MESSAGES = 1_000;

    private final AmqpAdmin amqpAdmin;

    public OrdersQueueHealthIndicator(AmqpAdmin amqpAdmin) {
        this.amqpAdmin = amqpAdmin;
    }

    @Override
    public Health health() {
        var properties = this.amqpAdmin.getQueueProperties("orders");

        if (properties == null) {
            return Health.down()
                    .withDetail("queue", "orders")
                    .withDetail("reason", "missing")
                    .build();
        }

        int messages = (Integer) properties.get(RabbitAdmin.QUEUE_MESSAGE_COUNT);
        int consumers = (Integer) properties.get(RabbitAdmin.QUEUE_CONSUMER_COUNT);

        var health = Health.up()
                .withDetail("messages", messages)
                .withDetail("consumers", consumers);

        return (consumers == 0 || messages > MAX_PENDING_MESSAGES)
                ? health.down().build()
                : health.build();
    }
}

O nome da classe sem o sufixo HealthIndicator vira o id ordersQueue, usado no grupo de readiness do YAML. O limite de mil mensagens é uma decisão explícita do exemplo, não uma recomendação universal; ajuste-o ao comportamento aceitável da fila real.

A correção concreta quando esse indicador acusa zero consumidores é restabelecer o listener responsável e então drenar a fila. Se a fila não existe, corrija sua declaração ou o nome configurado. Durante o diagnóstico, consulte /actuator/health/readiness com uma credencial autorizada e acompanhe os campos messages e consumers; depois confirme que o grupo voltou a UP. Isso observa o problema pela porta privada sem publicar detalhes de saúde na internet.

Para uma API downstream, crie outro HealthIndicator, faça um GET curto com RestClient, use timeout na faixa de 200 a 300 ms e devolva Health.down(ex) em falha de I/O. Só então acrescente o id desse indicador — e, se houver DataSource, o db automático do starter JDBC — ao include de readiness. O YAML acima lista só o que o snippet realmente registra, porque um membro desconhecido no grupo falha a subida. O Actuator não cria automaticamente um health indicator para clientes HTTP.

Liveness não é prontidão com outro nome

No Boot 4, os grupos de liveness e readiness estão habilitados por padrão. Eles ficam disponíveis em:

  • /actuator/health/liveness
  • /actuator/health/readiness

Os grupos padrão não incluem banco, RabbitMQ nem indicadores customizados. A separação é intencional:

  • Liveness responde se o processo local precisa ser reiniciado. Não inclua banco, API externa, cache ou fila. Uma falha externa em liveness faz o kubelet reiniciar réplicas saudáveis e pode transformar uma indisponibilidade compartilhada em uma tempestade de reinícios.
  • Readiness responde se a réplica deve receber tráfego. Aqui cabem rabbit, ordersQueue e, depois de existirem no contexto, db e um indicador HTTP. Aceite a consequência: se uma dependência compartilhada cair, todas as réplicas podem ficar não prontas e retirar o serviço inteiro do balanceamento.

Indicadores DOWN e OUT_OF_SERVICE resultam em HTTP 503; UP e UNKNOWN, em HTTP 200. Para o kubelet, respostas de 200 a 399 são sucesso.

A porta de gestão separada cria mais uma sutileza: ela pode responder enquanto o conector principal está quebrado. Com add-additional-paths: true, o Boot publica /livez e /readyz na porta principal. As probes podem testar o caminho realmente usado pela aplicação:

startupProbe:
  httpGet:
    path: /livez
    port: 8080

livenessProbe:
  httpGet:
    path: /livez
    port: 8080

readinessProbe:
  httpGet:
    path: /readyz
    port: 8080

A startupProbe evita que uma inicialização longa seja confundida com processo morto. Enquanto isso, readiness segura o tráfego. Kubernetes e OpenShift usam esse mesmo modelo de probes; não é necessário inventar uma rota especial para o OpenShift.

Se um indicador ficar lento, o Actuator registra aviso após 10 segundos por padrão. O limite pode ser ajustado com management.endpoint.health.logging.slow-indicator-threshold, mas um health check de downstream deve continuar curto. Probe não é lugar para executar uma auditoria completa da cadeia distribuída.

Métricas respondem perguntas que health checks não respondem

Health é um estado discreto para automação. Tendência, volume e latência pertencem às métricas e à observabilidade com Micrometer Observation.

Com micrometer-registry-prometheus no projeto e prometheus incluído na exposição, o scrape fica em /actuator/prometheus. As duas condições são necessárias; o endpoint não é exposto por padrão. Já /actuator/metrics serve para explorar as medições em JSON, não para o scrape do Prometheus.

Uma configuração mínima do Prometheus aponta para a porta privada de gestão:

scrape_configs:
  - job_name: devdojo-orders
    metrics_path: /actuator/prometheus
    static_configs:
      - targets: ["orders-service:8081"]

Entre as métricas fornecidas estão http.server.requests, http.client.requests, famílias jvm.*, processo, sistema, disco, uptime, application.started.time, application.ready.time, eventos de logger e pools de executor. Para estatísticas JVM de virtual threads, acrescente micrometer-java21.

Histogramas adequados ao Prometheus podem ser habilitados com management.metrics.distribution.percentiles-histogram; percentis calculados no processo usam .percentiles, e limites de buckets definidos pela aplicação usam .slo. A documentação não fornece um p95, p99 ou SLO pronto para o serviço. Esses valores só fazem sentido depois de observar a carga e as dependências reais, por isso nenhum número é presumido aqui.

Como verificar antes de liberar

Com a aplicação rodando, faça a checagem pela rede que alcança a porta de gestão:

curl -i http://localhost:8081/actuator
curl -i http://localhost:8081/actuator/health
curl -i http://localhost:8081/actuator/health/liveness
curl -i http://localhost:8081/actuator/health/readiness
curl -i -u actuator-admin:SENHA http://localhost:8081/actuator/health
curl -i -u actuator-admin:SENHA http://localhost:8081/actuator/prometheus
curl -i http://localhost:8080/actuator/env

A descoberta deve listar somente o que foi exposto. O health sem autorização não deve revelar componentes; a chamada autorizada pode mostrar detalhes conforme o papel configurado. O Prometheus deve responder no formato de texto após autenticação. A tentativa pública a /actuator/env na porta 8080 deve retornar 404.

Para reproduzir o falso verde da fila sem simular carga, mantenha apenas o indicador automático rabbit, interrompa o listener e publique mensagens. O broker continuará acessível e o componente rabbit ficará UP. Depois registre OrdersQueueHealthIndicator, inclua ordersQueue em readiness e repita: zero consumidores ou mais de mil mensagens deve levar o grupo a DOWN e a resposta a 503.

Recomendação final

Em produção, exponha health, info, metrics e prometheus somente em uma porta privada; acrescente loggers apenas se houver necessidade operacional, autenticação e acesso somente leitura. Mantenha dumps, configuração e endpoints administrativos fora da exposição e limite o acesso como segunda barreira.

Use liveness apenas para falhas locais irrecuperáveis e leve banco, fila e HTTP downstream para readiness quando retirar a réplica do tráfego for realmente a resposta correta. Se a indisponibilidade compartilhada deixaria todas as réplicas não prontas, não coloque a dependência no grupo por reflexo: trate a degradação acima com timeout e circuit breaking e observe-a por métricas.

Para o caso da fila, não aceite RabbitHealthIndicator como prova de consumo. Adote um indicador de fila na readiness quando backlog ou ausência de consumidores significar que a réplica não consegue cumprir sua função; caso contrário, mantenha o sinal nas métricas e nos alertas, sem transformar cada oscilação operacional em reinício de contêiner.

javaspring-bootactuator

// Continue.training — 次のステップ

Conhecimento só conta quando vira prática.

Volte ao artigo, execute os exemplos e compartilhe o que aprendeu.

Explorar mais artigos