Quase todos os serviços têm um /health. Um monitor chama-o a cada minuto, recebe um 200 e pinta um círculo verde. Num servidor tradicional isso dizia alguma coisa: se o processo responde, o processo está vivo. Em serverless, o endpoint de saúde costuma ser apenas mais uma função, que não partilha processo nem memória com as que fazem o trabalho a sério. Pode responder 200 durante horas enquanto as encomendas ficam paradas numa fila. Neste artigo vemos que falhas lhe escapam, o que fazer em vez disso e como o AAA Sentinel resolve o problema.
O que um 200 verifica de facto
Este é o /health mais habitual numa API sobre AWS Lambda. Se responde, sabem-se três coisas: que o API Gateway encaminha, que essa função arranca e que o seu código não lança uma exceção. Nada mais. Não tocou na base de dados, não sabe se a fila avança e não faz ideia de se a função que consome essa fila está a falhar há uma hora.
export const handler = async () => ({
statusCode: 200,
body: JSON.stringify({ status: 'ok' }),
})Em serverless, o endpoint de saúde e o código que processa os dados são funções diferentes, cada uma com as suas permissões, limites de concorrência e erros. Uma estar saudável não diz nada sobre a outra.
Cinco falhas que um 200 não vê
São as que mais se repetem em arquiteturas orientadas a eventos, e nenhuma altera a resposta do /health:
- A fila que cresce: o consumidor é mais lento do que aquilo que entra, ou está limitado pela concorrência. A API aceita encomendas e responde 202; o trabalho é feito com meia hora de atraso.
- As novas tentativas silenciosas: uma invocação assíncrona de Lambda que falha é repetida, por omissão, até mais duas vezes. Uma mensagem de SQS que não é apagada volta a ficar visível quando expira o tempo de visibilidade e é processada de novo. O sistema parece funcionar, com o dobro ou o triplo do trabalho e do custo.
- A fila de mensagens mortas que enche: quando uma mensagem esgota as receções (maxReceiveCount), o SQS move-a para a DLQ. Ninguém dá por isso até alguém a abrir, e cada mensagem lá dentro é algo que não aconteceu.
- O evento que ninguém consome: o PutEvents do EventBridge devolve sucesso mesmo que nenhuma regra corresponda ao evento. Basta mudar o nome do campo source ou desativar uma regra, e os eventos perdem-se sem um único erro.
- A dependência em baixo: o fornecedor de pagamentos ou a base de dados falham, mas o /health não os chama, por isso continua verde.
Primeira peça: um contrato de saúde por componente
Se o endpoint de saúde vai existir, que diga algo útil. Há um formato para isso: health+json, um rascunho do IETF que usa o tipo application/health+json. A resposta leva um status global (pass, warn ou fail), a version do serviço e um objeto checks com uma lista por componente, cada um com o seu componentType, o seu observedValue, o seu status e a hora da medição (time). A chave de cada check costuma seguir a forma componente:medida.
HTTP/1.1 200 OK
Content-Type: application/health+json
Cache-Control: no-store
{
"status": "warn",
"version": "1.42.0",
"checks": {
"orders-table:responseTime": [
{
"componentType": "datastore",
"observedValue": 38,
"observedUnit": "ms",
"status": "pass",
"time": "2026-10-18T09:30:00.000Z"
}
],
"orders-dlq:messages": [
{
"componentType": "component",
"observedValue": 3,
"status": "warn",
"time": "2026-10-18T09:30:00.000Z"
}
]
}
}- pass: o componente funciona.
- warn: funciona, mas algo não está bem. Aqui, há três mensagens na fila de mensagens mortas. O serviço está degradado, não em baixo, e responde 200.
- fail: o componente não funciona. O serviço responde 503 para que um balanceador de carga ou um monitor HTTP básico também o percebam.
Como gerá-lo sem escrever o formato à mão
O pacote @aaa-sentinel/health do npm, sem dependências, monta essa resposta a partir das verificações. Cada uma recebe um AbortSignal que dispara quando o tempo se esgota; o estado global é o pior de todos. Este handler produz exatamente a resposta anterior:
import { runHealthChecks } from '@aaa-sentinel/health'
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb'
import { SQSClient, GetQueueAttributesCommand } from '@aws-sdk/client-sqs'
const dynamo = new DynamoDBClient({})
const sqs = new SQSClient({})
// Lambda behind API Gateway or a function URL: the result is the HTTP response.
export const handler = () =>
runHealthChecks(
{
'orders-table:responseTime': async (signal) => {
const started = Date.now()
await dynamo.send(
new GetItemCommand({ TableName: process.env.ORDERS_TABLE, Key: { pk: { S: 'health' } } }),
{ abortSignal: signal },
)
return { componentType: 'datastore', observedValue: Date.now() - started, observedUnit: 'ms' }
},
'orders-dlq:messages': async (signal) => {
const { Attributes } = await sqs.send(
new GetQueueAttributesCommand({
QueueUrl: process.env.ORDERS_DLQ_URL,
AttributeNames: ['ApproximateNumberOfMessages'],
}),
{ abortSignal: signal },
)
const messages = Number(Attributes?.ApproximateNumberOfMessages ?? 0)
return { componentType: 'component', observedValue: messages, status: messages === 0 ? 'pass' : 'warn' }
},
},
{ version: process.env.APP_VERSION, timeoutMs: 2000 },
)- Uma verificação que lança um erro ou demora mais do que timeoutMs (2 s por omissão, 60 s no máximo) é um fail desse componente; o endpoint continua a responder.
- As mensagens de erro ficam fora da resposta, a menos que se passe exposeErrors: true. Costumam levar hosts ou cadeias de ligação, e os endpoints de saúde costumam ser públicos.
- minimumStatus: 'warn' mantém o serviço em warn ou pior, por exemplo durante uma manutenção.
- Quem já usa Spring Boot Actuator, ASP.NET Core Health Checks ou os endpoints do Kubernetes não precisa de mudar nada: o monitor de contrato de saúde do AAA Sentinel lê-os tal como estão.
Verifique-se o que só o código sabe: que a tabela responde com as permissões certas, que a chave do fornecedor continua válida. O que a nuvem já mede, lê-se da nuvem; é a peça seguinte.
Segunda peça: vigiar os recursos que estão por trás
Há coisas que um endpoint não consegue medir bem. A idade da mensagem mais antiga de uma fila, por exemplo, não é devolvida pelo GetQueueAttributes: está na métrica ApproximateAgeOfOldestMessage do CloudWatch. E é melhor sinal do que o número de mensagens, porque mil mensagens podem ser um pico normal e uma mensagem com quarenta minutos é sempre um problema. Por isso o AAA Sentinel tem monitores de recurso cloud: são passivos, não chamam nada do lado do cliente e leem as métricas através da ligação à nuvem que já foi dada para os custos.
Monitor de recurso cloud · o que se vigia na AWS
- Fila SQS
- Idade da mensagem mais antiga e mensagens na sua fila de mensagens mortas (qualquer mensagem lá já esgotou as tentativas)
- Função Lambda
- Erros, throttles e idade do iterador (IteratorAge) quando lê de um stream
- Tabela DynamoDB
- Leituras e escritas limitadas
- Regra do EventBridge
- Invocações falhadas para os seus destinos
- Step Functions
- Execuções falhadas ou com o tempo esgotado
- API Gateway
- Erros 5XX e de integração
- No Azure vigia Function Apps, Service Bus, Cosmos DB, API Management, tópicos do Event Grid e Logic Apps; no Google Cloud, Cloud Run, Cloud Functions, subscrições do Pub/Sub, triggers do Eventarc, Workflows, Firestore e API Gateway.
- O custo vai para a fatura do cliente, não para a nossa: na AWS, o GetMetricData fica em até cerca de 0,20 USD por recurso e por mês com leituras a cada 10 minutos. No Azure costuma caber no milhão de consultas gratuitas por mês, e no Google Cloud, no milhão de séries lidas gratuitas por mês.
Terceira peça: heartbeats para os crons
Uma tarefa agendada não tem endpoint para sondar. Se o agendamento do EventBridge Scheduler for desativado ou o script morrer a meio, não há erro para ver: simplesmente não acontece nada. A solução é inverter o sentido. O monitor de heartbeat dá uma URL; a tarefa chama-a quando termina bem e, se a chamada não chegar dentro da janela definida, o monitor passa a em baixo. Aceita GET e POST, porque muitos agendadores só sabem pedir uma URL.
#!/usr/bin/env bash
set -euo pipefail
node scripts/nightly-billing-export.js
# Only reached when the job succeeded: a failed or skipped run stays silent.
curl -fsS --max-time 10 --retry 3 "$HEARTBEAT_URL" > /dev/null// Scheduled Lambda (EventBridge Scheduler): the ping is the last line, after the real work.
export const handler = async () => {
await exportBilling()
await fetch(process.env.HEARTBEAT_URL, { method: 'POST', signal: AbortSignal.timeout(5000) })
}A URL deve ser chamada só quando a tarefa terminou bem, nunca no início. Um heartbeat no início diz que o cron arrancou, não que exportou a faturação.
Quarta peça: fluxos que indicam o passo partido
As três peças anteriores olham para componentes isolados. Um fluxo olha para a cadeia inteira: o AAA Sentinel dispara uma transação de teste (uma mensagem de SQS, uma invocação de Lambda, um evento do EventBridge ou uma chamada HTTP) com um traceId de 32 caracteres hexadecimais e um traceparent W3C, e verifica que chega a cada passo dentro do prazo. Um passo pode ser um item do DynamoDB, um objeto do S3, uma linha no CloudWatch Logs ou uma chamada do código do cliente a uma URL nossa. Há de 1 a 5 passos, cada um com um prazo entre 10 segundos e 15 minutos, e cada salto é cronometrado a partir do anterior. Os fluxos funcionam hoje na AWS e executam-se, no máximo, a cada 15 minutos, com um teto de custo mensal por fluxo.
import { propagateTrace } from '@aaa-sentinel/health'
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb'
const db = DynamoDBDocumentClient.from(new DynamoDBClient({}))
export const handler = async (event) => {
for (const record of event.Records) {
const trace = propagateTrace(record) // null for real traffic
if (trace) {
// Step 1 looks for ORDER#{traceId}; step 2 for the line "order stored {traceId}".
await db.send(new PutCommand({ TableName: process.env.ORDERS_TABLE, Item: { pk: `ORDER#${trace.traceId}`, test: true } }))
trace.log('order stored')
continue // no charge, no email for the test transaction
}
const order = JSON.parse(record.body)
await db.send(new PutCommand({ TableName: process.env.ORDERS_TABLE, Item: { pk: `ORDER#${order.id}`, ...order } }))
await chargeCustomer(order)
}
}Veredicto de um fluxo de encomendas que parte no passo 2
- Disparo
- Mensagem na fila orders, enviada às 09:30:00
- Passo 1 · item ORDER#{traceId} no DynamoDB
- Correto, 4 s após o disparo
- Passo 2 · linha «order stored {traceId}» no CloudWatch Logs
- Não chegou dentro do prazo de 60 s
- Passo 3 · objeto invoices/{traceId}.pdf no S3
- Não verificado
- O código passa o traceId ao passo seguinte, no cabeçalho x-aaa-trace ou como atributo da mensagem. O propagateTrace encontra-o em eventos de SQS, SNS, EventBridge, DynamoDB Streams, Kinesis e API Gateway ou nos cabeçalhos de um pedido HTTP, e nunca lança exceções.
- Só a marca do AAA Sentinel transforma um pedido num teste: um traceparent do próprio OpenTelemetry é tráfego real.
- O fluxo usa um papel à parte, com o seu modelo de CloudFormation, que só pode escrever nos recursos escolhidos pelo cliente.
- O evento que ninguém consome aparece aqui: o disparo corre bem e o primeiro passo nunca chega.
Como o AAA Sentinel decide que algo está em baixo
Um alerta falso às três da manhã ensina a equipa a ignorar os seguintes. Por isso, uma falha não alerta à primeira. Cada monitor passa por estes estados: operacional, suspeito, a confirmar, em baixo, a recuperar e operacional outra vez. A partir de suspeito nunca se avisa. Um URL volta a ser verificado a partir de uma segunda região da AWS (as sondas saem de eu-west-1 e a confirmação de eu-central-1); um recurso ou um fluxo tem de falhar várias vezes seguidas. Só o estado em baixo abre um incidente e envia o alerta, por e-mail, Slack, Microsoft Teams ou webhook assinado, com os mesmos canais e políticas dos alertas de custo.
- Um warn de health+json (ou um Degraded de ASP.NET) deixa o serviço degradado, não em baixo. Um UNKNOWN do Actuator não é uma falha.
- Os monitores novos passam 24 horas a aprender: observam, não alertam e propõem limiares.
- Os incidentes abrem e fecham sozinhos, com cronologia, notas, reconhecimento e um custo por minuto definido pela organização, na sua moeda.
- Cada sonda leva o cabeçalho X-AAA-Signature, assinado com um segredo da organização, para que a API a reconheça. Não publicamos IP fixos, por isso a assinatura é a forma de a identificar.
O que não faz
Para que se saiba onde estão os limites:
- Não é um APM: não ingere logs, não faz rastreio distribuído, não guarda métricas próprias nem tem painéis à medida.
- No Azure, o assistente de fluxos não descobre a cadeia: você indica onde o teste entra. No Google Cloud, o disparo entra por Pub/Sub ou HTTP.
- Os pacotes são para Node.js (@aaa-sentinel/health) e Python (aaa-sentinel-health). Em outras linguagens, o seu código lê a marca x-aaa-trace ou aaaTraceId por conta própria.
- Sonda a partir de duas regiões da AWS, não a partir de uma rede de localizações espalhadas pelo mundo.
Checklist para começar
Com estas peças no lugar, um círculo verde volta a significar que as encomendas chegam ao destino.
- O /health responde em health+json com um check por dependência, warn para o degradado e 503 para o que está em baixo.
- Cada fila tem a sua DLQ e um monitor sobre a idade da mensagem mais antiga.
- As funções que consomem eventos têm os erros e os throttles vigiados.
- Cada cron chama a sua URL de heartbeat quando termina bem.
- A cadeia que mais importa (a encomenda, o pagamento, a fatura) tem um fluxo que indica o passo que parte.
- Os 3 monitores http ou de heartbeat sem custo de qualquer plano cobrem o /health e o cron mais crítico; o pack de monitorização acrescenta o resto.