Logging Estruturado em Go com slog para Observabilidade e Alertas
Logs JSON consultáveis que se conectam a traces.
Os logs são uma interface de depuração que você ainda pode usar quando o sistema está em chamas. O problema é que os logs em texto puro envelhecem mal: assim que você precisa de filtragem, agregação e alertas, você começa a analisar frases.

O registro estruturado é o antídoto. Ele transforma cada linha de log em um pequeno evento com campos estáveis, para que as ferramentas possam pesquisar e agregar de forma confiável. Para entender como os logs se conectam a métricas, painéis e alertas na stack mais ampla, consulte o Guia de Observabilidade: Monitoramento, Métricas, Prometheus e Grafana.
O que é registro estruturado e por que ele escala
O registro estruturado é uma forma de logging onde um registro não é apenas uma string, mas uma mensagem mais atributos de pares chave-valor tipados. A ideia é chata da melhor forma possível: uma vez que os logs são legíveis por máquina, um incidente deixa de ser uma competição de grep.
Uma comparação rápida:
Texto puro (focado no humano, hostil à ferramenta)
failed to charge card user=42 amount=19.99 ms=842 err=timeout
Estruturado (focado na ferramenta, ainda legível)
{"msg":"failed to charge card","user_id":42,"amount":19.99,"duration_ms":842,"error":"timeout"}
Em produção, ajuda pensar nos logs como um fluxo de eventos emitidos pelo processo, enquanto o roteamento e o armazenamento ficam fora da aplicação. Esse modelo mental conduz você a escrever um evento por linha e manter os eventos fáceis de enviar e reprocessar.
Slog no Go como uma interface de logging compartilhada
O Go tem o pacote clássico log desde sempre, mas serviços modernos precisam de níveis e campos. O pacote log/slog (Go 1.21 e posterior) traz o registro estruturado para a biblioteca padrão e formaliza uma forma comum para registros de log: hora, nível, mensagem e atributos. Para um resumo compacto de linguagem e comandos ao lado deste guia, consulte o Go Cheatsheet.
As partes principais do modelo são:
Registro (Record)
Um registro é o que aconteceu. Nos termos do slog, ele contém hora, nível, mensagem e um conjunto de atributos. Você cria registros via métodos como Info e Error, ou via Log quando deseja fornecer o nível explicitamente.
Atributos
Atributos são os pares chave-valor que tornam os logs pesquisáveis. Se você registrar o mesmo conceito sob três chaves diferentes (user, userId, uid), você terá três conjuntos de dados diferentes. Chaves consistentes é onde o verdadeiro valor reside.
Handler (Manipulador)
Um handler é como os registros se tornam bytes. O TextHandler integrado escreve saída key=value, enquanto o JSONHandler escreve JSON delimitado por linha. Handlers são também onde a redação, renomeação de chaves e roteamento de saída tendem a acontecer.
Uma característica subestimada é que o slog pode ficar na frente do código existente. Quando você define um logger slog padrão, as funções slog de alto nível o usam, e o pacote clássico log também pode ser redirecionado para ele. Isso torna a migração incremental possível.
Grupos
Grupos resolvem o problema de “cada subsistema usa id”. Você pode agrupar um conjunto de atributos para uma solicitação (request.method, request.path) ou nomear um subsistema inteiro com WithGroup para que as chaves não colidam.
Uma configuração de slog moldada para produção
O seguinte setup atinge os objetivos habituais.
Os exemplos usam um pequeno pacote logx; para saber onde pacotes como esse costumam ficar em um módulo real, consulte Estrutura de Projetos Go: Práticas e Padrões.
- um evento JSON por linha
- logs escritos para stdout para coleta
- metadados estáveis do serviço anexados uma vez
- logging ciente do contexto para IDs de solicitação e rastreamento
- redação centralizada para chaves sensíveis
package logx
import (
"log/slog"
"os"
)
var level slog.LevelVar // padrão para INFO
func New() *slog.Logger {
opts := &slog.HandlerOptions{
Level: &level, // pode ser alterado em tempo de execução
AddSource: true, // incluir arquivo e linha quando disponível
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Redação centralizada: consistente e difícil de contornar acidentalmente.
switch a.Key {
case "password", "token", "authorization", "api_key":
return slog.String(a.Key, "[redacted]")
}
return a
},
}
h := slog.NewJSONHandler(os.Stdout, opts)
return slog.New(h).With(
"service", os.Getenv("SERVICE_NAME"),
"env", os.Getenv("ENV"),
"version", os.Getenv("VERSION"),
)
}
func SetLevel(l slog.Level) { level.Set(l) }
Um detalhe pequeno com grandes consequências: o handler JSON integrado usa chaves padrão (time, level, msg, source). Quando seu backend de logs espera um esquema diferente, ReplaceAttr é a válvula de alívio que permite normalizar chaves sem reescrever os locais de chamada.
O esquema importa mais que o logger
A maioria das falhas de “registro estruturado” são falhas de esquema.
Campos essenciais que continuam pagando o aluguel
Todo backend de log armazenará um timestamp, nível e mensagem. Na prática, um esquema de aplicativo útil frequentemente adiciona um pequeno conjunto de campos estáveis:
- service, env, version
- component (ou subsistema)
- event (um nome estável para o evento que ocorreu)
- request_id (quando uma solicitação existe)
- trace_id e span_id (quando o rastreamento existe)
- error (string) e error_kind (bucket estável)
Note o padrão: esses campos respondem a perguntas operacionais, não à curiosidade do desenvolvedor.
Convenções semânticas são um hack de consistência barato
Se você já usa OpenTelemetry, suas convenções semânticas fornecem um vocabulário padrão para atributos em todos os sinais de telemetria. Mesmo que você não exporte logs via OpenTelemetry, emprestar nomes de atributos reduz o imposto do “como chamamos este campo no serviço B”.
Alta cardinalidade e por que os logs ficam caros
Alta cardinalidade significa “muitos valores únicos”. Está tudo bem dentro de um payload JSON, mas torna-se doloroso quando um backend trata alguns campos como rótulos indexados ou chaves de fluxo. IDs de usuário, endereços IP, tokens de solicitação aleatórios e URLs completos tendem a explodir combinações.
O resultado prático é simples: mantenha rótulos e chaves de índice simples (serviço, ambiente, região) e mantenha campos de alta cardinalidade dentro do payload estruturado para filtragem no momento da consulta.
Correlação com IDs de solicitação e rastreamentos
A correlação é o ponto onde os logs deixam de ser apenas texto e começam a se comportar como telemetria.
Request ID como a chave de correlação de menor atrito
Um Request ID é a ponte mais simples entre uma solicitação recebida e tudo o que acontece por causa dela. Ele tende a funcionar mesmo sem rastreamento distribuído, e ainda é útil quando os rastreamentos são amostrados. Para a imagem completa de como os IDs de solicitação e outros metadados devem ser armazenados e recuperados do contexto — incluindo padrões de chaves tipadas e exemplos de middleware — consulte Go context.Context Done Right.
Um padrão comum é anexar um logger por solicitação ao contexto:
package logx
import (
"context"
"log/slog"
)
type ctxKey struct{}
func WithLogger(ctx context.Context, l *slog.Logger) context.Context {
return context.WithValue(ctx, ctxKey{}, l)
}
func FromContext(ctx context.Context) *slog.Logger {
if l, ok := ctx.Value(ctxKey{}).(*slog.Logger); ok && l != nil {
return l
}
return slog.Default()
}
Correlação de rastreamento com W3C Trace Context e OpenTelemetry
O W3C Trace Context define uma maneira padrão de propagar identidade de rastreamento (para HTTP, via traceparent e tracestate). O OpenTelemetry se baseia nisso para que IDs de trace e span IDs possam ser extraídos do contexto.
Este exemplo de middleware registra tanto request_id quanto identificadores de rastreamento quando disponíveis:
package middleware
import (
"crypto/rand"
"encoding/hex"
"net/http"
"go.opentelemetry.io/otel/trace"
"log/slog"
"example.com/project/logx"
)
func requestID() string {
var b [16]byte
_, _ = rand.Read(b[:])
return hex.EncodeToString(b[:])
}
func WithRequestLogger(base *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rid := r.Header.Get("X-Request-Id")
if rid == "" {
rid = requestID()
}
l := base.With(
"request_id", rid,
"method", r.Method,
"path", r.URL.Path,
)
if sc := trace.SpanContextFromContext(r.Context()); sc.IsValid() {
l = l.With(
"trace_id", sc.TraceID().String(),
"span_id", sc.SpanID().String(),
)
}
ctx := logx.WithLogger(r.Context(), l)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
Uma vez que os campos de correlação existem, a linha de log se torna um índice para outros dados. A diferença em um incidente ao vivo não é sutil.
Transformando logs estruturados em sinais de monitoramento e alerta
Os logs são ótimos para responder “o que aconteceu”. O alerta geralmente trata de “com que frequência e quão grave”.
Uma abordagem prática é tratar certos eventos de log como contadores:
- event=payment_failed
- event=db_timeout
- event=cache_miss
Muitas plataformas podem derivar métricas baseadas em logs contando registros correspondentes em uma janela. Logs estruturados tornam essa contagem resiliente, porque é baseada em um valor de campo em vez de uma correspondência de texto frágil. Quando você estiver pronto para visualizar e explorar esses sinais, Instalar e Usar Grafana no Ubuntu: Guia Completo percorre uma configuração completa do Grafana que você pode apontar para backends comuns de logs e métricas.
É também aqui que os níveis de log começam a importar. Logs de Debug frequentemente têm valor, mas também é onde custo e ruído se escondem. Usar um nível dinâmico (LevelVar) permite que o sistema fique quieto por padrão, enquanto ainda permite detalhes direcionados quando necessário.
Pensamentos finais
O registro estruturado no Go não é mais um debate de bibliotecas. A parte interessante é se seus registros de log são consistentes, correlacionáveis e acessíveis para armazenar.
Quando seus logs carregam campos estáveis como event, request_id e trace_id, eles deixam de ser “strings que alguém escreveu” e começam a ser um conjunto de dados que você pode operar.
Notas
A equipe do Go introduziu log/slog no Go 1.21 e enfatizou que logs estruturados usam pares chave-valor para que possam ser analisados, filtrados, pesquisados e analisados de forma confiável, e também notou a motivação de fornecer um framework comum compartilhado em todo o ecossistema.
A documentação do pacote log/slog define o modelo de registro (hora, nível, mensagem, pares chave-valor) e os handlers integrados (TextHandler para key=value e JSONHandler para JSON delimitado por linha), e documenta a integração SetDefault com o pacote clássico log.
Para correlação distribuída, a especificação W3C Trace Context padroniza a propagação de traceparent e tracestate, e o OpenTelemetry especifica que seu SpanContext conforma com o W3C Trace Context e expõe TraceId e SpanId, tornando a correlação log-rastreamento direta quando um span está presente.
Para custo de armazenamento de logs e desempenho, a documentação do Grafana Loki recomenda fortemente rótulos estáticos e delimitados e alerta sobre rótulos de alta cardinalidade criando muitos streams e um índice enorme, o que é diretamente relevante ao decidir o que se torna um rótulo versus o que permanece como um campo JSON não indexado.