Logs estruturados representam cada evento como campos, não como uma frase que precisa ser interpretada depois. Isso permite filtrar request_id, operação, duração e resultado em uma plataforma de observabilidade.
Configurar saída JSON
python -m pip install structlog
import logging
import structlog
logging.basicConfig(level=logging.INFO)
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
structlog.processors.JSONRenderer(),
]
)
log = structlog.get_logger("pagamentos")
log.info("pagamento_consultado", pagamento_id="pg_123", duracao_ms=42)
Use nomes de evento estáveis e campos com tipo consistente. Não monte uma mensagem diferente para cada caso. O tutorial de logging em Python explica níveis e handlers que continuam relevantes.
Acrescentar contexto por requisição
Associe request_id no início da requisição com os helpers de structlog.contextvars e limpe o contexto ao final. Assim, funções internas recebem correlação sem passar o identificador em toda assinatura. Em tarefas enviadas a outra thread ou processo, confirme explicitamente como o contexto é propagado.
Registre o identificador interno de uma entidade, mas remova senha, token, autorização, cookie e conteúdo pessoal. Logs costumam ter retenção e acesso diferentes do banco principal.
Testar o contrato dos eventos
Capture eventos em testes e confirme nome e campos essenciais. Evite testar a ordem textual de um JSON. Mudanças no schema de log devem ser revisadas como mudanças de interface, especialmente quando dashboards e alertas dependem delas.
A documentação oficial do structlog, consultada em 22 de julho de 2026, cobre processadores, JSON, integração com logging e variáveis de contexto. Estrutura ajuda a consultar logs, mas a qualidade ainda depende de eventos claros, cardinalidade controlada e política de privacidade.
Entender o pipeline de processadores
Cada chamada ao logger começa com um dicionário de evento. Os processadores recebem esse dicionário em sequência, acrescentam ou transformam campos e, no fim, um renderer produz a saída. A ordem importa: o timestamp e o nível precisam ser adicionados antes do JSONRenderer, que encerra a cadeia convertendo o evento.
Durante o desenvolvimento, troque apenas o renderer:
import os
renderer = (
structlog.processors.JSONRenderer()
if os.getenv("APP_ENV") == "production"
else structlog.dev.ConsoleRenderer(colors=True)
)
Os campos anteriores ao renderer devem ser os mesmos nos dois ambientes. Isso evita descobrir somente em produção que um dashboard espera duration_ms, mas o código emite elapsed. Cores são úteis em um terminal interativo, porém devem ser desativadas ao redirecionar a saída para arquivo ou coletor.
Inclua StackInfoRenderer e um formatador de exceções quando a aplicação realmente precisar dessas informações. Não registre str(exc) como único diagnóstico: ele perde tipo e traceback. Use log.exception("pedido_falhou", pedido_id=pedido_id) dentro do bloco de tratamento. Preserve a mensagem do evento como classificação estável e coloque detalhes variáveis em campos.
Integrar com o logging padrão
Bibliotecas de terceiros normalmente escrevem no módulo logging. Se a aplicação configura somente um logger separado do structlog, parte dos eventos terá formato diferente. ProcessorFormatter permite processar registros dos dois caminhos e produzir uma saída consistente. A configuração completa varia conforme handlers e servidor, mas o princípio é único: configure o logging uma vez no ponto de entrada e impeça propagação duplicada.
Duplicatas geralmente aparecem quando um logger possui handler próprio e também propaga para o logger raiz. Antes de adicionar filtros, inspecione handlers, propagate e a configuração do servidor ASGI. Em aplicações importáveis, evite executar basicConfig() como efeito colateral do módulo; deixe a aplicação ou o processo decidir destino e nível.
Níveis também fazem parte do contrato. Use debug para diagnóstico detalhado, info para marcos normais, warning para uma condição recuperável, error para falha da operação e exception quando o traceback é necessário. Um erro esperado de validação não deve gerar traceback a cada requisição.
Propagar contexto de requisição
No início de cada requisição, limpe o contexto para impedir vazamento entre operações reutilizadas pelo servidor. Depois vincule identificadores seguros:
from structlog.contextvars import bind_contextvars, clear_contextvars
async def middleware(request, call_next):
clear_contextvars()
request_id = request.headers.get("X-Request-ID") or gerar_id()
bind_contextvars(request_id=request_id, method=request.method)
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
Valide o identificador recebido e limite seu tamanho antes de registrá-lo. Um cliente não deve conseguir inserir quebras de linha ou milhares de caracteres no sistema de logs. Vincule também campos de domínio, como order_id, somente durante a operação pertinente e remova-os ao terminar para que não contaminem eventos posteriores.
contextvars acompanha tarefas assíncronas no mesmo contexto, mas a propagação entre threads, filas e processos não é automática em todos os casos. Ao enviar uma tarefa para um worker, inclua o identificador de correlação na mensagem e vincule-o novamente no consumidor. Não envie todo o contexto sem revisar: ele pode conter campos inadequados para outro sistema.
Definir um schema de eventos
Escolha nomes de campo previsíveis. Prefira duration_ms sempre numérico, success booleano e timestamps em UTC. Não alterne entre "42 ms", 42 e 0.042 para a mesma medida. Um pequeno catálogo documentando eventos críticos ajuda quem cria alertas e reduz divergências entre equipes.
Evite cardinalidade alta em campos usados como labels de métricas ou índices. route="/users/{id}" é mais agregável que a URL concreta de cada usuário. IDs ainda podem ser úteis para investigação no corpo pesquisável, mas não precisam virar dimensão indexada. Controle também o volume: registrar cada item de um lote pode custar mais do que um evento resumido com quantidade, falhas e duração.
Proteger dados e reduzir riscos
Use uma lista positiva de campos permitidos em eventos sensíveis. Tentar remover depois todas as possíveis variações de password, token e authorization é frágil. Nunca registre cabeçalhos inteiros, strings de conexão ou objetos completos de usuário. Quando um identificador pessoal não for necessário, omita ou aplique pseudonimização aprovada pela política da organização.
Redação precisa acontecer antes do renderer e antes de qualquer handler externo. Um processador pode substituir chaves conhecidas por "[REDACTED]", mas deve tratar estruturas aninhadas e ser coberto por testes. Logs também precisam de retenção, controle de acesso e procedimento de exclusão compatíveis com a finalidade declarada.
Testar e operar
structlog.testing.capture_logs() é útil para testes pequenos, embora não execute todos os processadores. Para validar a configuração real, direcione a saída a um stream, decodifique cada linha JSON e confira tipos e campos obrigatórios. Teste exceções, contexto limpo entre requisições e ausência de segredos conhecidos.
Em produção, envie JSON para stdout e deixe o ambiente de execução coletar e encaminhar os eventos, salvo requisito operacional diferente. Rotação, retenção e entrega normalmente pertencem ao agente ou plataforma, não ao processo web. Se o destino ficar indisponível, a política de bloqueio ou descarte deve ser explícita para que logging não derrube o serviço silenciosamente.
Comece com poucos eventos que respondam perguntas operacionais concretas: qual operação falhou, para qual recurso, em quanto tempo e com qual correlação. Revise consultas e alertas periodicamente. Um log estruturado vale pelo diagnóstico que permite, não pela quantidade de JSON armazenada.