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 structlogNo 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",
)
raiseO 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_dictColoque 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"] == 42Teste 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.







