---
title: "slog em Go: Logging Estruturado para Produção"
url: "https://golang.com.br/blog/slog-go-logging-estruturado/"
markdown_url: "https://golang.com.br/blog/slog-go-logging-estruturado.MD"
description: "Aprenda slog em Go: handlers JSON, níveis, atributos, context.Context, redaction, testes e padrões para logs estruturados em APIs e workers em produção."
date: "2026-05-17"
author: "Golang Brasil"
---

# slog em Go: Logging Estruturado para Produção

Aprenda slog em Go: handlers JSON, níveis, atributos, context.Context, redaction, testes e padrões para logs estruturados em APIs e workers em produção.


O pacote `log/slog` virou a escolha padrão para **logging estruturado em Go**. Ele entrou na biblioteca padrão no Go 1.21 e resolveu um problema antigo: como produzir logs consistentes, pesquisáveis e baratos de manter sem depender de wrappers proprietários ou formatos improvisados com `fmt.Printf`.

Para aplicações pequenas, `log.Println` ainda funciona. Em produção, porém, você precisa responder perguntas como: qual `request_id` falhou? Qual usuário recebeu `timeout`? Quantas chamadas para o provedor externo estão voltando 429? Qual worker processou esse job? Logging estruturado torna essas respostas possíveis porque cada linha de log carrega campos nomeados, não apenas texto solto.

Este guia mostra como usar `slog` em APIs, CLIs e workers Go: handlers JSON, níveis, grupos, `context.Context`, redaction de dados sensíveis, testes e erros comuns. Se você ainda está montando a base da aplicação, veja também o guia de [API REST em Go](/aprenda/api-rest-go/) e o tutorial de [observabilidade em Go](/tutoriais/go-observability/).

## Por que slog em vez de log.Printf?

O problema de `log.Printf("user %s failed: %v", userID, err)` não é performance. O problema é **consulta**. Em um incidente, alguém vai procurar por `user_id`, `job_id`, `trace_id`, status HTTP, endpoint, latência e ambiente. Se tudo isso está escondido dentro de uma string, cada busca depende de regex frágil.

Com `slog`, o mesmo evento vira um registro estruturado:

```go
logger.Error("falha ao buscar usuário",
    slog.String("user_id", userID),
    slog.String("request_id", requestID),
    slog.Int("status", 502),
    slog.Duration("duration", elapsed),
    slog.Any("err", err),
)
```

Em desenvolvimento, você pode renderizar isso como texto legível. Em produção, use JSON para que ferramentas como Loki, Datadog, Elasticsearch, Cloud Logging ou OpenTelemetry Collector indexem os campos corretamente.

## Configuração básica com JSONHandler

A forma mais direta de começar é criar um logger no `main` e injetá-lo nas dependências da aplicação:

```go
package main

import (
    "log/slog"
    "os"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    }))

    slog.SetDefault(logger)

    logger.Info("aplicação iniciada",
        slog.String("service", "checkout-api"),
        slog.String("env", "production"),
    )
}
```

O `JSONHandler` escreve uma linha JSON por evento. Isso é proposital: logs de produção devem ir para `stdout`/`stderr` e o ambiente de execução — Docker, Kubernetes, systemd ou plataforma serverless — coleta o fluxo.

Para desenvolvimento local, `TextHandler` costuma ser mais confortável:

```go
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))
```

Não crie um logger novo em cada função. Crie uma instância no bootstrap, adicione campos comuns e passe por construtores. Isso evita inconsistência e facilita testes.

## Níveis: debug, info, warn e error

`slog` trabalha com níveis. A regra prática:

- `Debug`: detalhe útil durante investigação, desligado em produção por padrão.
- `Info`: evento normal de negócio ou ciclo de vida.
- `Warn`: algo inesperado, mas recuperável.
- `Error`: falha que impediu a operação atual.

Exemplo em handler HTTP:

```go
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    start := time.Now()

    order, err := h.service.Create(r.Context())
    if err != nil {
        h.logger.Error("erro ao criar pedido",
            slog.String("path", r.URL.Path),
            slog.String("method", r.Method),
            slog.Duration("duration", time.Since(start)),
            slog.Any("err", err),
        )
        http.Error(w, "erro interno", http.StatusInternalServerError)
        return
    }

    h.logger.Info("pedido criado",
        slog.String("order_id", order.ID),
        slog.Duration("duration", time.Since(start)),
    )

    w.WriteHeader(http.StatusCreated)
}
```

Evite registrar tudo como `Error`. Se todo evento é erro, nenhum alerta é confiável. Também evite logs em laços muito quentes sem amostragem: logging em excesso encarece a infraestrutura e torna incidente mais difícil de investigar.

## Campos comuns com With

Use `With` para criar loggers derivados com campos fixos. Isso é perfeito para serviço, versão, ambiente, componente e IDs de correlação:

```go
base := slog.New(slog.NewJSONHandler(os.Stdout, nil)).With(
    slog.String("service", "billing-api"),
    slog.String("version", "2026.05.17"),
    slog.String("env", "production"),
)

repoLogger := base.With(slog.String("component", "postgres"))
workerLogger := base.With(slog.String("component", "invoice-worker"))
```

Dentro de uma operação específica, derive outro logger:

```go
logger := h.logger.With(
    slog.String("request_id", requestID),
    slog.String("customer_id", customerID),
)

logger.Info("processando cobrança")
```

Esse padrão evita repetir atributos em cada chamada e reduz o risco de esquecer um campo importante.

## Grupos para organizar payloads

Quando a aplicação cresce, nomes de campos podem colidir. `slog.Group` ajuda a agrupar dados relacionados:

```go
logger.Info("requisição recebida",
    slog.Group("http",
        slog.String("method", r.Method),
        slog.String("path", r.URL.Path),
        slog.Int("status", status),
    ),
    slog.Group("user",
        slog.String("id", userID),
        slog.String("plan", plan),
    ),
)
```

No JSON, isso vira objetos aninhados. Use grupos para `http`, `db`, `queue`, `user`, `job` e `payment`. Só não exagere: se sua ferramenta de logs tem dificuldade com campos aninhados, prefira nomes planos como `http_method` e `job_id`.

## Contexto, request ID e trace ID

`slog` tem métodos com contexto, como `InfoContext` e `ErrorContext`. Eles não extraem automaticamente valores do `context.Context`, mas permitem que handlers customizados ou integrações de tracing usem o contexto.

Um padrão simples é extrair `request_id` no middleware e criar um logger por requisição:

```go
type contextKey string

const requestIDKey contextKey = "request_id"

func LoggerMiddleware(logger *slog.Logger, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = uuid.NewString()
        }

        ctx := context.WithValue(r.Context(), requestIDKey, requestID)
        reqLogger := logger.With(slog.String("request_id", requestID))

        reqLogger.InfoContext(ctx, "requisição iniciada",
            slog.String("method", r.Method),
            slog.String("path", r.URL.Path),
        )

        next.ServeHTTP(w, r.WithContext(ctx))
    })
}
```

Se você usa OpenTelemetry, mantenha `trace_id` e `span_id` nos logs. Isso conecta logs, métricas e traces. Para a visão completa, o tutorial de [Go observability](/tutoriais/go-observability/) aprofunda Prometheus, tracing e alertas.

## Redaction: não vaze segredo no log

Log é uma superfície de segurança. Nunca registre senha, token, cookie, chave de API, documento completo, cartão ou payload sensível. O incidente clássico não é o atacante quebrar a criptografia; é alguém colocar `Authorization` inteiro no log de erro.

`slog` permite customizar saída com `ReplaceAttr`:

```go
func redactSecrets(groups []string, a slog.Attr) slog.Attr {
    switch a.Key {
    case "password", "token", "authorization", "cookie", "api_key":
        return slog.String(a.Key, "[REDACTED]")
    default:
        return a
    }
}

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    ReplaceAttr: redactSecrets,
}))
```

Também prefira registrar identificadores estáveis em vez de dados pessoais. `user_id` é melhor que email; `customer_id` é melhor que CPF; `card_last4` é aceitável quando necessário, mas cartão completo nunca.

## Testando logs em Go

Logs são comportamento observável. Em fluxos críticos, teste se a aplicação registra campos importantes. Uma abordagem simples é escrever o handler em um `bytes.Buffer`:

```go
func TestLoggerCampos(t *testing.T) {
    var buf bytes.Buffer
    logger := slog.New(slog.NewJSONHandler(&buf, nil))

    logger.Info("pedido criado", slog.String("order_id", "ord_123"))

    var got map[string]any
    if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
        t.Fatal(err)
    }

    if got["order_id"] != "ord_123" {
        t.Fatalf("order_id = %v", got["order_id"])
    }
}
```

Para aprender a estruturar a suíte, combine isso com [testes de tabela em Go](/blog/testes-tabela-go-guia-table-driven-tests/) e o guia de [testing/debug](/cheatsheet/testing-debug/). Teste logs de erro, redaction e presença de `request_id` em endpoints importantes.

## Erros comuns ao adotar slog

O primeiro erro é misturar estilos: parte do código usa `log.Printf`, parte usa `slog`, parte usa uma biblioteca antiga. Defina um padrão por serviço e migre por fronteiras: HTTP primeiro, workers depois, repositórios por último.

O segundo erro é logar no lugar errado. Camadas baixas devem retornar erro com contexto suficiente; camadas de borda — handler HTTP, consumer de fila, comando CLI — decidem se o erro vira `Warn` ou `Error`. Logar o mesmo erro em três camadas gera ruído.

O terceiro erro é usar mensagens dinâmicas demais. Prefira mensagem estável e campos variáveis:

```go
// Ruim
logger.Info("usuário " + userID + " criou pedido " + orderID)

// Bom
logger.Info("pedido criado",
    slog.String("user_id", userID),
    slog.String("order_id", orderID),
)
```

Mensagens estáveis agrupam melhor em dashboards e alertas.

## Checklist para produção

Antes de mandar uma API Go para produção, valide:

1. `JSONHandler` em produção e `TextHandler` apenas localmente.
2. Campos fixos: `service`, `env`, `version` e `component`.
3. Campos por requisição: `request_id`, `method`, `path`, `status`, `duration`.
4. Campos por worker: `job_id`, `queue`, `attempt`, `duration`, `result`.
5. `trace_id`/`span_id` quando houver OpenTelemetry.
6. `ReplaceAttr` ou política equivalente para redaction.
7. Erros logados uma vez, na borda certa.
8. Teste de redaction e campos obrigatórios.
9. Amostragem ou limitação para logs de alto volume.
10. Alertas baseados em métricas, não apenas volume bruto de logs.

`slog` não substitui métricas nem traces. Ele deixa os logs honestos, estruturados e integráveis. Para backend brasileiro, isso é especialmente útil em times pequenos: menos tempo garimpando incidente, mais tempo entregando produto.

Se você compara stacks, o <a href="https://python.dev.br/blog/logging-python/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'python.dev.br' })">guia de logging em Python</a> mostra como o mesmo problema aparece no ecossistema Python; a vantagem de Go é ter uma solução estruturada moderna já na biblioteca padrão. O próximo passo natural é combinar `slog` com métricas Prometheus em [Go e Prometheus](/tutoriais/go-prometheus/) e tracing no seu pipeline de observabilidade.
