Introdução
Toda vez que um problema acontece em produção, a primeira coisa que fazemos é abrir os logs. Mas em quantas vezes você encontrou algo assim?
| |
Sem OrderId. Sem CustomerId. Sem saber em qual instância aconteceu ou em qual versão da aplicação. Esse tipo de log existe em milhares de aplicações — e não ajuda absolutamente ninguém.
O problema não é falta de log. É falta de contexto, falta de estrutura e falta de controle. Quando a aplicação precisa de mais detalhes, você precisa alterar a configuração, fazer deploy, esperar o restart — e torcer para capturar o erro antes de reverter.
Neste artigo, vou mostrar uma estratégia completa de logging para .NET 8 que resolve esses três problemas de uma vez:
- Estruturado: cada entrada de log carrega propriedades tipadas, não apenas texto plano
- Contextual: toda linha de log inclui automaticamente informações da aplicação, do host e do request
- Dinâmico: o nível de log pode ser alterado em produção com um timer de reversão automática — sem deploy, sem restart
Tudo isso usando ILogger<T> nativo, sem criar wrappers desnecessários, com compliance com SonarQube e integrado ao Azure Application Insights.
A implementação completa está disponível no repositório blog-zocateli-sample no GitHub. Ao longo do artigo, vou mostrar os trechos-chave e explicar o raciocínio por trás de cada decisão. Se quiser ver o contexto completo em qualquer momento, consulte os fontes no repositório.
📦 Código-fonte: A implementação completa deste artigo está no repositório blog-zocateli-sample no GitHub. Clone, explore e adapte ao seu contexto.
💡 Este artigo complementa o Pipeline de Configuração do .NET 8+, que explica como o sistema de configuração,
IOptions<T>e secrets funcionam. A estratégia de logging descrita aqui depende diretamente desses conceitos.
O Problema: Por Que Seus Logs Não Ajudam
Considere esses dois cenários reais de log em produção:
Antes — log sem contexto:
| |
Um analista de suporte olha para isso e não consegue responder nenhuma pergunta: qual pedido? De qual cliente? Em qual instância? Era a versão nova ou a antiga? Esse erro reproduz ou foi pontual?
Depois — log estruturado com contexto:
| |
Com esse segundo formato, o analista pode:
- Filtrar por
OrderIdpara ver tudo que aconteceu com aquele pedido - Agrupar por
HostNamepara identificar se o problema é de uma instância específica - Usar o
CorrelationIdpara rastrear todo o pipeline de um único request - Comparar
ApplicationVersionpara saber se o bug é da versão nova
O custo de troubleshooting cai drasticamente. Em vez de horas garimpando logs genéricos, você encontra a causa em minutos com uma query no Application Insights.
💡 Um log sem contexto em produção é como procurar um erro no Google sem digitar a mensagem de erro — tecnicamente você está pesquisando, mas não vai encontrar nada útil.
Logging Estruturado: O Que É e Por Que Importa
Logging estruturado significa que cada entrada de log carrega propriedades tipadas e indexáveis, não apenas uma string concatenada.
No .NET, a diferença é sutil no código mas enorme no resultado:
| |
Quando você usa o template com {OrderId}, o ILogger preserva o valor como uma propriedade separada. No Application Insights, ela aparece em customDimensions e pode ser filtrada, agregada e correlacionada com queries KQL. Quando você usa string interpolation com $, o valor é embutido na mensagem — o Application Insights recebe apenas texto e não consegue indexar.
As consequências práticas são diretas:
- Com propriedades estruturadas:
customDimensions["OrderId"] == "3fa85f64..."— filtrável - Com string interpolada: precisa fazer
containsou regex na mensagem — lento e impreciso
Além da questão funcional, o SonarQube marca string interpolation em logs como regra S6664 (Log message template should be compile-time constant), já que pode haver impacto de performance e perde-se a capacidade de agrupar mensagens semelhantes.
⚠️ Nunca use
$"Pedido {orderId}"em chamadas de log. Isso cria uma nova string a cada chamada (mesmo quando o nível de log está desabilitado) e perde a propriedade estruturada. Use sempre o template:"Pedido {OrderId}".
Enriquecimento Automático: Contexto da Aplicação em Todo Log
Se cada desenvolvedor precisa lembrar de incluir ApplicationName, HostName e CorrelationId em cada chamada de log, isso não vai acontecer. A solução é enriquecer automaticamente.
Middleware com ILogger.BeginScope()
O ILogger suporta escopos — um bloco que adiciona propriedades a todas as entradas de log feitas dentro dele. Combinando isso com um middleware, garantimos que todo log de qualquer request carrega o contexto da aplicação:
| |
O middleware é registrado como singleton (por implementar IMiddleware) e ativado no pipeline:
| |
As propriedades de ApplicationName e Version vêm de uma seção dedicada do appsettings.json:
| |
ℹ️ A classe
LoggingOptionsé configurada viaIOptions<T>pattern. Se você quer entender como esse mecanismo funciona em profundidade, veja o artigo sobre Pipeline de Configuração do .NET 8+.
TelemetryInitializer para Application Insights
O BeginScope() enriquece os logs do ILogger, mas a telemetria que o Application Insights captura automaticamente (requests HTTP, dependências, exceptions) não passa pelo ILogger. Para garantir que toda telemetria carregue as mesmas propriedades, usamos um ITelemetryInitializer:
| |
Registrado no DI como:
| |
Com essas duas peças, você tem cobertura completa: LogEnrichmentMiddleware para logs do ILogger e ApplicationTelemetryInitializer para request traces, dependency calls e exceptions capturadas automaticamente pelo SDK.
Nível de Log Dinâmico com Timer Automático
Cenário real: sua aplicação em produção está com um comportamento estranho. Você precisa ver logs de nível Debug para entender o que está acontecendo. Mas o appsettings.json em produção está configurado com Information.
O caminho tradicional seria: alterar a configuração, fazer deploy (ou restart), capturar os logs detalhados, e depois desfazer tudo para não afogar o Application Insights em volume (e custo). Esse processo pode levar de minutos a horas dependendo do seu pipeline.
A solução proposta aqui é um endpoint administrativo que altera o nível de log em runtime, com timer de reversão automática:
| |
Após 15 minutos, o nível volta automaticamente ao configurado no appsettings.json. Sem deploy. Sem restart. Sem risco de esquecer o Debug ligado.
O Mecanismo: ConfigurationProvider Customizado
O sistema de logging do .NET lê os níveis de log do pipeline de configuração. A chave que define o nível padrão é Logging:LogLevel:Default, e por categoria fica Logging:LogLevel:{CategoryName}.
A estratégia é criar um ConfigurationProvider customizado que pode ser atualizado em runtime e que, quando alterado, dispara OnReload() para notificar o framework:
| |
O ponto crítico é o registro: esse provider deve ser adicionado por último no pipeline de configuração, para que seu valor sobrescreva o do appsettings.json:
| |
O Serviço com Timer
O DynamicLogLevelService orquestra a lógica de alteração e reversão automática:
| |
Pontos importantes desta implementação:
- O
Timeré criado comTimeout.InfiniteTimeSpancomo period — ele executa uma única vez após odueTime - Se
SetLogLevelfor chamado novamente antes do timer expirar, o timer anterior é descartado e um novo é criado - O
lockgarante thread-safety entre o timer (que executa em uma thread do ThreadPool) e chamadas simultâneas IDisposableé implementado para cleanup correto doTimer
💡 Proteja esse endpoint com autorização adequada — em produção, apenas administradores devem alterar o nível de log. Veja o artigo sobre Autenticação e Autorização com JWT, OAuth2 e OpenID para estratégias de proteção.
ILogger Nativo: Sem Reinventar a Roda
Um padrão que eu vejo com frequência preocupante é a criação de um ICustomLogger ou LogService que encapsula o ILogger<T>:
| |
Esse wrapper quebra diversos mecanismos:
- A categoria do log vira sempre
CustomLoggerem vez da classe real que originou o log - Filtros por namespace (
Logging:LogLevel:MeuNamespace) deixam de funcionar - A rastreabilidade no Application Insights é prejudicada
- Testes ficam mais complexos (precisa mockar o wrapper e não o ILogger)
A abordagem correta no .NET moderno é usar ILogger<T> diretamente e o atributo [LoggerMessage] para gerar os métodos de log via source generator:
| |
Benefícios do [LoggerMessage] Source Generator
- Performance: o source generator cria código que verifica o nível de log antes de montar a mensagem — zero alocação quando o log está desabilitado
- AOT-friendly: compatível com Native AOT, sem reflection em runtime
- SonarQube compliance: resolve as regras S6664 (template compile-time constant), S2629 (logging guard) e S6667 (structured logging)
- EventId: cada mensagem tem um ID numérico fixo, facilitando alertas e dashboards
- Testável: como são métodos estáticos com
ILoggercomo parâmetro, é trivial passar um mock nos testes
O uso é direto em qualquer endpoint ou serviço:
| |
⚠️ Criar um
ICustomLoggerque encapsulaILogger<T>adiciona uma camada sem valor, quebra a rastreabilidade e dificulta os testes. UseILogger<T>diretamente com[LoggerMessage]— ele é a abstração correta.
Integração com Azure Application Insights
O Application Insights é o APM (Application Performance Management) do Azure que captura automaticamente requests HTTP, dependências (banco de dados, HTTP clients, filas) e exceptions. O logging estruturado que implementamos complementa essa telemetria automática com o contexto de negócio.
Setup
A configuração é mínima — uma linha no Program.cs e a connection string na configuração:
| |
| |
⚠️ Nunca coloque a connection string diretamente no código-fonte ou no
appsettings.jsoncommitado. Use variáveis de ambiente, Azure Key Vault ou User Secrets para desenvolvimento local. Veja como configurar no artigo Pipeline de Configuração do .NET 8+.
Como o Log Estruturado Aparece no Application Insights
Quando você faz um log usando [LoggerMessage] com propriedades estruturadas, cada propriedade aparece em customDimensions no Application Insights. Combinando com as propriedades injetadas pelo LogEnrichmentMiddleware e pelo ApplicationTelemetryInitializer, o resultado é:
| Coluna | Valor |
|---|---|
message | Order 3fa85f64… created for customer C-1234, total: 150.00 |
customDimensions.OrderId | 3fa85f64-5717-4562-b3fc-2c963f66afa6 |
customDimensions.CustomerId | C-1234 |
customDimensions.Total | 150.00 |
customDimensions.ApplicationName | SampleApi |
customDimensions.ApplicationVersion | 1.0.0 |
customDimensions.HostName | pod-api-7b4f9c |
customDimensions.CorrelationId | 0HN6KQ2JH2S5E:00000001 |
Query KQL para Troubleshooting
Com essas propriedades indexadas, queries no Application Insights se tornam cirúrgicas:
| |
ℹ️ O Application Insights já captura automaticamente requests HTTP, dependências e exceptions — o log estruturado complementa com o contexto de negócio que só a sua aplicação conhece.
A Aplicação de Exemplo
A aplicação de exemplo implementa todos os conceitos deste artigo em uma Minimal API .NET 8. A estrutura do projeto organiza os componentes de logging em uma pasta dedicada:
| |

O diagrama acima mostra como os componentes se conectam: cada request HTTP passa pelo LogEnrichmentMiddleware que abre um BeginScope() com as propriedades de contexto. Qualquer chamada de log feita dentro daquele request — seja nos endpoints, serviços ou em bibliotecas internas — herda automaticamente essas propriedades. Em paralelo, o ApplicationTelemetryInitializer garante que a telemetria automática do Application Insights (requests, dependencies) também carregue as mesmas informações.
Como Rodar Localmente
| |
Testar o endpoint de log dinâmico:
| |
O repositório completo com instruções detalhadas está em github.com/lzocateli/blog-zocateli-sample — Logging.
Dicas e Boas Práticas
Aqui estão as práticas que considero essenciais para uma estratégia de logging robusta:
Use categorias de log com critério: o .NET usa o namespace da classe como categoria (
ILogger<OrderService>gera categoriaSampleApi.Endpoints.OrderService). Isso permite filtrar por namespace noappsettings.json— por exemplo,"SampleApi.Endpoints": "Debug"ativa debug apenas nos endpoints sem afetar o resto da aplicação.Nunca logue dados sensíveis: nomes, CPFs, tokens, senhas e dados financeiros não devem aparecer em logs. Além do risco de segurança, a LGPD exige proteção de dados pessoais. Se precisar rastrear um usuário, use um identificador opaco como
CustomerId.Controle o custo do Application Insights: em alto volume, o Application Insights cobra por ingestão de dados. Use sampling adaptativo para reduzir volume sem perder visibilidade. A configuração padrão do SDK já inclui sampling — evite desabilitá-lo sem cálculo de custo.
EventId consistente por mensagem: cada
[LoggerMessage]deve ter umEventIdúnico e fixo. Isso permite criar alertas no Azure Monitor baseados no EventId (mais confiável que regex na mensagem) e facilita dashboards de operação.Ambiente de desenvolvimento com JSON console: configure
appsettings.Development.jsonpara usar oJsonConsoleFormattercomIncludeScopes: true. Isso mostra as propriedades estruturadas direto no terminal durante o desenvolvimento, sem precisar do Application Insights.Teste os logs: com
[LoggerMessage]eILogger<T>, é trivial verificar nos testes unitários se os logs corretos foram emitidos. A aplicação de exemplo inclui testes do middleware que verificam as propriedades injetadas no scope.Log de operações administrativas como Warning: alterações de nível de log, reinicializações e operações manuais devem ser logados como
Warning— visíveis por padrão e fáceis de auditar.
Conclusão
Logging eficiente não é sobre quantidade — é sobre qualidade e contexto. Neste artigo, cobrimos três pilares que, juntos, transformam logs de ruído em ferramenta de diagnóstico:
- Estrutura: propriedades tipadas com
[LoggerMessage]source generator em vez de strings concatenadas - Contexto automático:
LogEnrichmentMiddlewarecomBeginScope()eApplicationTelemetryInitializergarantem que toda entrada de log carrega as informações da aplicação, do host e do request - Controle dinâmico:
ConfigurationProvidercustomizado comOnReload()e timer permite ativarDebugem produção de forma segura e temporária
Tudo isso funciona com o ILogger<T> nativo do .NET — sem wrappers, sem abstrações inventadas, sem quebrar SonarQube. O Application Insights recebe as propriedades em customDimensions, permitindo queries KQL cirúrgicas para troubleshooting.
Se sua aplicação ainda usa _logger.LogInformation($"Processando pedido {id}"), agora é um bom momento para evoluir. Clone o repositório de exemplo, adapte para o seu contexto e veja a diferença na próxima vez que precisar investigar um problema em produção.
Para trabalhar com logs em serviços background de longa duração, veja como aplicar essa mesma estratégia em .NET Worker e Background Service.
Leia Também
- Pipeline de Configuração do .NET 8+ —
IOptions<T>, Secrets e configuração externalizada que sustentam a estratégia de logging - .NET Worker e Background Service — logging em serviços background de alto volume
- Gargalo em Banco de Dados com EF Core — troubleshooting de performance onde logs são essenciais
- Programação Assíncrona com C# — async/await e contexto de execução que afetam log scopes
- Autenticação e Autorização com JWT e OAuth2 — protegendo endpoints administrativos como o de log level dinâmico
Referências
- Logging in .NET — Microsoft Learn
- LoggerMessage source generator — Microsoft Learn
- Compile-time logging source generation — Microsoft Learn
- Application Insights for ASP.NET Core — Microsoft Learn
- Configuration in ASP.NET Core — Microsoft Learn
- Log scopes — Microsoft Learn
- Azure Monitor KQL reference
- Application Insights sampling — Microsoft Learn
- SonarQube rule S6664 — Log message template
- SonarQube rule S2629 — Logging templates should be constant
- Repositório blog-zocateli-sample — Logging — Código-fonte completo dos exemplos deste artigo

Ao comentar, você concorda com nossa Política de Privacidade, Termos de Uso e Política de Exclusão de Dados.