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: Debug para detalhes de desenvolvimento, Info para eventos normais, Warn para situações anormais e Error para falhas.
  • Mantenha as mensagens claras e objetivas, evitando informações sensíveis como senhas.
  • Inclua contexto relevante: use slog.Group para agrupar atributos e With para 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.Error para 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

Exercícios

  1. Crie um programa que use slog para 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")
    }
  2. 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),
            ),
        )
    }
  3. 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())
    }
  4. Crie um logger com nível mínimo configurável usando slog.LevelVar e 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")
    }
  5. Escreva uma função que receba um slog.Logger e 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"))
    }