0026 — Heartbeat do integrador-client: repasse ao central¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-10 · Decidido em: 2026-09-10
Contexto¶
O integrador-client passou a mandar um heartbeat (versão, fluxos, hostname) para que o central-backend
saiba quais instalações estão vivas e alerte o silêncio. O porquê da feature inteira — rota em dois
saltos por esta instância (o firewall do cliente só libera int.<cliente>), contrato congelado entre os
quatro repos, alerta por horas úteis — é da
decisão local 0028 do central-backend.
Esta decisão registra só o que é deste salto: como o Integrador recebe, repassa e responde.
O salto tem um problema de observabilidade. Ele tem duas classes de falha com pesos opostos:
misconfiguração local (env vazia, CLIENTE_ID errado, token divergente), que precisa virar ERROR no
GlitchTip, e o central fora do ar, que é transitório, se repete a cada hora em cada instância e não pode
virar issue. O caminho padrão da casa (lançar exceção e deixar o UnifiedErrorResponseProcessor
renderizar) loga todo 5xx em ERROR e devolve o nome do status como code.
Decisão¶
- Recebe, carimba e repassa sem gravar.
POST /api/v1/heartbeatfica sob o BearerINTEGRADOR_API_TOKENde/api/**, valida o corpo pela tabela do contrato e acrescenta ocliente_idda instância (CLIENTE_ID, a identidade vem do contexto e nunca do corpo). Depois repassa ao central emPOST {CENTRAL_URL}/api/integrador/heartbeat, BearerCENTRAL_API_TOKEN. O DTO de saída é fechado e sai comnull/[]explícitos. - Erros montados no controller. O
HeartbeatControllerdevolveProblemDetailà mão, como oViewRejectionHandler, e escolhe o nível do log:503 heartbeat_desligadoe502 heartbeat_recusadosaem com ERROR;502 central_indisponivelsai com WARN. É a segunda exceção deliberada ao processor neste repo, depois do/api/v1/xadm(erros). O400de validação continua sendo do processor. - Env vazia responde 503; não recusa o boot.
CENTRAL_API_TOKENé segredo de saída: ausente, o app degrada com comportamento declarado (norma engenharia/seguranca). A recusa de boot vale para o validador de entrada. As instâncias sem integrador-client ficam com as duas envs vazias, sem custo. - Sem flag de desligar e sem retry. O desligamento é no client (
heartbeat.intervaloMinutos=0). Perder um heartbeat é irrelevante, porque o próximo cobre — por isso o@Clientnão leva@Retryable. - O 404 do central é conferido. O cliente declarativo do Micronaut não lança exceção no 404: com
retorno
voidele some; comHttpResponse, volta como resposta. Por isso o cliente devolveHttpResponsee o controller confere o status. Sem isso, umaCENTRAL_URLerrada (o proxy responde 404 em HTML) passaria como sucesso.
Alternativas preteridas¶
- Lançar
ServerException/HttpStatusExceptione deixar o processor responder. Mantém um caminho só, mas o central fora viraria uma issue por hora em cada instância, e ocodesairiaBAD_GATEWAYem vez dos códigos do contrato. - Recusar o boot sem
CENTRAL_API_TOKEN/CLIENTE_ID. Obrigaria as instâncias sem integrador-client a cadastrar segredo que não usam, e inverteria a norma de segredo de saída. - Fallback do
CLIENTE_IDpara oCLIENTE. OCLIENTEé nome de exibição (Maxsul) e nunca passa na regra do central; o fallback esconderia a misconfiguração atrás de um erro de validação. @Retryableno cliente. Custa latência para o client e não compra nada: o heartbeat seguinte já é a nova tentativa.
Consequências¶
- Os ERRORs do heartbeat são sempre misconfiguração, e o runbook os mapeia um a um (incidentes comuns).
- Ordem de release obrigatória: este server com a rota antes do jar do client. Ao contrário, cada heartbeat vira 404 autenticado, que a lib loga em ERROR (implantação).
- Mudar o contrato do salto é mudança nos três repos ao mesmo tempo, com as fixtures
src/test/resources/contrato/de cada um.