Skip to content

Observabilidade

O Khronos emite três coisas diferentes, e confundi-las leva a números errados:

SaídaFerramentaPara quê
Eventos estruturados (logrus)GraylogReconstruir um login passo a passo
Métricas (Prometheus)GrafanaAgregados, alertas, SLO
Log operacional (slog)stdoutRequisições HTTP, erros de bootstrap, cron

Trace id

Cada requisição HTTP registrada no huma recebe um trace_id no RequestLogger, que:

  1. escreve o header X-Trace-ID na resposta (antes e depois do handler);
  2. injeta o valor no contexto sob constant.ContextKeyTraceID;
  3. é propagado nas chamadas de saída por utils.SetTraceHeader(request).

O cron gera o seu próprio (uuid.New()) e injeta no contexto da mesma forma, então um refresh em background também é rastreável de ponta a ponta.

Para investigar um login:

trace_id:"6f6c6f2d-…"

Eventos de login

Formato

Todo evento tem a mesma forma, produzida por emitProviderEvent em internal/services/events.go:

json
{
  "message":  "khronos-latam-event",
  "event":    "latam.oauth",
  "step":     "oauth",
  "provider": "latam",
  "username": "12345678900",
  "trace_id": "6f6c6f2d-…",
  "outcome":  "error",
  "duration_ms": 1834,
  "status_class": "4xx",
  "error_code": "invalid_credentials",
  "error": "error on latam login response status 401 Unauthorized | body {...}"
}
  • messagekhronos-{provider}-event. Um por companhia, o que torna o stream selecionável só pela mensagem.
  • event{provider}.{step}. Identifica a observação sozinho.
  • step — o estágio. Antes os dois campos guardavam a mesma string, deixando um deles sem informação.

Campos permitidos

A construção do evento usa uma allowlist, não os campos que o chamador passou:

go
var eventFieldAllowlist = []string{
    "outcome", "duration_ms", "status_class", "login_path",
    "attempt", "retried_step", "error_code", "error",
}

É o que garante que credenciais, payloads, tokens, cookies e URLs não cheguem ao Graylog — mesmo que um chamador futuro coloque isso no mapa de fields.

Steps

stepEmitido porSignificado
requestHandlerO evento terminal, um por requisição
cache_get / cache_set / cache_deleteServicesOperações de cache
path_selectedServicesQual dos três caminhos foi tomado
loginServicesChamada de login na companhia
oauth / oauth_refresh / oauth_mfaServicesChamadas OAuth
verify / verify_mfaLATAMVerificação do código MFA
authenticators / challengeSmilesDescoberta e disparo do canal de MFA
mfa_loginSmilesA troca de MFA inteira
document_statusSmilesConsulta de bloqueio da conta
guest_loginSmilesGeração de cookies
xp_sessionLATAMGeração de cookies
sessionAzulMontagem do cookie de sessão
session_checkAzulA sessão em cache ainda vive?
accountAzulConsulta de dados da conta
login_retryAzulUma retentativa começou
pigeon_get_mfa_codePigeonBusca do código, no fluxo da companhia

Outcomes

outcomeSignifica
successDeu certo
errorFalhou (e só nesse caso há error_code e error)
skippedO passo não rodou nesta execução
hit / miss / invalid / expiredEstados de leitura de cache
validVerificação de sessão passou (Azul)
retriedUma retentativa foi disparada

miss, invalid e expired não são erro

Eles não carregam error_code. Marcá-los como erro tornava error_code:* inutilizável como filtro de falhas — toda expiração normal aparecia como incidente.

O evento terminal

Cada requisição de login emite exatamente um {provider}.login.completed, no defer do handler:

go
defer func() {
    services.EmitLoginCompleted(ctx, constant.CiaLatam, input.Body.Username, started, err)
}()

Conte erros só no evento terminal

Os eventos de passo dizem onde a requisição falhou; o terminal diz que ela falhou, uma vez. Somar os dois conta a mesma falha duas ou três vezes. Consultas de taxa de erro devem filtrar event:"*.login.completed".

error_code vs error

  • error_code — o bucket, vindo de metrics.ClassifyLoginError. É o campo para agregar.
  • error — o texto real do que quebrou, redigido. É o campo para ler.

Quando um passo mapeia a falha do upstream em uma sentinela, a causa original é preservada com stepFieldsWithCause(err, cause, started): error_code vem do erro retornado (mantendo a classificação e o status HTTP), enquanto error carrega o texto que o mapeamento descartaria.

Sem isso, o log lia Usuario e/ou senha invalido(s) e nunca mostrava a resposta por trás.

Redaction

Todo erro que vai para log passa por utils.RedactError. Ele mascara:

PadrãoExemplo
Chaves sensíveis em JSON"password":"[REDACTED]"
Pares chave=valorkeyToken=[REDACTED]
Credenciais em URLhttps://[REDACTED]@proxy:8080

A lista de chaves cobre password, access_token, refresh_token, id_token, mfa_token, key_token, client_secret, authorization, cookie, set-cookie, proxy_url, token e as variações em camelCase — porque os upstreams usam as duas grafias e \b não separa keyToken.

O resultado é truncado em 512 bytes, em fronteira de rune válida, para que uma resposta grande do upstream não inunde o Graylog com um único campo.

Chaves mais longas vêm antes

A ordem da alternação no regex é intencional: access_token precisa vir antes de token, senão o mascaramento pegaria só o sufixo da chave.

Log operacional (slog)

O RequestLogger escreve uma linha por requisição:

[6f6c6f2d-…] 2026/08/07 - 12:00:00 | 200 |    1.234567s |    10.0.0.1:52344 | POST | /latam/login

Também usam slog: o bootstrap (starting API, connected to redis), o graceful shutdown e o cron.

Onde procurar o quê

PerguntaOnde
"Por que este login específico falhou?"Graylog, trace_id:"…"
"Quantos logins da Smiles falharam na última hora?"Prometheus, khronos_login_errors_total
"Estamos sendo bloqueados pelo antibot?"khronos_login_errors_total{reason="antibot"}
"O cache está sendo aproveitado?"khronos_login_path_total
"O Pigeon está devolvendo códigos?"khronos_dependency_operations_total{dependency="pigeon"}
"Este CPF tem sessão?"POST /{cia}/exists-session

Documentação interna — 123milhas