Logs são a primeira e muitas vezes única fonte de informação quando algo dá errado em produção. Um sistema com logging bem estruturado permite diagnosticar problemas em segundos. Um sistema com fmt.Println espalhados pelo código torna o diagnóstico uma caçada demorada e frustrante.
A distinção fundamental é entre logging textual e logging estruturado. Logging textual produz strings legíveis por humanos mas difíceis de processar por máquinas. Logging estruturado produz documentos — tipicamente JSON — onde cada campo é um par chave-valor que sistemas como Elasticsearch, Datadog, Loki e CloudWatch podem indexar, filtrar e agregar.
O pacote slog: logging estruturado na biblioteca padrão
Go 1.21 introduziu log/slog — o primeiro logger estruturado na biblioteca padrão. Antes dele, cada projeto escolhia entre dezenas de bibliotecas externas sem padrão comum. O slog resolve isso com uma API elegante e extensível.
Conceitos fundamentais
package main
import (
"context"
"log/slog"
"os"
)
func main() {
// Logger padrão — TextHandler para terminal, JSONHandler para produção
// TextHandler: saída legível por humanos
loggerTexto := slog.New(slog.NewTextHandler(os.Stdout, nil))
// JSONHandler: saída estruturada para sistemas de log
loggerJSON := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// Definindo o logger padrão global
slog.SetDefault(loggerJSON)
// Níveis de log: Debug, Info, Warn, Error
slog.Debug("debug desabilitado por padrão")
slog.Info("aplicação iniciada")
slog.Warn("configuração ausente, usando padrão")
slog.Error("falha ao conectar", "erro", "connection refused")
// Atributos chave-valor
slog.Info("requisição recebida",
"método", "GET",
"caminho", "/usuarios",
"ip", "192.168.1.1",
"user_agent", "Mozilla/5.0",
)
// Com context — permite propagação de valores
ctx := context.Background()
slog.InfoContext(ctx, "operação concluída", "duração_ms", 142)
// Usando logger não-global
loggerTexto.Info("mensagem com logger específico")
loggerJSON.Info("mensagem em JSON")
}
Saída do TextHandler:
time=2024-03-15T10:30:00.000Z level=INFO msg="aplicação iniciada"
time=2024-03-15T10:30:00.001Z level=INFO msg="requisição recebida" método=GET caminho=/usuarios
Saída do JSONHandler:
{"time":"2024-03-15T10:30:00.000Z","level":"INFO","msg":"aplicação iniciada"}
{"time":"2024-03-15T10:30:00.001Z","level":"INFO","msg":"requisição recebida","método":"GET","caminho":"/usuarios","ip":"192.168.1.1"}
Configurando o slog
package main
import (
"log/slog"
"os"
)
func configurarLogger(ambiente string) *slog.Logger {
var nivel slog.Level
var handler slog.Handler
switch ambiente {
case "producao":
nivel = slog.LevelInfo
handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: nivel,
AddSource: true, // inclui arquivo e linha no log
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Renomeia campos para compatibilidade com sistemas externos
if a.Key == slog.TimeKey {
a.Key = "timestamp"
}
if a.Key == slog.LevelKey {
a.Key = "severity"
}
return a
},
})
case "desenvolvimento":
nivel = slog.LevelDebug
handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: nivel,
AddSource: true,
})
default:
nivel = slog.LevelWarn
handler = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
Level: nivel,
})
}
return slog.New(handler)
}
func main() {
logger := configurarLogger(os.Getenv("APP_ENV"))
slog.SetDefault(logger)
slog.Info("logger configurado", "ambiente", os.Getenv("APP_ENV"))
}
Logger com atributos fixos: With
With cria um novo logger filho que inclui atributos em todos os logs subsequentes — ideal para adicionar contexto de requisição, serviço ou componente:
package main
import (
"log/slog"
"os"
"time"
)
func processarRequisicao(logger *slog.Logger, reqID, metodo, caminho string) {
// Logger filho com contexto da requisição
log := logger.With(
"request_id", reqID,
"método", metodo,
"caminho", caminho,
)
inicio := time.Now()
log.Info("requisição iniciada")
// Simula processamento
time.Sleep(50 * time.Millisecond)
log.Info("buscando dados", "tabela", "usuarios")
log.Info("requisição concluída",
"status", 200,
"duração_ms", time.Since(inicio).Milliseconds(),
)
}
func main() {
// Logger base com identificação do serviço
base := slog.New(slog.NewJSONHandler(os.Stdout, nil)).With(
"serviço", "api",
"versao", "1.2.0",
"ambiente", "producao",
)
processarRequisicao(base, "req-abc-123", "GET", "/usuarios/42")
processarRequisicao(base, "req-def-456", "POST", "/pedidos")
}
Grupos: organizando atributos hierarquicamente
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// WithGroup cria namespace para atributos relacionados
logger.Info("servidor iniciado",
slog.Group("config",
slog.String("host", "0.0.0.0"),
slog.Int("port", 8080),
slog.Duration("timeout", 30*time.Second),
),
slog.Group("banco",
slog.String("host", "localhost"),
slog.Int("port", 5432),
slog.Int("pool_size", 25),
),
)
Saída JSON:
{
"level": "INFO",
"msg": "servidor iniciado",
"config": {"host": "0.0.0.0", "port": 8080, "timeout": 30000000000},
"banco": {"host": "localhost", "port": 5432, "pool_size": 25}
}
Handler customizado: adicionando campos automáticos
package main
import (
"context"
"log/slog"
"os"
)
// Chave de contexto para request ID
type chaveRequestID struct{}
// Handler que injeta automaticamente o request ID do context
type handlerComContext struct {
slog.Handler
}
func (h *handlerComContext) Handle(ctx context.Context, r slog.Record) error {
if reqID, ok := ctx.Value(chaveRequestID{}).(string); ok {
r.AddAttrs(slog.String("request_id", reqID))
}
return h.Handler.Handle(ctx, r)
}
func novoLoggerComContext() *slog.Logger {
handler := &handlerComContext{
Handler: slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}),
}
return slog.New(handler)
}
func main() {
logger := novoLoggerComContext()
// Sem request ID no context
ctx := context.Background()
logger.InfoContext(ctx, "sem contexto")
// Com request ID no context
ctxComID := context.WithValue(ctx, chaveRequestID{}, "req-xyz-789")
logger.InfoContext(ctxComID, "com request ID")
logger.WarnContext(ctxComID, "aviso com request ID", "codigo", 429)
}
Zerolog: performance extrema
O Zerolog é a biblioteca de logging mais performática do ecossistema Go. Sua API de encadeamento de chamadas permite que o compilador otimize agressivamente — em benchmarks, logs desabilitados custam literalmente zero alocações.
go get github.com/rs/zerolog
go get github.com/rs/zerolog/log
package main
import (
"os"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// Configuração global
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
zerolog.SetGlobalLevel(zerolog.InfoLevel)
// Logger JSON para produção
logger := zerolog.New(os.Stdout).
With().
Timestamp().
Str("serviço", "api").
Str("versao", "2.0.0").
Logger()
// Substituir o logger global
log.Logger = logger
// Logging básico
log.Info().Msg("aplicação iniciada")
log.Warn().
Str("componente", "cache").
Int("tamanho_atual", 950).
Int("limite", 1000).
Msg("cache próximo do limite")
log.Error().
Err(fmt.Errorf("conexão recusada")).
Str("host", "banco.interno").
Int("tentativas", 3).
Msg("falha ao conectar ao banco")
// Debug — zero alocações se nível for Info ou superior
log.Debug().
Str("query", "SELECT * FROM usuarios").
Dur("duração", 45*time.Millisecond).
Msg("query executada")
}
Zerolog: API completa de tipos
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
// Tipos disponíveis na API de encadeamento
logger.Info().
Str("texto", "valor").
Strs("lista", []string{"a", "b", "c"}).
Int("inteiro", 42).
Int64("int64", 9223372036854775807).
Float64("float", 3.14159).
Bool("booleano", true).
Dur("duracao", 150*time.Millisecond).
Time("momento", time.Now()).
Err(err). // campo "error"
Interface("qualquer", meuStruct). // qualquer tipo via JSON
Dict("aninhado", zerolog.Dict().
Str("chave", "valor").
Int("numero", 1),
).
Msg("log completo")
Zerolog com context
package main
import (
"context"
"net/http"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func middlewareLoggingZerolog(prox http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = fmt.Sprintf("%d", time.Now().UnixNano())
}
// Cria logger filho com campos da requisição
logger := log.With().
Str("request_id", reqID).
Str("método", r.Method).
Str("caminho", r.URL.Path).
Str("ip", r.RemoteAddr).
Logger()
// Injeta no context para uso nos handlers
ctx := logger.WithContext(r.Context())
inicio := time.Now()
rw := novoResponseWriter(w)
prox.ServeHTTP(rw, r.WithContext(ctx))
// Log final com status e duração
logger.Info().
Int("status", rw.status).
Dur("duração", time.Since(inicio)).
Int("bytes", rw.bytesEscritos).
Msg("requisição concluída")
})
}
func handler(w http.ResponseWriter, r *http.Request) {
// Recupera o logger do context
logger := zerolog.Ctx(r.Context())
logger.Debug().Msg("processando handler")
w.Write([]byte("ok"))
}
Zerolog: saída para desenvolvimento
// Console writer — saída colorida e legível para desenvolvimento
consoleWriter := zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "15:04:05",
FormatLevel: func(i interface{}) string {
switch i.(string) {
case "debug":
return "\033[36mDEBUG\033[0m"
case "info":
return "\033[32mINFO \033[0m"
case "warn":
return "\033[33mWARN \033[0m"
case "error":
return "\033[31mERROR\033[0m"
}
return i.(string)
},
}
logger := zerolog.New(consoleWriter).With().Timestamp().Logger()
logger.Info().Str("componente", "servidor").Msg("iniciado")
logger.Warn().Int("conexoes", 98).Msg("conexões altas")
logger.Error().Err(errors.New("timeout")).Msg("operação falhou")
Múltiplos destinos
package main
import (
"io"
"os"
"github.com/rs/zerolog"
)
func criarLoggerMultiDestino(arquivoLog string) (zerolog.Logger, error) {
arquivo, err := os.OpenFile(arquivoLog,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return zerolog.Logger{}, err
}
// Escreve tanto no arquivo (JSON) quanto no terminal (texto)
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
multi := zerolog.MultiLevelWriter(consoleWriter, arquivo)
return zerolog.New(multi).With().Timestamp().Logger(), nil
}
Rotação de logs com lumberjack
import "gopkg.in/natefinish/lumberjack.v2"
func criarLoggerComRotacao() zerolog.Logger {
rotador := &lumberjack.Logger{
Filename: "/var/log/app/api.log",
MaxSize: 100, // MB por arquivo
MaxBackups: 5, // máximo de arquivos de backup
MaxAge: 30, // dias para manter arquivos antigos
Compress: true, // comprime arquivos antigos
}
return zerolog.New(rotador).With().Timestamp().Logger()
}
Benchmark: slog vs zerolog vs log padrão
package benchmark_test
import (
"io"
"log"
"log/slog"
"testing"
"github.com/rs/zerolog"
)
var logger = zerolog.New(io.Discard)
var slogger = slog.New(slog.NewJSONHandler(io.Discard, nil))
func BenchmarkLogPadrao(b *testing.B) {
log.SetOutput(io.Discard)
for i := 0; i < b.N; i++ {
log.Printf("mensagem %d", i)
}
}
func BenchmarkSlog(b *testing.B) {
for i := 0; i < b.N; i++ {
slogger.Info("mensagem", "i", i, "componente", "benchmark")
}
}
func BenchmarkZerolog(b *testing.B) {
for i := 0; i < b.N; i++ {
logger.Info().Int("i", i).Str("componente", "benchmark").Msg("mensagem")
}
}
func BenchmarkZerologDesabilitado(b *testing.B) {
l := zerolog.New(io.Discard).Level(zerolog.Disabled)
for i := 0; i < b.N; i++ {
l.Debug().Int("i", i).Msg("nunca escrito")
// ~0 ns/op — zero alocações
}
}
Resultados típicos:
BenchmarkLogPadrao-8 3.000.000 400 ns/op 64 B/op 2 allocs/op
BenchmarkSlog-8 5.000.000 220 ns/op 0 B/op 0 allocs/op
BenchmarkZerolog-8 15.000.000 70 ns/op 0 B/op 0 allocs/op
BenchmarkZerologDesabilitado-8 1.000.000.000 0.5 ns/op 0 B/op 0 allocs/op
Quando usar cada um
| Situação | Recomendação |
|---|---|
| Projeto novo sem dependências extras | log/slog |
| API com alta carga e latência crítica | Zerolog |
| Integração com sistemas existentes | Zerolog (interface zerolog.Logger) |
| CLI ou ferramenta simples | log/slog com TextHandler |
| Microsserviço com OpenTelemetry | log/slog (integração nativa) |
Projeto legado com log padrão |
Migrar para log/slog gradualmente |
Resumo do que foi coberto
Este artigo apresentou logging estruturado em Go de forma completa: log/slog com TextHandler e JSONHandler, configuração de níveis e opções, With para contexto persistente, grupos hierárquicos, handler customizado para injeção de campos do context, Zerolog com sua API de encadeamento e performance extrema, saída para desenvolvimento com ConsoleWriter, múltiplos destinos com MultiLevelWriter, rotação de logs com lumberjack e benchmark comparativo entre as opções. O próximo artigo explora configuração com variáveis de ambiente e Viper.
Referências e leituras complementares
-
Documentação log/slog — Referência oficial do pacote. https://pkg.go.dev/log/slog
-
Go Blog: Structured Logging with slog — Introdução oficial ao slog. https://go.dev/blog/slog
-
rs/zerolog — Repositório e documentação do Zerolog. https://github.com/rs/zerolog
-
lumberjack — Rotação de logs para Go. https://github.com/natefinish/lumberjack
-
OpenTelemetry Go — Integração de logs com traces e métricas. https://opentelemetry.io/docs/instrumentation/go/
-
Elastic Common Schema — Convenção de campos para logs estruturados. https://www.elastic.co/guide/en/ecs/current/index.html