Um ping à página inicial diz que o servidor responde. Não diz se a base de dados responde, se a fila está a encher ou se o fornecedor de pagamentos está há dez minutos a devolver erros. É para isso que existem os endpoints de saúde: um URL onde o próprio serviço verifica as suas dependências e informa sobre cada uma. O problema é que não há um formato único. Uma equipa de Java usa o Spring Boot Actuator, uma de .NET usa os Health Checks do ASP.NET Core, outra segue o rascunho health+json do IETF e a plataforma corre em Kubernetes, que tem o seu próprio estilo. Neste artigo mostramos como é cada um, onde a configuração costuma falhar e como o AAA Sentinel os lê sem obrigar a mudar nada.
Quatro dialetos para a mesma pergunta
Todos respondem a «está tudo bem?», mas com palavras diferentes:
- health+json: um rascunho do IETF (draft-inadarei-api-health-check) com o tipo de conteúdo application/health+json e três estados: pass, warn e fail.
- Spring Boot Actuator: o endpoint /actuator/health, com os estados UP, DOWN, OUT_OF_SERVICE e UNKNOWN.
- ASP.NET Core Health Checks: o que o MapHealthChecks publica, com os estados Healthy, Degraded e Unhealthy.
- Kubernetes: /livez e /readyz do API server, que respondem ok ou, com ?verbose, uma linha por verificação.
health+json: o rascunho do IETF
É o formato mais neutro: não depende de nenhum framework. O documento traz um status geral e, opcionalmente, um objeto checks. Cada chave de checks identifica um componente e o que se mede, na forma componente:medida, e o seu valor é uma lista, porque o mesmo componente pode ser medido em vários nós. Cada entrada tem o seu próprio status e, se se quiser, observedValue, observedUnit, componentType, componentId, time e output.
HTTP/1.1 200 OK
Content-Type: application/health+json
Cache-Control: no-store
{
"status": "warn",
"version": "1.4.2",
"checks": {
"postgres:responseTime": [
{
"componentType": "datastore",
"observedValue": 12,
"observedUnit": "ms",
"status": "pass",
"time": "2026-10-18T09:00:00Z"
}
],
"orders-queue:depth": [
{
"componentType": "component",
"observedValue": 1480,
"status": "warn",
"time": "2026-10-18T09:00:00Z"
}
]
}
}- pass: saudável. warn: funciona, mas com algum problema. fail: não funciona.
- O rascunho admite sinónimos: ok e up equivalem a pass; error e down equivalem a fail.
- O código HTTP acompanha o estado: 2xx para pass e warn, 4xx ou 5xx para fail (o habitual é 503).
- Campos opcionais ao nível do serviço: version, releaseId, serviceId, description, notes e output.
Spring Boot Actuator: /actuator/health
Com o spring-boot-starter-actuator no projeto, o /actuator/health fica publicado por HTTP desde o primeiro arranque. O que falta é o detalhe: por omissão, show-details vale never e a resposta fica pelo estado agregado.
GET /actuator/health
HTTP/1.1 200 OK
{ "status": "UP" }GET /actuator/health
HTTP/1.1 503 Service Unavailable
{
"status": "DOWN",
"components": {
"db": {
"status": "DOWN",
"details": { "error": "org.springframework.jdbc.CannotGetJdbcConnectionException" }
},
"diskSpace": {
"status": "UP",
"details": { "total": 53687091200, "free": 31138512896, "threshold": 10485760, "exists": true }
},
"ping": { "status": "UP" }
}
}Desde o Spring Boot 2.2 os indicadores vêm em components; antes vinham em details. Por omissão, DOWN e OUT_OF_SERVICE respondem 503, e UP e UNKNOWN respondem 200. Esse mapeamento altera-se com management.endpoint.health.status.http-mapping.
Como mostrar os componentes no Spring
Há duas propriedades distintas e convém não as misturar. show-components decide se aparece a lista de componentes com o respetivo estado; show-details decide se aparece também o detalhe de cada um (versão da base de dados, espaço em disco, mensagens de erro). Se show-components não for definida, assume o valor de show-details. Para um monitor externo bastam os estados, por isso uma configuração razoável é esta:
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
show-components: always
show-details: when-authorized
probes:
enabled: true- show-components: always mostra o estado de db, diskSpace, redis e dos restantes a qualquer pessoa, sem dados internos.
- show-details: when-authorized reserva o detalhe a utilizadores autenticados (e, se for definido management.endpoint.health.roles, a esses papéis).
- probes.enabled publica ainda /actuator/health/liveness e /actuator/health/readiness; no Kubernetes ativam-se sozinhos.
- Para não expor nada na porta pública, management.server.port passa o Actuator para outra porta.
ASP.NET Core Health Checks
Em .NET, as verificações registam-se com AddHealthChecks e o endpoint publica-se com MapHealthChecks. Cada verificação devolve um HealthCheckResult, e o relatório agregado assume o pior estado.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks()
.AddCheck<DatabaseHealthCheck>("database")
.AddCheck<QueueHealthCheck>("queue");
var app = builder.Build();
app.MapHealthChecks("/health");
app.Run();GET /health
HTTP/1.1 200 OK
Content-Type: text/plain
DegradedO escritor por omissão devolve apenas uma palavra em texto simples: Healthy, Degraded ou Unhealthy. Sem componentes. E, por omissão, Degraded responde 200, tal como Healthy; só Unhealthy responde 503. Um monitor que olhe apenas para o código HTTP nunca verá um serviço degradado. Os códigos alteram-se com HealthCheckOptions.ResultStatusCodes.
Um escritor JSON para ASP.NET
Para ter o estado por componente é preciso um ResponseWriter. A opção sem dependências é escrever um próprio, ao estilo do exemplo da documentação da Microsoft, com uma propriedade results:
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = (context, report) => context.Response.WriteAsJsonAsync(new
{
status = report.Status.ToString(),
results = report.Entries.ToDictionary(
entry => entry.Key,
entry => new
{
status = entry.Value.Status.ToString(),
description = entry.Value.Description,
}),
}),
});{
"status": "Degraded",
"results": {
"database": { "status": "Healthy", "description": null },
"queue": { "status": "Degraded", "description": "1480 messages waiting" }
}
}- Serialize o estado com ToString(). Se o enum HealthStatus for serializado tal como está, sem JsonStringEnumConverter, sai como número: 0 é Unhealthy, 1 é Degraded e 2 é Healthy.
- Não coloque na resposta exceções nem cadeias de ligação: os endpoints de saúde costumam ser públicos.
A alternativa da comunidade: HealthChecks.UI.Client
Muitos projetos usam o AspNetCore.HealthChecks.UI.Client, do projeto comunitário AspNetCore.Diagnostics.HealthChecks (não é um pacote da Microsoft). O seu escritor devolve as verificações numa propriedade entries, com duração e etiquetas:
// dotnet add package AspNetCore.HealthChecks.UI.Client
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
});{
"status": "Degraded",
"totalDuration": "00:00:00.0153210",
"entries": {
"database": { "data": {}, "duration": "00:00:00.0081120", "status": "Healthy", "tags": [] },
"queue": { "data": {}, "description": "1480 messages waiting", "duration": "00:00:00.0040310", "status": "Degraded", "tags": [] }
}
}Kubernetes: /livez e /readyz
O API server do Kubernetes publica /livez e /readyz (e o antigo /healthz). Sem parâmetros respondem ok com um 200. Com ?verbose devolvem uma linha por verificação: [+]nome ok se passa e [-]nome failed se não, com um 500 quando alguma falha. Algumas aplicações imitam esse formato nos seus próprios endpoints.
GET /readyz?verbose
HTTP/1.1 500 Internal Server Error
[+]ping ok
[+]log ok
[-]etcd failed: reason withheld
[+]informer-sync ok
readyz check failedComo se correspondem os estados
Os quatro formatos têm um estado saudável e um em falha. A nuance está no meio: warn, Degraded e UNKNOWN não são uma falha. É assim que o AAA Sentinel os lê:
Contrato de saúde · de cada estado a saudável, degradado ou em baixo
- health+json
- pass, ok, up → saudável · warn → degradado · fail, error, down → em baixo
- Spring Boot Actuator
- UP → saudável · UNKNOWN → degradado · DOWN, OUT_OF_SERVICE → em baixo
- ASP.NET Core
- Healthy (ou 2) → saudável · Degraded (ou 1) → degradado · Unhealthy (ou 0) → em baixo
- Kubernetes
- ok ou [+]nome → saudável · [-]nome → em baixo
- Código HTTP fora de 2xx
- Em baixo, diga o corpo o que disser
- Corpo que não segue nenhum contrato
- Decide o código HTTP, como num ping; se o corpo chegou cortado, degradado
As armadilhas do costume
Quase todos os falsos «está tudo bem» vêm daqui:
- Actuator sem show-components nem show-details: só há {"status":"UP"}, e um componente em falha aparece apenas como DOWN global, sem se saber qual.
- ASP.NET com o escritor por omissão: uma palavra em texto simples e um 200 para Degraded.
- O código HTTP e o corpo contradizem-se: um 200 com status fail (escritores próprios que respondem sempre 200) ou um 503 com status pass (um proxy à frente que devolve o seu próprio erro).
- Degradado não é em baixo: se um warn acordar alguém às três da manhã, a equipa acabará por silenciar o monitor.
- As maiúsculas contam: up é health+json e UP é Actuator. Um Up a meio caminho não é nenhum dos dois.
- Um endpoint de saúde que demora 30 segundos porque espera por uma dependência bloqueada. Cada verificação deve ter um tempo limite.
Como o contrato de saúde do AAA Sentinel os lê
O monitor do tipo contrato de saúde faz um GET ao URL HTTPS e lê a resposta tal como chega. O formato não se escolhe ao criá-lo: é reconhecido pelo corpo. Estas são as regras:
- Se o corpo for JSON, tenta por esta ordem: health+json (status em minúsculas, com os sinónimos), Actuator (status em maiúsculas, componentes em components ou details) e ASP.NET (Healthy, Degraded ou Unhealthy, componentes em results ou entries).
- Um status numérico só é lido como o enum de .NET se vier acompanhado das entradas em results ou entries; um número solto podia ser de qualquer coisa.
- Se não for JSON, lê o texto simples do escritor por omissão do ASP.NET e, se não for esse o caso, a saída do Kubernetes: ok ou uma linha [+]/[-] por verificação.
- De cada componente retira o estado. Em health+json, quando uma chave tem várias entradas (vários nós), conta a pior.
- O estado do serviço é o pior entre o que declara e o dos seus componentes: um pass com um componente em fail está em baixo.
- Se o código HTTP não for 2xx, está em baixo mesmo que o corpo diga pass. Se for 2xx e o corpo disser fail, também está em baixo: precisamente o caso que um ping não vê.
- Se o corpo não encaixar em nenhum contrato, decide o código HTTP. Se chegou cortado por tamanho, não é dado como saudável: fica degradado.
- É possível definir uma latência máxima (até 10 segundos): uma resposta mais lenta conta como em baixo.
Degradado e em baixo não são tratados da mesma forma
Um serviço degradado continua a servir, por isso não é tratado como uma falha:
- Degradado (um warn, um Degraded ou um UNKNOWN): fica registado e a página de estado mostra-o como degradado, mas não abre incidente nem dispara como uma falha.
- Em baixo: entra na máquina de estados habitual. Primeiro fica suspeito, a verificação repete-se a partir de uma segunda região da AWS (eu-central-1) e, só se se confirmar, abre um incidente e alerta por e-mail, Slack, Microsoft Teams ou webhook.
- Quando volta a estar saudável, o incidente fecha-se sozinho e chega o aviso de recuperação.
- Nas primeiras 24 horas o monitor observa e propõe limiares sem alertar.
Se o endpoint pedir credenciais, o monitor aceita bearer, basic, API key, AWS SigV4 e tokens do Azure AD ou do Google Cloud com a identidade da conta já ligada. Cada sonda leva o cabeçalho X-AAA-Signature, assinado com um segredo da organização, para que se possa distingui-la do restante tráfego.
health+json em Node com @aaa-sentinel/health
Para um serviço em Node que ainda não tenha endpoint de saúde, o @aaa-sentinel/health (no npm, sem dependências, Node 20 ou posterior) entrega-o pronto em health+json. Recebe um objeto com uma função por componente e devolve a resposta HTTP completa. Numa Lambda atrás do API Gateway ou de um function URL, devolve-se tal como está:
import { runHealthChecks } from '@aaa-sentinel/health'
export const handler = () =>
runHealthChecks(
{
database: async () => {
await db.query('select 1')
},
queue: async () => ((await queueDepth()) < 1000 ? 'pass' : 'warn'),
payments: async (signal) => {
const started = Date.now()
await fetch('https://payments.internal/ping', { signal })
return { observedValue: Date.now() - started, observedUnit: 'ms' }
},
},
{ version: process.env.APP_VERSION, timeoutMs: 2000 },
)import express from 'express'
import { runHealthChecks } from '@aaa-sentinel/health'
const app = express()
app.get('/health', async (_req, res) => {
const r = await runHealthChecks(checks, { version: process.env.APP_VERSION })
res.writeHead(r.statusCode, r.headers).end(r.body)
})HTTP/1.1 200 OK
Content-Type: application/health+json
Cache-Control: no-store
{
"status": "warn",
"version": "1.4.2",
"checks": {
"database": [{ "status": "pass", "time": "2026-10-18T09:00:00.120Z" }],
"queue": [{ "status": "warn", "time": "2026-10-18T09:00:00.118Z" }],
"payments": [{ "observedValue": 41, "observedUnit": "ms", "status": "pass", "time": "2026-10-18T09:00:00.161Z" }]
}
}- Uma verificação que não devolve nada ou devolve true é pass; false é fail; também pode devolver pass, warn ou fail, ou um objeto com observedValue e observedUnit.
- As verificações correm em simultâneo, cada uma com o seu tempo limite (timeoutMs, 2 segundos por omissão e 60 no máximo). A que lança um erro ou excede o tempo é fail para esse componente, e o endpoint continua a responder. Cada uma recebe um AbortSignal para interromper a chamada.
- O estado geral é a pior verificação. fail responde 503; pass e warn, 200. Assim também o entendem os balanceadores de carga.
- As mensagens de erro ficam fora da resposta, salvo com exposeErrors: true, porque costumam conter hosts ou cadeias de ligação.
- minimumStatus: 'warn' mantém o serviço pelo menos em warn, por exemplo durante uma manutenção.
- Com os resultados já calculados, healthResponse constrói a mesma resposta sem executar nada, e healthBody devolve apenas o documento.
Checklist
Antes de criar o monitor, confirme que o endpoint cumpre isto:
- Responde por HTTPS a um GET, sem redirecionar para uma página de login.
- Devolve o estado de cada componente: show-components no Spring, um ResponseWriter JSON no ASP.NET, checks em health+json.
- Não expõe detalhes internos a quem não os deve ver.
- Cada verificação tem um tempo limite e o endpoint responde em poucos segundos.
- Distingue degradado de em baixo: warn, Degraded ou UNKNOWN para o que funciona a meio.
- O código HTTP é coerente com o corpo: 503 quando o estado é em baixo.
- Em Node, o @aaa-sentinel/health trata do formato, dos tempos limite e do código HTTP em poucas linhas.