Pular para conteúdo

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/heartbeat fica sob o Bearer INTEGRADOR_API_TOKEN de /api/**, valida o corpo pela tabela do contrato e acrescenta o cliente_id da instância (CLIENTE_ID, a identidade vem do contexto e nunca do corpo). Depois repassa ao central em POST {CENTRAL_URL}/api/integrador/heartbeat, Bearer CENTRAL_API_TOKEN. O DTO de saída é fechado e sai com null/[] explícitos.
  • Erros montados no controller. O HeartbeatController devolve ProblemDetail à mão, como o ViewRejectionHandler, e escolhe o nível do log: 503 heartbeat_desligado e 502 heartbeat_recusado saem com ERROR; 502 central_indisponivel sai com WARN. É a segunda exceção deliberada ao processor neste repo, depois do /api/v1/xadm (erros). O 400 de 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 @Client não leva @Retryable.
  • O 404 do central é conferido. O cliente declarativo do Micronaut não lança exceção no 404: com retorno void ele some; com HttpResponse, volta como resposta. Por isso o cliente devolve HttpResponse e o controller confere o status. Sem isso, uma CENTRAL_URL errada (o proxy responde 404 em HTML) passaria como sucesso.

Alternativas preteridas

  • Lançar ServerException/HttpStatusException e deixar o processor responder. Mantém um caminho só, mas o central fora viraria uma issue por hora em cada instância, e o code sairia BAD_GATEWAY em 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_ID para o CLIENTE. O CLIENTE é 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.
  • @Retryable no 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.