Pular para o conteúdo

Trilha de Go · parte 31 de 37

Logging Estruturado com slog: Performance, Contexto e Produção

Como fazer logs estruturados com o pacote slog da biblioteca padrão, com níveis, campos de contexto e saída pronta para produção.

Publicada em · 5 min de leitura

Tamanho da fonte:
Largura da coluna:

Logging Estruturado com slog: Performance, Contexto e Produção

Historicamente, o ecossistema Go sofreu com a fragmentação de bibliotecas de log. O pacote nativo log original oferecia apenas saída de texto simples sem níveis de severidade ou suporte a campos estruturados (log.Printf), forçando a comunidade a adotar soluções de terceiros como Logrus, Zap (Uber) ou Zerolog.

A partir do Go 1.21, a equipe oficial do Go introduziu o log/slog na Standard Library. O slog une a ergonomia de logs estruturados em JSON, alto desempenho com alocação mínima de memória e uma separação arquitetural estrita entre a API de chamada (Logger) e os mecanismos de formatação e transporte (Handler).


1. Arquitetura Interna do slog

O slog é desenhado sobre o princípio de desacoplamento: o código de negócio interage apenas com a struct slog.Logger, enquanto a formatação física (JSON, texto colorido ou envio direto para Datadog/Loki) é delegada à interface slog.Handler:

Arquitetura e Pipeline do log/slog
figura 1 · Arquitetura e Pipeline do log/slog

As Camadas do Pipeline:

  1. Chamada de Aplicação: O código invoca métodos como logger.InfoContext(ctx, "mensagem", attrs...).
  2. Construção do slog.Record: Os dados são empacotados em uma struct leve alocada na pilha (stack) contendo timestamp, nível, mensagem e argumentos.
  3. Interceptação no slog.Handler: O Handler verifica se o nível está ativo (handler.Enabled(ctx, level)), aplica transformações de atributos e formata a saída.
  4. Destino I/O: O payload formatado é gravado em um stream io.Writer (geralmente os.Stdout, coletado por agentes como FluentBit, Promtail ou Datadog Agent).

2. Handlers Nativos: Texto vs JSON

O slog disponibiliza dois handlers prontos para uso:

package main

import (
	"log/slog"
	"os"
)

func InitLogger(env string) *slog.Logger {
	var handler slog.Handler

	if env == "production" {
		// Em produção: JSON puro para ingestão por ElasticSearch, Datadog ou Grafana Loki
		handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
			Level:     slog.LevelInfo,
			AddSource: true, // Adiciona arquivo e linha (ex: "source":"main.go:42")
		})
	} else {
		// Em desenvolvimento local: Texto legível para o terminal humano
		handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
			Level: slog.LevelDebug,
		})
	}

	logger := slog.New(handler)
	// Define como logger global padrão (substitui o antigo log.Println)
	slog.SetDefault(logger)

	return logger
}

3. Performance e Atributos Fortemente Tipados

O slog permite passar pares soltos de chave-valor ou atributos fortemente tipados através da struct slog.Attr:

// 1. Sintaxe Flexível (Chave-Valor soltos):
// Confortável, porém força boxing de valores primitivos em interfaces any
slog.Info("pedido_criado", "order_id", "123", "total", 99.50)

// 2. Sintaxe Fortemente Tipada (Zero Allocations):
// Evita alocações no Heap ao empacotar primitivos diretamente
slog.Info("pedido_criado",
    slog.String("order_id", "123"),
    slog.Float64("total", 99.50),
    slog.Int("items_count", 3),
)

4. Agrupamento de Atributos com slog.Group

Para evitar poluição na raiz do JSON e manter taxonomias limpas, agrupe atributos por domínio:

slog.Info("transacao_processada",
    slog.Group("user",
        slog.String("id", "usr_99"),
        slog.String("email", "dev@empresa.com"),
    ),
    slog.Group("payment",
        slog.Float64("amount", 250.00),
        slog.String("gateway", "stripe"),
    ),
)

JSON Resultante:

{
  "time": "2026-10-20T10:00:00Z",
  "level": "INFO",
  "msg": "transacao_processada",
  "user": {
    "id": "usr_99",
    "email": "dev@empresa.com"
  },
  "payment": {
    "amount": 250.0,
    "gateway": "stripe"
  }
}

5. Mascaramento Automático de Dados Sensíveis com slog.LogValuer

Em conformidade com a LGPD e GDPR, senhas, tokens e dados pessoais nunca devem vazar para arquivos de log. O slog resolve isso de forma elegante através da interface slog.LogValuer:

type LogValuer interface {
    LogValue() slog.Value
}

Implementação de Tipos Seguros

package domain

import "log/slog"

// Password protege contra vazamento acidental em logs
type Password string

func (p Password) LogValue() slog.Value {
	return slog.StringValue("****** [REDACTED]")
}

// CreditCard mascara os dígitos centrais
type CreditCard string

func (c CreditCard) LogValue() slog.Value {
	s := string(c)
	if len(s) >= 16 {
		return slog.StringValue(s[:4] + " **** **** " + s[12:])
	}
	return slog.StringValue("**** [MASKED]")
}

Ao registrar slog.Any("senha", user.Password) ou slog.Any("cartao", user.Card), o slog invoca automaticamente o método LogValue(), garantindo que o valor original em texto claro nunca chegue aos logs.


6. Mudança Dinâmica de Nível em Runtime com slog.LevelVar

Em produção, você quer que o nível padrão seja INFO para economizar I/O e custos de ingestão. Mas quando um incidente ocorre, você precisa ativar o DEBUG sem reiniciar os pods.

package main

import (
	"log/slog"
	"net/http"
	"os"
)

var logLevel = &slog.LevelVar{} // Default inicial: LevelInfo

func SetupDynamicLogging() {
	handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: logLevel,
	})
	slog.SetDefault(slog.New(handler))

	// Endpoint administrativo para alterar o nível em tempo real
	http.HandleFunc("POST /admin/log-level", func(w http.ResponseWriter, r *http.Request) {
		levelStr := r.URL.Query().Get("level")
		switch levelStr {
		case "debug":
			logLevel.Set(slog.LevelDebug)
		case "info":
			logLevel.Set(slog.LevelInfo)
		case "warn":
			logLevel.Set(slog.LevelWarn)
		case "error":
			logLevel.Set(slog.LevelError)
		default:
			http.Error(w, "nível inválido (use debug, info, warn, error)", http.StatusBadRequest)
			return
		}

		slog.Warn("Nível de log alterado dinamicamente", slog.String("novo_nivel", levelStr))
		w.Write([]byte("Nível alterado com sucesso para " + levelStr))
	})
}

7. Context-Aware Handler: Injetando Trace-ID Automaticamente

A maior fraqueza de logs tradicionais é a desconexão com o Tracing Distribuído e OpenTelemetry e com o Correlation ID criado em Middlewares em Go.

Para que qualquer chamada slog.InfoContext(ctx, "msg") automaticamente anexe o trace_id e o request_id sem que o desenvolvedor precise digitá-los manualmente em todos os logs, criamos um Handler Decorator:

Injeção de Trace-ID via ContextHandler no slog
figura 2 · Injeção de Trace-ID via ContextHandler no slog

Implementação do ContextHandler

package logging

import (
	"context"
	"log/slog"
)

type contextKey string

const (
	TraceIDKey   contextKey = "trace_id"
	RequestIDKey contextKey = "request_id"
)

type ContextHandler struct {
	slog.Handler
}

func NewContextHandler(h slog.Handler) *ContextHandler {
	return &ContextHandler{Handler: h}
}

// Handle intercepta o registro antes de formatar e anexa dados do context
func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error {
	if traceID, ok := ctx.Value(TraceIDKey).(string); ok && traceID != "" {
		r.AddAttrs(slog.String("trace_id", traceID))
	}

	if reqID, ok := ctx.Value(RequestIDKey).(string); ok && reqID != "" {
		r.AddAttrs(slog.String("request_id", reqID))
	}

	return h.Handler.Handle(ctx, r)
}

8. Boas Práticas e Checklist de Produção

  • Sempre use *Context: Prefira slog.InfoContext(ctx, ...), slog.ErrorContext(ctx, ...) em vez de slog.Info(...) para que contextos e cancelamentos sejam propagados.
  • Nunca Logue o Mesmo Erro Múltiplas Vezes: Se uma camada inferior retornar um erro, retorne-o com fmt.Errorf("...: %w", err) e registre o log apenas uma vez na borda da aplicação (no Handler HTTP ou no Worker de fila).
  • Proteja Dados Pessoais: Use a interface slog.LogValuer para qualquer struct que transporte senhas, tokens ou dados pessoais sensíveis.
  • Evite Interpolação de Strings: Nunca faça slog.Info(fmt.Sprintf("User %s logou", u.Name)). Use sempre a mensagem constante e os atributos: slog.Info("user_logged_in", slog.String("name", u.Name)).
  • Use JSON em Produção: Facilita filtros por campos no Datadog (@user.id:"usr_99"), Loki e ElasticSearch.

9. Resumo Conceitual

  • O log/slog é o padrão oficial do Go para logging estruturado a partir do Go 1.21.
  • Desacopla a interface slog.Logger do formato e destino slog.Handler.
  • Atributos fortemente tipados (slog.String, slog.Int) evitam alocações extras de memória no Heap.
  • Implemente slog.LogValuer para mascaramento automático de dados sensíveis e conformidade com LGPD.
  • Utilize slog.LevelVar para alternar o nível de log entre INFO e DEBUG em runtime sem restart.
  • Crie um ContextHandler customizado para correlacionar automaticamente todos os logs com o trace_id de Tracing Distribuído e OpenTelemetry e Middlewares em Go.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?