Monitoramento18 de outubro de 2026 9 min de leitura

health+json, Spring Actuator e ASP.NET: um monitor que entende os três

Quase todos os serviços já têm um endpoint de saúde, mas cada ecossistema escreve-o à sua maneira. Percorremos o health+json, o Spring Boot Actuator, os Health Checks do ASP.NET Core e os endpoints do Kubernetes: o que devolvem, como se correspondem os estados, as armadilhas de configuração e como o contrato de saúde do AAA Sentinel os lê tal como estão.

Este artigo também está disponível em:esenfr

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.

GET /healthjson
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.

Resposta por omissãojson
GET /actuator/health

HTTP/1.1 200 OK

{ "status": "UP" }
Com componentes e detalhesjson
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:

application.ymlyaml
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.

Program.cscsharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks()
    .AddCheck<DatabaseHealthCheck>("database")
    .AddCheck<QueueHealthCheck>("queue");

var app = builder.Build();

app.MapHealthChecks("/health");

app.Run();
O que respondehttp
GET /health

HTTP/1.1 200 OK
Content-Type: text/plain

Degraded

O 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:

Program.cs · escritor própriocsharp
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,
            }),
    }),
});
Respostajson
{
  "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:

Program.cs · pacote da comunidadecsharp
// dotnet add package AspNetCore.HealthChecks.UI.Client
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
});
Respostajson
{
  "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.

Saída detalhadatext
GET /readyz?verbose

HTTP/1.1 500 Internal Server Error

[+]ping ok
[+]log ok
[-]etcd failed: reason withheld
[+]informer-sync ok
readyz check failed

Como 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á:

handler.ts · AWS Lambdats
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 },
  )
server.ts · Express ou Node httpts
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)
})
Respostajson
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.