Logging estruturado (slog)
LidoEsta aula ensina logging estruturado em Go com o pacote padrão log/slog, cobrindo desde a criação de loggers básicos até o uso de níveis, handlers personalizados e boas práticas para produzir logs consistentes e úteis em produção.
O logging é uma parte essencial de qualquer aplicação, pois fornece visibilidade sobre o comportamento do sistema em tempo de execução. Em Go, o pacote padrão log/slog (introduzido na versão 1.21) oferece uma abordagem estruturada e moderna para registrar eventos, permitindo que você emita logs em formato chave-valor, facilitando a análise e o monitoramento. Nesta aula, você aprenderá a usar o slog para criar logs claros, com níveis de severidade, e a customizar a saída com handlers. Ao final, terá as ferramentas para implementar um logging eficaz em seus projetos Go.
O slog foi projetado para ser rápido, flexível e integrado ao ecossistema Go, substituindo práticas antigas de formatação de strings com fmt.Printf. Ele permite que você registre informações de forma estruturada, o que é especialmente importante em sistemas distribuídos e microsserviços, onde logs precisam ser facilmente pesquisáveis e correlacionados.
log/slog
O pacote log/slog fornece uma API para logging estruturado. A função mais básica é slog.Info, que registra uma mensagem no nível de informação. Veja um exemplo simples:
package main
import (
"log/slog"
)
func main() {
slog.Info("Servidor iniciado", "porta", 8080, "ambiente", "produção")
}A saída padrão é em formato de texto, mas pode ser configurada para JSON. O slog também oferece funções para outros níveis, como Debug, Warn e Error. Para criar um logger com configurações específicas, usamos slog.New com um handler (veremos na seção Handlers). O slog também suporta contextos, permitindo incluir informações de correlação (ex.: ID de requisição) automaticamente.
Um recurso importante é a possibilidade de adicionar atributos em grupo, usando slog.Group, e de registrar erros com slog.Error, que aceita um erro como argumento e o serializa adequadamente. O pacote também oferece slog.Log para controle fino do nível e da mensagem.
Níveis
Os níveis de log são fundamentais para filtrar a quantidade de informação. O slog define quatro níveis padrão: Debug, Info, Warn e Error, em ordem crescente de severidade. Você pode usar esses níveis para indicar a importância de cada mensagem. Por exemplo, mensagens de debug são úteis durante o desenvolvimento, mas raramente devem ser exibidas em produção. Para controlar quais níveis são registrados, você pode configurar o handler com um nível mínimo.
Exemplo de uso de níveis:
slog.Debug("Detalhe de variável", "valor", 42)
slog.Info("Operação concluída")
slog.Warn("Recurso quase esgotado", "uso", 90)
slog.Error("Falha ao conectar", "erro", err)Para definir um nível mínimo, use slog.LevelVar e ajuste dinamicamente. Isso é útil em ambientes onde você quer aumentar a verbosidade em tempo de execução, por exemplo, para depuração.
Handlers
Handlers são responsáveis por formatar e escrever os logs. O slog oferece dois handlers padrão: slog.NewTextHandler (saída em texto) e slog.NewJSONHandler (saída em JSON). Você pode criar um logger com um handler específico:
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("Exemplo JSON")Além dos handlers padrão, você pode implementar um handler personalizado implementando a interface slog.Handler. Isso permite controlar a formatação, o destino e até mesmo adicionar informações contextuais. Para implementar um handler, você deve implementar os métodos Enabled, Handle e WithAttrs e WithGroup.
Exemplo de um handler personalizado que adiciona um timestamp em formato específico:
type customHandler struct {
out io.Writer
attrs []slog.Attr
}
func (h *customHandler) Enabled(ctx context.Context, level slog.Level) bool {
return level >= slog.LevelInfo
}
func (h *customHandler) Handle(ctx context.Context, r slog.Record) error {
// Formata a saída
_, err := fmt.Fprintf(h.out, "%s [%s] %s", r.Time.Format("2006-01-02 15:04:05"), r.Level, r.Message)
if err != nil {
return err
}
for _, attr := range h.attrs {
fmt.Fprintf(h.out, " %s=%v", attr.Key, attr.Value)
}
r.Attrs(func(a slog.Attr) bool {
fmt.Fprintf(h.out, " %s=%v", a.Key, a.Value)
return true
})
fmt.Fprintln(h.out)
return nil
}
func (h *customHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &customHandler{out: h.out, attrs: append(h.attrs, attrs...)}
}
func (h *customHandler) WithGroup(name string) slog.Handler {
// Implementação simplificada
return h
}Na prática, você raramente precisará implementar um handler do zero, mas é útil entender como eles funcionam para customizar o comportamento do logging.
Boas práticas
Ao usar slog, siga estas boas práticas para obter logs úteis e consistentes:
- Use níveis adequados:
Debugpara detalhes de desenvolvimento,Infopara eventos normais,Warnpara situações anormais eErrorpara falhas. - Mantenha as mensagens claras e objetivas, evitando informações sensíveis como senhas.
- Inclua contexto relevante: use
slog.Grouppara agrupar atributos eWithpara adicionar campos fixos (ex.: serviço, versão). - Prefira JSON em produção para facilitar a integração com ferramentas de análise.
- Evite logar em loops de alta frequência sem controle, pois pode impactar a performance.
- Use
slog.Errorpara registrar erros, passando o erro como argumento, para que ele seja serializado corretamente.
Exemplo de uso com contexto e grupos:
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger = logger.With("serviço", "api")
logger.Info("Requisição recebida",
slog.Group("request",
slog.String("method", "GET"),
slog.String("path", "/users"),
),
slog.Int("status", 200),
)Essas práticas ajudarão a manter seus logs legíveis e acionáveis.
Referências
- Documentação oficial do pacote log/slog
- Anúncio do log/slog no blog oficial da Go
- Go by Example: Slog
- How To Use Structured Logging in Go (DigitalOcean)
- Go Wiki: Logging
- Structured Logging in Go with slog (Medium)
Exercícios
Crie um programa que use
slogpara registrar uma mensagem de info e uma de erro, com pelo menos dois atributos cada.✓ Resposta:package main import ( "log/slog" ) func main() { slog.Info("Usuário logado", "user_id", 123, "ip", "192.168.1.1") slog.Error("Falha ao salvar arquivo", "file", "data.txt", "error", "permissão negada") }Configure um logger que produza saída em JSON e registre uma mensagem de warning com um grupo de atributos.
✓ Resposta:package main import ( "log/slog" "os" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) logger.Warn("Recurso quase esgotado", slog.Group("resource", slog.String("type", "memory"), slog.Float64("usage", 0.95), ), ) }Implemente um handler personalizado que adicione um prefixo "APP: " à mensagem e escreva em um buffer (use bytes.Buffer) em vez de os.Stdout.
✓ Resposta:package main import ( "bytes" "context" "fmt" "log/slog" ) type prefixHandler struct { out *bytes.Buffer } func (h *prefixHandler) Enabled(ctx context.Context, level slog.Level) bool { return true } func (h *prefixHandler) Handle(ctx context.Context, r slog.Record) error { _, err := fmt.Fprintf(h.out, "APP: %s %s\n", r.Level, r.Message) return err } func (h *prefixHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return h } func (h *prefixHandler) WithGroup(name string) slog.Handler { return h } func main() { var buf bytes.Buffer logger := slog.New(&prefixHandler{out: &buf}) logger.Info("Teste") fmt.Println(buf.String()) }Crie um logger com nível mínimo configurável usando
slog.LevelVare altere o nível em tempo de execução para suprimir mensagens de debug.✓ Resposta:package main import ( "log/slog" "os" ) func main() { levelVar := new(slog.LevelVar) levelVar.Set(slog.LevelDebug) handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: levelVar}) logger := slog.New(handler) logger.Debug("Mensagem de debug visível") levelVar.Set(slog.LevelInfo) logger.Debug("Mensagem de debug suprimida") }Escreva uma função que receba um
slog.Loggere registre uma mensagem de erro com contexto de usuário e requisição, usando grupos.✓ Resposta:package main import ( "log/slog" ) func logError(logger *slog.Logger, userID int, path string, err error) { logger.Error("Erro na requisição", slog.Int("user_id", userID), slog.Group("request", slog.String("method", "GET"), slog.String("path", path), ), slog.Any("error", err), ) } func main() { logger := slog.Default() logError(logger, 42, "/api/users", fmt.Errorf("connection refused")) }