Aparência
Visão geral
Caminho de uma requisição
POST /smiles/login
│
▼
┌────────────────────────────────────────────────┐
│ chi.Mux │
│ └─ MetricsMiddleware (bot-tools-go) │ latência/contagem HTTP
└────────────────────────────────────────────────┘
│
┌────────────────────────────────────────────────┐
│ huma API │
│ └─ RequestLogger │ gera/propaga trace id
└────────────────────────────────────────────────┘
│ valida o body contra o schema do Input
▼
┌────────────────────────────────────────────────┐
│ handlers.SmilesLoginHandler │
│ defer EmitLoginCompleted(...) │ evento terminal, 1 por requisição
└────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────┐
│ services.SmilesService.Login() │
│ cache → refresh → login novo │
│ emite eventos por passo, conta métricas │
└────────────────────────────────────────────────┘
│ │
▼ ▼
infra/redis requests/smiles/*.go
│
▼
serviço de automação SmilesMiddlewares
Só existem dois, e a ordem importa:
MetricsMiddleware— registrado nochi.Mux, antes da API huma. Cobre tudo, inclusive/metricse as rotas estáticas.middleware.RequestLogger— registrado na API huma (router.UseMiddleware). Cobre apenas as operações registradas viahuma.Register.
RequestLogger
go
traceID := utils.TraceIDFromContext(ctx.Context())
if traceID == "" {
traceID = uuid.New().String()
}
ctx.SetHeader("X-Trace-ID", traceID)
ctx = huma.WithValue(ctx, constant.ContextKeyTraceID, traceID)
next(ctx)
ctx.SetHeader("X-Trace-ID", traceID)Pontos relevantes:
- O header é escrito antes e depois do
next. Antes, para que respostas de erro também o carreguem; depois, para que o valor continue autoritativo caso a operação mexa nos headers. - O trace id vai para o contexto sob
constant.ContextKeyTraceID— um tipo próprio (type contextKey string), não uma string solta. - A partir daí,
utils.SetTraceHeader(request)propaga o mesmo id nas chamadas ao serviço de automação e ao Pigeon, via headerX-Trace-ID.
Trace id vindo de fora
O middleware lê o contexto, não o header da requisição. Na prática isso significa que o Khronos sempre gera um id novo por requisição HTTP; a reutilização acontece internamente (o cron, por exemplo, cria o seu com uuid.New() e injeta no contexto).
Registro de rotas
Todas as rotas passam por huma.Register, o que dá de graça: validação do body, geração do OpenAPI e mapeamento de huma.StatusError para o status HTTP correto.
go
huma.Register(api, huma.Operation{
OperationID: operationSmilesLogin,
Method: http.MethodPost,
Path: pathSmilesLogin,
Summary: summaryLogin,
Description: fmt.Sprintf(descriptionLogin, tagSmiles),
DefaultStatus: http.StatusOK,
Tags: []string{tagSmiles},
}, SmilesLoginHandler)Rotas com Hidden: true (/smiles/account-info, /smiles/account-statement, /smiles/account-exp-points, /smiles/dashboard) funcionam normalmente, mas não aparecem no OpenAPI nem no /docs.
Ciclo de vida
humacli cuida do start/stop:
go
hooks.OnStart(func() { http.ListenAndServe(":"+port, handler) })
hooks.OnStop(func() { time.Sleep(config.Options.AppGracePeriod) })O OnStop apenas dorme APP_GRACE_PERIOD — não há Shutdown graceful do servidor HTTP. A janela serve para o balanceador tirar o pod de rotação antes de o processo morrer.
Falha ao subir não mata o processo
Se ListenAndServe retornar erro, o código apenas registra dois slog.Error e retorna. O processo continua vivo, sem servidor — o pod não reinicia sozinho, mas falha o health check em /ping.
Concorrência
- O cliente Redis é criado sob
sync.Once(ensureConnection), então a conexão é única e segura para uso concorrente. - O registry de métricas também é
sync.Once. - Os services são criados por requisição (
NewSmilesService(ctx, params)) e não guardam estado compartilhado. - O
LatamServicemonta umhttp.Clientpróprio, comcookiejare política explícita de não seguir redirecionamentos (http.ErrUseLastResponse) — o fluxo da LATAM depende de ler o302e seus cookies. - O job do cron usa um lock distribuído (
SETNXemoauth:smiles:task:lock, TTL de 9 min) para que só uma réplica execute por vez.