Logging estruturado com zap ou zerolog
1. Por que logging estruturado em Go?
O pacote log padrão do Go é suficiente para aplicações simples, mas apresenta limitações significativas em sistemas distribuídos e microsserviços. Logs em texto solto como "2024-01-15T10:30:00Z Usuário 42 fez login" são difíceis de parsear, filtrar e analisar em ferramentas modernas.
O logging estruturado resolve esses problemas ao produzir saída em formato JSON ou similar, permitindo:
- Busca e filtragem precisa em ferramentas como Elasticsearch, Datadog e Grafana Loki
- Campos contextuais: adicionar metadados como requestID, userID e latência sem concatenação manual
- Análise automatizada: alertas baseados em campos específicos, agregações e dashboards
// Exemplo da diferença entre logging tradicional e estruturado
// log padrão - difícil de parsear
log.Printf("Usuário %d fez login com IP %s", userID, ip)
// logging estruturado - fácil de processar
logger.Info("login realizado",
zap.Int("userID", userID),
zap.String("ip", ip),
)
2. Introdução ao zap: rápido e tipado
O zap, mantido pela Uber, é conhecido por sua performance excepcional e API tipada que minimiza alocações.
Instalação e configuração básica
import "go.uber.org/zap"
func main() {
// Logger de produção - JSON, timestamp epoch
logger, _ := zap.NewProduction()
defer logger.Sync()
// Logger de desenvolvimento - texto colorido, mais verboso
devLogger, _ := zap.NewDevelopment()
defer devLogger.Sync()
logger.Info("servidor iniciado",
zap.String("porta", ":8080"),
zap.Int("workers", 10),
)
}
Logs tipados vs SugaredLogger
// Logger tipado - máximo desempenho, sem alocações desnecessárias
logger.Info("processando requisição",
zap.String("method", "POST"),
zap.Float64("duration_ms", 45.2),
zap.Int("status", 200),
)
// SugaredLogger - conveniência, similar ao fmt.Printf
sugar := logger.Sugar()
sugar.Infow("processando requisição",
"method", "POST",
"duration_ms", 45.2,
"status", 200,
)
A diferença principal: zap.Logger é mais rápido e seguro em tipos, enquanto zap.SugaredLogger oferece flexibilidade com printf-style.
3. Introdução ao zerolog: minimalista e fluente
O zerolog oferece uma API fluente e elegante, com foco em simplicidade e zero alocações.
Instalação e configuração básica
import "github.com/rs/zerolog/log"
func main() {
// Configuração global
zerolog.TimeFieldFormat = time.RFC3339Nano
// API fluente - encadeamento de métodos
log.Info().
Str("servico", "api").
Int("porta", 8080).
Bool("debug", true).
Msg("servidor iniciado")
// Níveis de log
log.Debug().Msg("iniciando conexão com banco")
log.Info().Msg("conexão estabelecida")
log.Warn().Msg("latência acima do esperado")
log.Error().Err(err).Msg("falha na consulta")
// log.Fatal().Msg("erro crítico") // encerra o programa
}
Níveis de log completos
// Debug - informações detalhadas para desenvolvimento
log.Debug().Str("query", "SELECT * FROM users").Msg("executando consulta")
// Info - eventos normais do sistema
log.Info().Int("users", count).Msg("usuários ativos carregados")
// Warn - situações anormais mas não críticas
log.Warn().Float64("latencia_ms", 2500).Msg("requisição lenta")
// Error - falhas que precisam de atenção
log.Error().Err(err).
Str("requestID", reqID).
Msg("falha ao processar pagamento")
// Fatal - erro grave que encerra o programa
log.Fatal().Msg("configuração inválida do banco de dados")
4. Configurações avançadas de saída e formato
Saída para arquivo com rotação
import (
"gopkg.in/natefinch/lumberjack.v2"
"go.uber.org/zap/zapcore"
)
func configurarLoggerArquivo() *zap.Logger {
writer := lumberjack.Logger{
Filename: "/var/log/app.log",
MaxSize: 100, // megabytes
MaxBackups: 3,
MaxAge: 28, // dias
}
encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
core := zapcore.NewCore(encoder, zapcore.AddSync(&writer), zapcore.InfoLevel)
return zap.New(core)
}
Timestamp customizado e níveis dinâmicos
// zerolog: timestamp customizado
zerolog.TimeFieldFormat = "2006-01-02 15:04:05.000"
zerolog.SetGlobalLevel(zerolog.InfoLevel)
// zap: nível atômico para alteração em runtime
atom := zap.NewAtomicLevel()
atom.SetLevel(zap.DebugLevel)
logger := zap.New(zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
os.Stdout,
atom,
))
5. Adicionando contexto e campos dinâmicos
Injeção de campos comuns via With()
// zap
logger := zap.L().With(
zap.String("service", "payment-api"),
zap.String("environment", "production"),
)
// zerolog
zerolog := log.With().
Str("service", "payment-api").
Str("environment", "production").
Logger()
Contexto de requisição HTTP
func middlewareLogContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := uuid.New().String()
ctx := context.WithValue(r.Context(), "requestID", requestID)
// zap
log := zap.L().With(zap.String("requestID", requestID))
ctx = context.WithValue(ctx, "logger", log)
// zerolog
zlog := zerolog.Ctx(r.Context()).With().
Str("requestID", requestID).
Logger()
ctx = zlog.WithContext(ctx)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
6. Integração com HTTP handlers e middleware
Middleware de logging completo
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Capturar status code
wrapper := &responseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(wrapper, r)
duration := time.Since(start)
// Log estruturado da requisição
log.Info().
Str("method", r.Method).
Str("path", r.URL.Path).
Int("status", wrapper.statusCode).
Float64("duration_ms", float64(duration.Microseconds())/1000).
Str("user_agent", r.UserAgent()).
Msg("request completed")
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
Testando logs com httptest
func TestHandlerLogs(t *testing.T) {
var buf bytes.Buffer
logger := zerolog.New(&buf)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger.Info().Str("path", r.URL.Path).Msg("request received")
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest("GET", "/api/users", nil)
handler.ServeHTTP(httptest.NewRecorder(), req)
// Assert no log gerado
var logEntry map[string]interface{}
json.Unmarshal(buf.Bytes(), &logEntry)
assert.Equal(t, "/api/users", logEntry["path"])
assert.Equal(t, "request received", logEntry["message"])
}
7. Comparação prática: zap vs zerolog
Benchmark de performance
| Biblioteca | Alocações/op | Tempo/op |
|---|---|---|
| zap.Logger | 0 | ~200ns |
| zerolog | 0 | ~300ns |
| logrus | 5 | ~800ns |
| log padrão | 3 | ~500ns |
Quando usar cada um
Use zap quando:
- Performance é crítica (alta throughput)
- Precisa de tipagem forte para evitar erros
- Trabalha com times grandes que exigem consistência
Use zerolog quando:
- Prefere API fluente e expressiva
- Precisa de um logger leve e minimalista
- Deseja integração fácil com context.Context
8. Boas práticas e armadilhas comuns
Rate limiting em loops de alta frequência
// zap: sampling automático
logger, _ := zap.Config{
Sampling: &zap.SamplingConfig{
Initial: 100, // loga as primeiras 100 entradas
Thereafter: 100, // depois loga 1 a cada 100
},
}.Build()
// zerolog: implementação manual com sampler
sampler := &zerolog.BasicSampler{N: 10}
sampled := log.Sample(sampler)
Segurança concorrente
Ambas as bibliotecas são thread-safe por padrão. Não é necessário sincronização adicional.
// Seguro para uso em múltiplas goroutines
go func() {
log.Info().Msg("goroutine 1")
}()
go func() {
log.Info().Msg("goroutine 2")
}()
Evitando fmt.Sprintf em logs estruturados
// ERRADO - perde a estrutura JSON
log.Info(fmt.Sprintf("usuário %d acessou recurso %s", userID, resource))
// CERTO - preserva campos estruturados
log.Info().
Int("userID", userID).
Str("resource", resource).
Msg("acesso a recurso")
Logging em testes com bytes.Buffer
func TestLogging(t *testing.T) {
var buf bytes.Buffer
logger := zerolog.New(&buf)
logger.Info().Str("test", "value").Msg("test message")
var result map[string]interface{}
json.Unmarshal(buf.Bytes(), &result)
assert.Equal(t, "test message", result["message"])
assert.Equal(t, "value", result["test"])
}
Referências
- Documentação oficial do zap — Documentação completa da biblioteca zap com exemplos de configuração e uso
- Documentação oficial do zerolog — Referência completa da API do zerolog
- Zap: Blazing Fast, Structured Logging in Go (Uber Engineering) — Artigo original da Uber apresentando o zap e suas motivações de performance
- Zerolog: Zero Allocation JSON Logger — Repositório oficial com exemplos, benchmarks e guia de migração
- Logging in Go with Zap: A Practical Guide — Tutorial prático da Linode sobre logging estruturado com zap em aplicações Go
- Structured Logging with Zerolog — Artigo detalhado de Alex Edwards sobre uso avançado do zerolog
- Lumberjack: Log Rolling Package for Go — Biblioteca para rotação de arquivos de log, frequentemente usada com zap e zerolog