Pular para o conteúdo

Logs estruturados e Loki

Intermediário16 min de leituraobservabilidade
Antes disto:

Log em texto livre é legível para uma pessoa lendo uma instância. Com trinta réplicas e milhões de linhas por hora, ele vira um arquivo grande onde você faz grep torcendo pelo melhor. Log estruturado é a mesma informação em campos — e a diferença aparece no dia em que você precisa de “todos os erros de pagamento do cliente X na última hora”.

Emita JSON com um conjunto pequeno de campos que nunca mudam de nome:

{"ts":"2026-09-04T09:12:44.812Z","level":"error","service":"pedidos","env":"prod",
"trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","msg":"falha ao autorizar pagamento",
"pedido_id":"9f3a","gateway":"cielo","http_status":502,"duracao_ms":1841}

Três regras que evitam retrabalho:

  • Uma mensagem por evento, com msg estável e o que varia em campos. msg com o valor interpolado impede agrupar por tipo de erro.
  • trace_id em todo log de requisição. É o que liga log e trace sem adivinhação.
  • Nada de segredo ou dado pessoal. Log costuma ir para mais lugares e durar mais que o banco; token, CPF e cabeçalho Authorization não entram. Redija na origem, não no destino.

ERROR é o que exige alguém agir, e por isso não pode ser o nível padrão de exceção esperada. WARN é degradação com recuperação automática. INFO marca transição de estado do sistema (subiu, migrou, recarregou config), não cada passo do fluxo. DEBUG fica desligado em produção, com chave para ligar por serviço quando precisar.

Se ERROR aparece mil vezes por minuto em operação normal, ele deixou de significar algo — e o alerta baseado nele já foi silenciado por alguém.

Loki não indexa o texto. Ele indexa labels e guarda o resto comprimido, o que o torna barato — desde que os labels sejam poucos e de baixa cardinalidade.

{namespace="loja", app="pedidos"} |= "pagamento" # filtro de linha
| json # extrai campos do JSON
| http_status >= 500 # filtra por campo extraído
| line_format "{{.pedido_id}} {{.gateway}} {{.msg}}"
# taxa de erro por gateway, a partir do log
sum by (gateway) (rate({app="pedidos"} | json | level="error" [5m]))

O erro clássico é promover pedido_id, trace_id ou pod_ip a label: cada valor cria um fluxo novo e o índice explode. Label é namespace, app, env, cluster. O resto é campo, extraído na consulta.

Com o trace_id no log e no span, a navegação vira um clique nos dois sentidos: do trace lento para os logs daquela requisição, e do log de erro para o trace que o produziu. Configure isso no datasource do Grafana (derivedFields no Loki) uma vez e ninguém mais copia identificador à mão.

Log é o pilar que mais cresce e o que mais desperdiça. Reduza na origem, nesta ordem:

  1. Amostre o repetitivo. Log de acesso com sucesso pode ir a 1 em 10; erro vai sempre.
  2. Corte o inútil. Health check, heartbeat e stack trace duplicado de biblioteca.
  3. Retenção por classe. 7 a 14 dias para depuração, mais tempo só para o que tem exigência legal ou de auditoria — e esse vai para armazenamento frio.
  4. Meça quem gera. Volume por serviço é conversa objetiva com o time dono.
topk(10, sum by (app) (rate({namespace="loja"}[1h]))) # quem produz mais linhas

O que você quer contar (quantos erros por minuto) é métrica, não log. Contar linha em consulta a cada painel custa caro; um counter na aplicação custa quase nada.

Próximo passo: quando o log diz “demorou 1841 ms” mas não diz onde, siga para Tracing distribuído.