Golang

Logging estruturado com slog e Zerolog Já leu

10 min de leitura

Logging estruturado com slog e Zerolog
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 pel

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

Comentários

Mais em Golang

Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go
Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go

Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go O binário...

Estruturas de controle: if, for e switch
Estruturas de controle: if, for e switch

Todo programa &uacute;til precisa tomar decis&otilde;es e repetir opera&ccedi...

Artigo 45 — Domain-Driven Design aplicado a projetos Go
Artigo 45 — Domain-Driven Design aplicado a projetos Go

Artigo 45 — Domain-Driven Design aplicado a projetos Go DDD: o domínio como c...