Logging estruturado com structlog

Publicado em: 24/07/2026
Tempo de leitura: 6 minutos
Desenvolvedor monitorando logs estruturados em Python

Logs são uma das principais fontes de diagnóstico em aplicações Python, mas mensagens de texto soltas rapidamente se tornam difíceis de pesquisar, agrupar e interpretar. Quando um sistema cresce, é comum precisar responder perguntas como: qual usuário recebeu este erro, em qual serviço, com qual identificador de requisição, durante qual operação e em quanto tempo? O logging estruturado com structlog resolve esse problema ao transformar eventos em dados consistentes, normalmente representados como JSON.

Neste guia, você aprenderá a configurar structlog, adicionar contexto, integrar com a biblioteca padrão logging, proteger dados sensíveis, testar eventos e preparar uma configuração adequada para desenvolvimento e produção. Para aproveitar melhor os exemplos, vale revisar qualidade de código com Ruff, recursos avançados do Python, projetos Python com RAG e configurações seguras com Pydantic Settings.

O que é logging estruturado?

No logging tradicional, o programa grava uma frase pronta, como “Falha ao processar pedido 42 do usuário 7”. Para uma pessoa, a mensagem é compreensível, mas uma ferramenta precisa interpretar o texto para descobrir o número do pedido, o usuário e o tipo de falha. No modelo estruturado, o mesmo evento pode conter campos separados: event, order_id, user_id, level e timestamp.

Essa estrutura facilita filtros, painéis, alertas e correlação entre serviços. Plataformas de observabilidade conseguem indexar campos, calcular frequências e localizar todos os eventos relacionados ao mesmo identificador. O structlog foi criado para tornar esse modelo natural no Python, sem obrigar a aplicação a construir dicionários manualmente em cada chamada.

Instalação e primeiro exemplo

Crie um ambiente virtual e instale a biblioteca:

python -m venv .venv
source .venv/bin/activate
pip install structlog

No Windows, use .venv\Scripts\activate. Em seguida, crie um arquivo app.py:

import structlog

log = structlog.get_logger()
log.info("user_authenticated", user_id=42, method="password")

Sem configuração adicional, o structlog já mantém o nome do evento e os campos associados. Porém, uma aplicação real deve definir processadores, formato de saída, níveis e integração com o logging padrão.

Configurando processadores

Processadores são funções executadas em sequência. Eles recebem o logger, o método e o dicionário do evento. Cada processador pode acrescentar, remover ou transformar informações. Uma configuração prática é:

import logging
import structlog

logging.basicConfig(level=logging.INFO, format="%(message)s")

structlog.configure(
    processors=[
        structlog.contextvars.merge_contextvars,
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso", utc=True),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.processors.JSONRenderer(),
    ],
    wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
    logger_factory=structlog.PrintLoggerFactory(),
    cache_logger_on_first_use=True,
)

log = structlog.get_logger("api")
log.info("service_started", version="1.0.0")

O resultado inclui nível, horário em UTC, nome do evento e campos adicionais. O JSONRenderer é apropriado para produção, pois gera uma linha JSON por evento. A documentação oficial do structlog detalha processadores e integrações. A documentação do logging do Python explica níveis, handlers e filtros.

Contexto com bind

Uma vantagem central do structlog é associar contexto a um logger. Em vez de repetir os mesmos campos em todas as chamadas, use bind:

request_log = log.bind(
    request_id="req-8f2a",
    user_id=42,
    endpoint="/orders",
)

request_log.info("request_received", method="POST")
request_log.info("order_created", order_id=991)
request_log.info("request_finished", duration_ms=83)

Todos os eventos carregam os mesmos campos de correlação. Isso permite reconstruir a sequência completa de uma requisição. Quando o contexto não deve mais existir, crie outro logger ou use unbind.

Context variables em aplicações assíncronas

Em servidores web assíncronos, um logger global com bind pode misturar dados de requisições concorrentes. O módulo contextvars evita esse risco. O structlog oferece funções próprias:

from structlog.contextvars import (
    bind_contextvars,
    clear_contextvars,
)

async def handle_request(request):
    clear_contextvars()
    bind_contextvars(
        request_id=request.headers.get("X-Request-ID"),
        path=request.url.path,
    )
    log.info("request_started")
    try:
        return await process(request)
    finally:
        log.info("request_finished")
        clear_contextvars()

Limpar o contexto no início e no final impede vazamento de informações entre tarefas. Frameworks como FastAPI podem executar essa lógica em middleware.

Integração com logging padrão

Muitas bibliotecas externas usam logging.getLogger(). Para unificar a saída, utilize ProcessorFormatter. Assim, eventos do structlog e mensagens de dependências passam pelo mesmo handler e podem ser renderizados em JSON.

import logging
import structlog

shared_processors = [
    structlog.contextvars.merge_contextvars,
    structlog.processors.add_log_level,
    structlog.processors.TimeStamper(fmt="iso", utc=True),
]

formatter = structlog.stdlib.ProcessorFormatter(
    processor=structlog.processors.JSONRenderer(),
    foreign_pre_chain=shared_processors,
)

handler = logging.StreamHandler()
handler.setFormatter(formatter)
root = logging.getLogger()
root.handlers.clear()
root.addHandler(handler)
root.setLevel(logging.INFO)

structlog.configure(
    processors=[
        *shared_processors,
        structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
    ],
    logger_factory=structlog.stdlib.LoggerFactory(),
    wrapper_class=structlog.stdlib.BoundLogger,
    cache_logger_on_first_use=True,
)

Esse arranjo é especialmente útil em projetos com servidores ASGI, clientes HTTP, ORMs e filas, pois todos os componentes seguem uma saída consistente.

Saída legível no desenvolvimento

JSON é ótimo para máquinas, mas cansativo no terminal local. Você pode escolher o renderizador conforme o ambiente:

import os

is_dev = os.getenv("APP_ENV", "development") == "development"
renderer = (
    structlog.dev.ConsoleRenderer(colors=True)
    if is_dev
    else structlog.processors.JSONRenderer()
)

Mantenha a lista de campos igual nos dois ambientes. A diferença deve estar apenas na apresentação. Dessa forma, os testes e painéis continuam confiáveis.

Exceções e stack traces

Ao capturar uma exceção, use log.exception dentro do bloco except:

try:
    charge_customer(customer_id=42)
except PaymentError:
    log.exception(
        "payment_failed",
        customer_id=42,
        provider="example-pay",
    )
    raise

O processador format_exc_info converte os detalhes da exceção. Evite registrar o mesmo erro em várias camadas. Normalmente, a camada que decide como tratar ou propagar a falha é o melhor lugar para gravar o evento.

Protegendo dados sensíveis

Logs podem se tornar uma fonte de vazamento. Nunca registre senhas, tokens, chaves de API, cookies de sessão, números completos de cartão ou documentos pessoais. Crie um processador de redaction:

SENSITIVE_KEYS = {"password", "token", "api_key", "authorization"}

def redact_secrets(logger, method_name, event_dict):
    for key in SENSITIVE_KEYS:
        if key in event_dict:
            event_dict[key] = "[REDACTED]"
    return event_dict

Coloque esse processador antes do renderizador. Além disso, defina uma lista explícita de campos permitidos em eventos críticos. Redaction é uma camada de defesa, não uma autorização para enviar objetos completos ao logger.

Níveis e nomes de eventos

Use debug para detalhes de diagnóstico, info para eventos normais, warning para situações recuperáveis, error para falhas e critical para indisponibilidade grave. Evite usar error em toda exceção tratada, pois isso cria alertas inúteis.

Os nomes dos eventos devem ser estáveis e fáceis de buscar. Prefira order_created em vez de frases variáveis. Campos guardam os detalhes; o evento identifica o tipo da ocorrência.

Testando logs estruturados

Logs também fazem parte do comportamento observável. O structlog possui ferramentas de captura:

import structlog

def create_user(user_id):
    structlog.get_logger().info("user_created", user_id=user_id)

def test_create_user_logs_event():
    with structlog.testing.capture_logs() as logs:
        create_user(42)

    assert logs[0]["event"] == "user_created"
    assert logs[0]["user_id"] == 42

Teste campos importantes, mas não prenda o teste à ordem JSON ou a horários dinâmicos. Em projetos maiores, valide também que segredos nunca aparecem.

Desempenho e volume

Logging tem custo de CPU, serialização, armazenamento e transferência. Não grave grandes objetos, respostas inteiras ou eventos dentro de loops intensivos sem necessidade. Use amostragem em eventos muito frequentes e métricas para contagens agregadas. Logs são melhores para contexto detalhado; métricas são melhores para tendências.

Defina retenção conforme a utilidade e a legislação aplicável. Um ambiente de desenvolvimento pode manter poucos dias, enquanto auditorias podem exigir períodos maiores. Reduza o nível de bibliotecas barulhentas com configurações específicas.

Estrutura recomendada de campos

Uma convenção simples ajuda toda a equipe. Campos comuns incluem service, environment, version, event, level, timestamp, request_id, trace_id, user_id e duration_ms. Nem todos precisam aparecer em todos os eventos.

Documente os eventos principais como se fossem uma pequena API. Mudanças em nomes e tipos podem quebrar alertas e painéis. Para sistemas distribuídos, adote o mesmo identificador de correlação em todos os serviços.

Checklist para produção

  • Use timestamps UTC e formato ISO 8601.
  • Emita uma linha JSON por evento.
  • Adicione contexto de requisição com contextvars.
  • Integre mensagens da biblioteca logging.
  • Remova segredos e dados pessoais.
  • Defina níveis consistentes.
  • Teste eventos críticos e redaction.
  • Configure retenção, rotação e limites.
  • Monitore volume e custo de ingestão.
  • Use IDs de correlação entre serviços.

Conclusão

O logging estruturado com structlog transforma mensagens dispersas em eventos pesquisáveis e consistentes. Com processadores, contexto, integração com logging padrão, saída JSON, proteção de segredos e testes, a aplicação ganha uma base sólida de observabilidade.

Comece pelos eventos mais importantes: início e fim de requisições, operações de negócio, integrações externas e exceções. Defina poucos campos estáveis, acompanhe o volume e evolua a convenção conforme surgirem necessidades reais. O objetivo não é registrar tudo, mas fornecer contexto suficiente para diagnosticar problemas com rapidez e segurança.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Configurações seguras em aplicação Python com Pydantic Settings
    Boas Práticas
    Foto de perfil de Leandro Hirt da Academify

    Pydantic Settings: configurações seguras

    Aprenda a organizar configurações com Pydantic Settings, validar variáveis de ambiente e proteger segredos em projetos Python.

    Ler mais

    Tempo de leitura: 5 minutos
    23/07/2026
    Desenvolvedor programando em Python com Ruff para lint e formatação
    Boas Práticas
    Foto de perfil de Leandro Hirt da Academify

    Ruff no Python: lint e formatação de código passo a passo

    Aprenda a usar Ruff no Python para lint, formatação, correções automáticas, pyproject.toml, VS Code e CI com uma configuração prática.

    Ler mais

    Tempo de leitura: 10 minutos
    22/07/2026
    Limpeza de dados sujos em Python para data cleaning
    Boas Práticas
    Foto de perfil de Leandro Hirt da Academify

    Como limpar dados sujos no Python: Guia prático de Data Cleaning

    No mundo da programação, existe um ditado muito famoso: “Lixo entra, lixo sai”. Isso significa que, não importa o quão

    Ler mais

    Tempo de leitura: 11 minutos
    12/04/2026
    Proteção de API Flask usando autenticação JWT em Python
    Boas Práticas
    Foto de perfil de Leandro Hirt da Academify

    Como proteger sua API Flask com JWT em minutos

    A segurança é um dos pilares mais importantes no desenvolvimento de aplicações modernas. Quando decidimos Como proteger sua API Flask

    Ler mais

    Tempo de leitura: 9 minutos
    05/04/2026
    Como resolver erros com variáveis de ambiente usando python-dotenv
    Boas Práticas
    Foto de perfil de Leandro Hirt da Academify

    Variáveis .env dão erro? Resolva com python‑dotenv em minutos

    Você já passou pela frustração de configurar um projeto, definir suas chaves de API e, ao rodar o código, receber

    Ler mais

    Tempo de leitura: 10 minutos
    27/03/2026
    Medição de tempo de execução de código Python com timeit
    Boas Práticas
    Foto de perfil de Leandro Hirt da Academify

    Descubra como medir o tempo de código com timeit

    Se você já se perguntou por que um trecho de código demora mais que outro ou se uma alteração realmente

    Ler mais

    Tempo de leitura: 12 minutos
    10/03/2026