Em julho de 2013, Nicholas Blumhardt — então engenheiro na
Octopus Deploy — publicou o release inicial de uma biblioteca
.NET chamada Serilog. O slogan era
provocativo: "diagnostic logging in the .NET ecosystem". O que
Serilog propunha não era novo — Splunk e Loggly já vendiam o
conceito —, mas Blumhardt foi o primeiro a empacotar de forma
ergonômica em uma biblioteca de aplicação: cada log entry
é um objeto estruturado, não uma string formatada. A
sintaxe era quase idêntica ao tradicional
String.Format, mas com uma diferença crucial — os
placeholders eram chaves nomeadas que viravam campos
no log final.
A ideia ressoou e, em uma década, virou padrão da indústria.
Python ganhou structlog (Hynek Schlawack, 2013). Go incluiu
log/slog na biblioteca padrão em agosto de 2023
(Go 1.21). JavaScript tem pino (2016) e winston com
structured-format. Java tem Logback, Logstash encoder, e
mais recentemente structured logging nativo via SLF4J 2.x. O
vocabulário convergiu — em 2026, "logging estruturado" é
pré-requisito de qualquer aplicação séria, e quem ainda emite
logs printf-style está deixando observabilidade
em cima da mesa.
Logging é o cross-cutting concern mais clássico que existe — Kiczales usou logging como exemplo motivador no paper original de 1997. A pergunta que parece resolvida é mais sutil do que parece: como propagar contexto entre camadas, como evitar vazamento de dado sensível, como balancear ruído versus sinal, e como integrar com tracing e métricas (conceito 08) sem duplicação. Este conceito articula o que separa logging "feito" de logging "que sustenta produção".
O escopo é deliberado: o aspect de logging — como organizar o cross-cutting concern. O conceito 08 cobre a integração com tracing e métricas via OpenTelemetry; o conceito 11 cobre auditoria como concern separado. Aqui o foco é a disciplina diária do log diagnóstico — formato, campos, contexto, propagação, segurança.
Estruturado versus formatado — a diferença que muda tudo
Considere as duas linhas a seguir, equivalentes do ponto de vista do desenvolvedor que escreve o código:
// formatado (string)
logger.Information($"Pedido {pedidoId} criado para cliente {clienteId} em {duration}ms");
// estruturado (objeto)
logger.Information("Pedido {PedidoId} criado para cliente {ClienteId} em {Duration}ms",
pedidoId, clienteId, duration);
Para um humano lendo o terminal, o resultado em texto é
idêntico: "Pedido 4f2a... criado para cliente
8a91... em 47ms". Para uma ferramenta de busca, são
universos diferentes. A linha de cima sai como
uma string opaca — você pode buscar por
"Pedido" ou por "4f2a", mas não
consegue dizer "me dá todos os logs onde
PedidoId == 4f2a... AND Duration > 100". A linha
de baixo sai como JSON estruturado:
{
"@t": "2026-04-30T14:33:21.482Z",
"@l": "Information",
"@mt": "Pedido {PedidoId} criado para cliente {ClienteId} em {Duration}ms",
"PedidoId": "4f2a-...",
"ClienteId": "8a91-...",
"Duration": 47,
"@m": "Pedido 4f2a-... criado para cliente 8a91-... em 47ms"
}
Em ferramentas como Loki, Elasticsearch, Datadog, ou Splunk, a
diferença é decisiva. Buscar por
{Duration}> 100 AND ClienteId="8a91-..." é
consulta nativa em logs estruturados, e é busca de texto livre
lenta e propensa a erro em logs printf-style. A
consequência: a mesma equipe com a mesma intenção pode ter
observabilidade radicalmente diferente dependendo só dessa
escolha de sintaxe.
O segundo benefício é estabilidade. Em logging estruturado, o
campo PedidoId sempre se chama
PedidoId, mesmo quando a mensagem muda. Renomear
a mensagem ("Pedido criado" → "Pedido salvo com sucesso") não
quebra alertas que dependem do campo. Em logging formatado,
qualquer regex que busca "Pedido (\w+) criado"
quebra silenciosamente quando alguém ajusta a frase. Sistemas
observáveis precisam ser estáveis a refatoração de string —
logging estruturado é o que entrega isso.
O core mínimo — campos que todo log entry deve ter
Há um conjunto de campos que todo log de aplicação séria precisa carregar, idealmente injetados pelo framework para que ninguém precise lembrar. A lista varia ligeiramente entre organizações, mas o núcleo é canônico.
timestamp — em UTC, com precisão
mínima de milissegundos, formato ISO 8601 (RFC 3339). Sem
timezone correto e formato consistente, correlação cross-serviço
vira impossível. Loggers modernos põem por padrão.
level — Trace, Debug, Info,
Warn, Error, Fatal. A discussão sobre o que vai em cada nível
merece capítulo próprio (abaixo). O importante: nível é campo
tipado, filtrável, não substring da mensagem.
service / service.name
— qual serviço/aplicação emitiu o log. Em ambiente
multi-serviços, sem isso a busca consolidada vira inútil.
OpenTelemetry semantic conventions usa
service.name; convenção da equipe varia.
environment — prod,
staging, dev. Logs de prod e dev no
mesmo backend sem separação dão margem a alertas falsos.
trace_id / span_id
— propagados de OpenTelemetry. Permitem juntar log com trace
no Jaeger/Tempo, e log de serviços diferentes que atendem a
mesma request distribuída. Conceito 08 detalha. Sem trace_id,
você consegue ver árvore de chamadas mas não consegue
conectar com os logs que essas chamadas emitiram — fica meia
observabilidade.
correlation_id / request_id
— ID por request HTTP, gerado se ausente. Se você usa
OpenTelemetry, em geral trace_id serve para o
mesmo papel; em sistemas que ainda não têm tracing,
correlation_id é o substituto mínimo. Conceito 04 já mostrou
como middleware injeta.
user_id (quando aplicável) — ID
do usuário que originou a operação. Crucial para
diagnóstico de tipo "esse usuário tá vendo erro 500" — sem
user_id em log, você precisa correlacionar à mão
por timestamp, e em sistemas com mil RPS isso é caça ao
tesouro.
Níveis de log — o que vai em cada um
A semântica de níveis varia ligeiramente entre frameworks,
mas a convenção que sobrevive é razoavelmente unânime.
Articular bem evita a degradação clássica em que tudo
vira Info e o nível perde valor de filtro.
Trace / Verbose — detalhe interno de execução. Útil em desenvolvimento, raramente habilitado em produção. Cobre coisas como "entrando na função X com parâmetros Y" — útil para reconstruir fluxo, mas barulhento demais em escala.
Debug — informação útil para diagnóstico profundo de problema, sem ser detalhe internal mecânico. "Cache miss para chave X", "Retry attempt 2 of 3 para serviço Y". Geralmente desabilitado em produção, ligado por feature flag ou rota especial quando se está investigando.
Info — eventos relevantes do fluxo normal do sistema. "Pedido criado", "Job iniciado", "Cliente logou". A régua é: alguém de operação leria isso para entender o que o sistema está fazendo agora? Se sim, Info. Se é detalhe mecânico, Debug.
Warn — algo subótimo aconteceu, mas o sistema seguiu funcionando. "Retry necessário", "Cache não disponível, caindo para fonte primária", "Dependência externa lenta". Warn não é erro — é sinal de degradação ou comportamento incomum. Se Warn está virando ruído, está sendo usado para erro mascarado.
Error — algo falhou de forma observável para o usuário ou para o sistema. Exception não tratada, request retornando 5xx, job falhando. Error importa — alertas e SLOs geralmente se conectam aqui.
Fatal / Critical — o sistema (ou parte dele) não pode continuar operando. Aplicação não consegue subir, banco indisponível por minuto, perda de dado. Fatal deve ser raro e geralmente acompanha shutdown ou restart.
Há uma regra prática que economiza horas: se um Error
é seguido por uma exceção que sobe e o framework já loga,
você está logando duas vezes. ASP.NET Core, FastAPI
e a maioria dos frameworks logam exceções não-tratadas
automaticamente; logar Error dentro de um catch
e relançar duplica entry. Padrão correto: capture, enriqueça
contexto se útil, e relance — deixe o framework logar uma vez
no nível mais externo.
Serilog em .NET — o canônico
Serilog redefiniu logging em .NET ao ponto que, em 2026, é
praticamente padrão de fato. Microsoft.Extensions.Logging
(a abstração da Microsoft) também suporta logging estruturado
desde .NET Core 1.0 (2016), mas Serilog continua à frente em
ergonomia de configuração e em sinks (destinos de saída).
// Program.cs (ASP.NET Core 10 + Serilog)
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.WithProperty("service", "catalog-api")
.Enrich.WithProperty("environment", builder.Environment.EnvironmentName)
.WriteTo.Console(new JsonFormatter()) // produção: JSON
.WriteTo.OpenTelemetry() // exporta para coletor
.CreateLogger();
builder.Host.UseSerilog();
// uso típico
public class PedidoService
{
private readonly ILogger<PedidoService> _log;
public PedidoService(ILogger<PedidoService> log) => _log = log;
public async Task<Pedido> Criar(CriarPedidoCmd cmd, CancellationToken ct)
{
using (_log.BeginScope(new Dictionary<string, object>
{
["ClienteId"] = cmd.ClienteId,
["ItemCount"] = cmd.Itens.Count
}))
{
_log.LogInformation("Criando pedido");
var pedido = await _repo.SalvarAsync(new Pedido(cmd), ct);
_log.LogInformation("Pedido {PedidoId} criado", pedido.Id);
return pedido;
}
}
}
Note três coisas. Primeiro:
Enrich.FromLogContext() permite que middleware ou
filter adicione propriedades que aparecem em todo log
durante o escopo do request — correlation_id é o caso clássico.
Segundo: BeginScope com dicionário
produz logs onde ClienteId e ItemCount
aparecem como campos, não como string interpolada na mensagem.
Terceiro: o message template com
placeholders nomeados ("Pedido {PedidoId} criado")
é parseado e os argumentos viram campos automaticamente.
structlog em Python
structlog, criado por Hynek Schlawack em 2013, é a biblioteca de referência para logging estruturado em Python. Diferente de logging tradicional, structlog é pipeline-based: cada log passa por uma sequência de processors que transformam o evento até a renderização final. Essa arquitetura permite injetar correlation ID, formatar timestamps, e exportar JSON sem subclassar nada.
import structlog
import logging
# configuração no startup
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars, # injeta context vars
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
structlog.processors.dict_tracebacks,
structlog.processors.JSONRenderer(), # output: JSON
],
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
cache_logger_on_first_use=True,
)
log = structlog.get_logger()
# em algum middleware FastAPI
async def correlation_middleware(request, call_next):
cid = request.headers.get("X-Correlation-ID") or new_id()
structlog.contextvars.bind_contextvars(correlation_id=cid)
try:
return await call_next(request)
finally:
structlog.contextvars.clear_contextvars()
# uso típico
async def criar_pedido(cmd: CriarPedidoCmd) -> Pedido:
bound = log.bind(cliente_id=cmd.cliente_id, item_count=len(cmd.itens))
bound.info("criando pedido")
pedido = await repo.salvar(Pedido.from_cmd(cmd))
bound.info("pedido criado", pedido_id=pedido.id)
return pedido
Dois detalhes. Primeiro:
contextvars é a primitiva do asyncio para
propagar valores entre corotinas — equivalente ao
LogContext do Serilog. Segundo:
log.bind(...) retorna um logger que carrega os
campos extras automaticamente em todos os subsequentes — é
como abrir um escopo. structlog é mais ergonômico
que o logging da stdlib justamente por isso.
slog em Go — biblioteca padrão desde 1.21
Em agosto de 2023, Go 1.21 incluiu o pacote
log/slog. A motivação foi tardia mas direta: a
comunidade tinha convergido em logging estruturado via
bibliotecas externas (zap da Uber, zerolog,
logrus), mas o ecossistema sofria com a divergência
— cada biblioteca tinha API própria e configurações próprias.
Jonathan Amsterdam liderou a proposta de incluir uma API
padrão na stdlib.
package main
import (
"context"
"log/slog"
"os"
)
type ctxKey string
const correlationKey ctxKey = "correlation_id"
func main() {
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
logger := slog.New(handler).With(
"service", "catalog-api",
"environment", "production",
)
slog.SetDefault(logger)
}
// uso
func CriarPedido(ctx context.Context, cmd CriarPedidoCmd) (*Pedido, error) {
log := slog.With(
"cliente_id", cmd.ClienteID,
"item_count", len(cmd.Itens),
)
log.InfoContext(ctx, "criando pedido") // pega correlation_id do ctx
p, err := repo.Salvar(ctx, Pedido{ClienteID: cmd.ClienteID, Itens: cmd.Itens})
if err != nil {
log.ErrorContext(ctx, "falha ao salvar pedido", "err", err)
return nil, err
}
log.InfoContext(ctx, "pedido criado", "pedido_id", p.ID)
return p, nil
}
slog.With retorna um logger derivado com campos
pré-bound — equivalente a BeginScope em Serilog
ou bind em structlog. InfoContext
pega o context.Context, e via um custom
Handler você consegue extrair valores de contexto
(correlation_id, trace_id) e adicioná-los a cada log
automaticamente.
Propagação de contexto — onde está o ouro
Logging estruturado fica inútil se cada camada precisa lembrar de adicionar correlation_id à mão. A virtude da técnica é propagação automática: middleware injeta correlation_id no contexto da request, e todo log emitido durante essa request — em qualquer camada — sai com o ID, sem que ninguém peça. Os mecanismos:
.NET / Serilog:
LogContext.PushProperty dentro de um
using — qualquer log emitido durante esse escopo
ganha a propriedade. Em pipeline async, o ambient flow é
preservado por AsyncLocal<T>.
Python / structlog:
contextvars.bind_contextvars(...) — usa o módulo
contextvars da stdlib (PEP 567, Python 3.7).
asyncio propaga contextvars através de tasks corretamente
desde 3.7.
Go / slog: usa context.Context
padrão. O custom Handler lê valores do context na
hora de emitir e adiciona como campos. Diferente de .NET e
Python, Go exige que você passe o context explicitamente em
cada chamada — disciplina cultural.
Java / SLF4J: usa MDC (Mapped Diagnostic Context), uma estrutura ThreadLocal. Em virtual threads (Java 21+), MDC continua funcionando, mas equipes precisam validar que bibliotecas terceiras propagam corretamente.
Vazamento de dado sensível em log. Alguém escreve
logger.LogInformation("user logged in {User}", user),
e o objeto user tem campo password_hash,
cpf, ou jwt_token. Em logging
estruturado, todos os campos serializáveis vão para o JSON —
e a partir desse momento o dado sensível está em log,
possivelmente em backend de busca, possivelmente exfiltrado
em backup. Toda equipe séria configura
destructuring policies: Serilog tem
Destructure.ByTransforming<User>(...),
structlog tem custom processor para mascarar, slog tem
LogValuer. Sem isso, basta um log por engano e
você tem incidente de privacidade.
Configuração por ambiente — dev versus prod
Boa configuração de logging tem dois modos. Em desenvolvimento, saída humana — texto colorido com timestamp curto, ideal para ler no terminal. Em produção, saída JSON — uma linha por entry, ingestível por qualquer ferramenta. Trocar entre os dois é configuração, não recompilação.
Igualmente importante: nível de log diferente por ambiente.
Dev pode rodar em Debug ou Trace;
prod geralmente em Info com habilitação dinâmica
de Debug via feature flag para diagnóstico
pontual. Logs em Trace em produção custam caro —
um sistema com 1k RPS e Trace habilitado emite ~50k entries por
segundo, geralmente saturando o coletor.
Sampling é a técnica para reduzir volume sem perder visão. Equipes com tráfego alto fazem amostragem por sucesso (ex.: logam 10% dos sucessos, 100% dos erros) ou por path (ex.: 1% dos health checks, 100% dos endpoints de domínio). Sampling em logs é tema avançado, mas vale conhecer a existência: sem ela, sistemas de alto tráfego viram proibitivamente caros em backend de log.
O mesmo log, três bibliotecas
Para fixar a equivalência conceitual, considere o cenário canônico — log de criação de pedido com correlation_id automático e enriquecimento de cliente_id.
using Serilog.Context;
// middleware (registrado no pipeline)
app.Use(async (ctx, next) => {
var cid = ctx.Request.Headers["X-Correlation-ID"]
.FirstOrDefault() ?? Guid.NewGuid().ToString();
using (LogContext.PushProperty("correlation_id", cid))
await next();
});
// service
public async Task<Pedido> Criar(CriarPedidoCmd cmd, CancellationToken ct)
{
using var _ = LogContext.PushProperty("cliente_id", cmd.ClienteId);
_log.LogInformation("criando pedido com {ItemCount} itens", cmd.Itens.Count);
var p = await _repo.SalvarAsync(new Pedido(cmd), ct);
_log.LogInformation("pedido {PedidoId} criado", p.Id);
return p;
}
O using de PushProperty garante
que cliente_id sai em todo log dentro do
escopo, sem precisar mencionar em cada chamada. Stack-friendly,
async-safe via AsyncLocal.
import structlog
from starlette.middleware.base import BaseHTTPMiddleware
log = structlog.get_logger()
class CorrelationMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
cid = request.headers.get("X-Correlation-ID") or new_id()
structlog.contextvars.bind_contextvars(correlation_id=cid)
try:
return await call_next(request)
finally:
structlog.contextvars.clear_contextvars()
# service
async def criar_pedido(cmd: CriarPedidoCmd) -> Pedido:
bound = log.bind(cliente_id=cmd.cliente_id)
bound.info("criando pedido", item_count=len(cmd.itens))
p = await repo.salvar(Pedido.from_cmd(cmd))
bound.info("pedido criado", pedido_id=p.id)
return p
contextvars propaga corretamente em asyncio.
log.bind() retorna logger derivado — equivalente
ao escopo do Serilog, sem o using.
// handler que extrai correlation_id do context
type CtxHandler struct{ slog.Handler }
func (h *CtxHandler) Handle(ctx context.Context, r slog.Record) error {
if cid, ok := ctx.Value(correlationKey).(string); ok {
r.AddAttrs(slog.String("correlation_id", cid))
}
return h.Handler.Handle(ctx, r)
}
// middleware
func WithCorrelationID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cid := r.Header.Get("X-Correlation-ID")
if cid == "" { cid = uuid.NewString() }
ctx := context.WithValue(r.Context(), correlationKey, cid)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// service
func CriarPedido(ctx context.Context, cmd CriarPedidoCmd) (*Pedido, error) {
log := slog.With("cliente_id", cmd.ClienteID)
log.InfoContext(ctx, "criando pedido", "item_count", len(cmd.Itens))
p, err := repo.Salvar(ctx, Pedido{ClienteID: cmd.ClienteID, Itens: cmd.Itens})
if err != nil {
log.ErrorContext(ctx, "falha", "err", err)
return nil, err
}
log.InfoContext(ctx, "pedido criado", "pedido_id", p.ID)
return p, nil
}
Go é o mais explícito: o context flui por todas as chamadas, e o handler decide o que extrair. Mais código, mas ZERO mágica.
Anti-padrões frequentes
Interpolação de string em mensagem.
logger.Info($"Pedido {id} criado") em C# ou
log.info(f"pedido {id} criado") em Python é a
morte do logging estruturado. A string interpolada vira
message opaca, e id não é campo. Em Serilog,
a equipe configura analisador estático que falha o build
quando vê $"..." em log call. Em Python, lint
via ruff ou flake8-logging pega
o caso. Em Go, fmt.Sprintf dentro de
slog.Info(...) tem o mesmo cheiro.
Log de sucesso muito verboso. Logging em cada passo de cada operação parece útil em desenvolvimento e vira ruído em produção. Heurística: cada operação de domínio emite um Info de "iniciei" (opcional) e um Info de "completei com sucesso, dados X" (geralmente útil). Detalhes intermediários ficam em Debug, ligados sob demanda.
Log e exception sem stack trace.
_log.LogError("falha ao criar pedido") sem o
ex faz você ter o erro no log sem a
stack trace. Padrão correto: _log.LogError(ex, "falha
ao criar pedido") em C#, log.exception(...)
em Python (que adiciona stack), "err", err em Go
slog. Sem stack, debug vira fofoca.
Logger criado com new em todo lugar.
Cada lugar onde alguém faz new Logger(...) ou
log = structlog.get_logger("...") sem usar a
configuração central tende a divergir — formatador, sinks,
enrichers diferentes. A regra: uma configuração
central, todos os módulos pegam logger via DI ou via
get_logger(__name__) que herda da configuração.
Antes de adicionar log novo, pergunte: "se eu estiver
tentando diagnosticar um incidente em produção daqui a 6
meses, esse log vai me ajudar a chegar à causa?". Se sim,
merece Info ou Warn com campos
certos. Se é só "passei por aqui", merece Debug
ou nada — o sistema já tem traces. Logging sênior é
intencional, não automático: cada entry registra um evento
do sistema, não um detalhe de código.
Por que importa para a sua carreira
Logs são a primeira ferramenta de diagnóstico em qualquer
incidente, e a qualidade do log determina quanto tempo o
time fica de pé às 3 da manhã investigando. Quem entrega
sistemas com logging estruturado sólido — campos consistentes,
correlation_id propagado, níveis usados com critério, sem
vazamento sensível — entrega autonomia operacional. Em
entrevistas de design, "como você organizaria observabilidade
em uma API nova?" é pergunta recorrente, e a parte sobre logs
diferencia: a resposta forte cita o core de campos, fala de
propagação por context, menciona destructuring para evitar
vazamento, e articula a integração com tracing. Em revisão
de código, cassar PR onde aparece $"{...}" em
log call é serviço prestado ao time inteiro.
Como praticar
-
Auditoria de logs em projeto existente.
Pegue um repositório seu e use
grep/ripgrep para listar todas as chamadas de log. Categorize cada uma: está estruturada (placeholders nomeados ou kwargs) ou formatada (string interpolada)? Carrega correlation_id? Tem camposervice/environment? Usa nível apropriado? Identifique pelo menos cinco oportunidades de melhoria e proponha em PR. -
Pipeline de log em três linguagens. Implemente
o mesmo cenário em ASP.NET Core (Serilog), FastAPI
(structlog) e Go (slog): middleware injetando
correlation_id, service registrando em
Infocom camposcliente_ideitem_count, e exception handler que loga emErrorcom stack trace. Verifique que o JSON de saída tem todos os campos esperados, e que o correlation_id atravessa todos os logs do request. Esse exercício faz você sentir as três bibliotecas no mesmo dia. -
Destructuring policy. Crie um modelo
Usercom camposid,email,password_hash,cpf. Configure cada uma das três bibliotecas para que, ao logar umUser, os campospassword_hashecpfsejam mascarados (por exemplo, mostrar só os 4 últimos dígitos). Verifique que mesmo se alguém escreverlog.Info("login {User}", user)sem cuidado, o sistema não vaza. Esse é o tipo de configuração que evita incidente de privacidade.
Referências para aprofundar
- docs Serilog Documentation.
- docs structlog Documentation.
- docs Go log/slog Documentation.
- docs OpenTelemetry — Logs Specification.
- livro Observability Engineering — Charity Majors, Liz Fong-Jones, George Miranda (O'Reilly, 2022).
- livro Logging in Action — Phil Wilkins (Manning, 2022).
- livro Distributed Systems Observability — Cindy Sridharan (O'Reilly, 2018).
- artigo Structured Logging — The Best Friend You'll Want When Things Go Wrong — Nicholas Blumhardt (post original, 2013).
- artigo The 12 Factor App — Logs — Adam Wiggins / Heroku (2011).
- artigo Structured Logging with slog — Jonathan Amsterdam (Go blog, 2023).
- artigo Logging Levels — A Holistic View — Jamie Tanna (blog, 2023).
- vídeo Logging Best Practices in .NET — Nicholas Blumhardt (NDC, 2018+).