HTTP logging e diagnóstico de requisições
Objective
Quando um endpoint se comporta mal em produção, você quer ver o que chegou e o que saiu, sem vazar os dados que a requisição carregava. O ASP.NET Core tem dois middlewares para isso, o HTTP logging e o W3C logging, mais o logging estruturado com ids de correlação para amarrar tudo. Este conceito mostra o que cada um escreve por padrão (menos do que a maioria espera), o que liga as partes sensíveis, como desligar por endpoint ou por requisição e como um trace id aparece em todas as linhas. Tudo foi executado no SDK 10.0.100 (runtime 10.0.0) contra uma minimal API pequena.
Use Cases
- Descobrir por que um cliente recebe 400: quais headers e qual corpo o servidor realmente viu.
- Manter um access log em formato padrão para ferramentas que já o leem.
- Registrar uma requisição sem nunca gravar senha, token ou segredo de query string em disco.
- Seguir uma requisição pelas linhas de log do middleware e do seu próprio código.
Deep Dive
O HTTP logging fica desligado até você pedir duas vezes
Duas chamadas habilitam o middleware, AddHttpLogging e UseHttpLogging. Isso não basta para ver nada. O template do projeto define a categoria Microsoft.AspNetCore como Warning, e o middleware loga em Information. Só com as duas chamadas, minha execução imprimiu zero linhas do HttpLoggingMiddleware. A correção da documentação é uma entrada de categoria no appsettings.Development.json:
plaintext"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
(Passei a mesma chave como argumento de linha de comando.) Com isso, um GET /search?q=secret-token enviado com os headers Authorization e X-Correlation-Id produziu duas mensagens, com event ids 1 e 2:
textRequest: Method: GET Path: /search Authorization: [Redacted] X-Correlation-Id: [Redacted] Response: StatusCode: 200 Content-Type: text/plain; charset=utf-8
Dois padrões para notar. Os valores de header só são logados para nomes de uma lista de permitidos; o resto aparece como [Redacted] (a documentação descreve RequestHeaders e ResponseHeaders como os conjuntos cujos valores são logados). E a query string ?q=secret-token não aparece na saída.
O que os flags de campo realmente incluem
HttpLoggingFields é um enum de flags. Imprimi os valores:
textRequestPath=0x1 RequestQuery=0x2 RequestProtocol=0x4 RequestMethod=0x8 RequestScheme=0x10 RequestProperties=0x1D ResponseStatusCode=0x20 RequestHeaders=0x40 ResponseHeaders=0x80 RequestBody=0x400 ResponseBody=0x800 Duration=0x1000 All=0x1CFD
RequestProperties vale 0x1D, que deixa de fora RequestQuery (0x2), e All (0x1CFD, que também omite os dois flags de trailers) idem. A query string é a única coisa que All não loga. Você ativa de propósito:
csharpo.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.RequestQuery;
e a linha QueryString: ?q=secret-token aparece. Pergunte-se se deveria: query strings são onde tokens e e-mails vão parar.
Corpos, limites e uma linha combinada
Com All, limite de corpo 40 e CombineLogs = true, um POST /login com corpo JSON gerou uma única mensagem (event id 9) em vez de duas:
textRequest and Response: Authorization: [Redacted] X-Correlation-Id: abc-123 (adicionado em RequestHeaders) RequestBody: {"user":"alice","password":"hunter2","no RequestBodyStatus: [Truncated by RequestBodyLogLimit] ResponseBody: {"ok":true} Duration: 6,357 (ms; a vírgula é o separador decimal desta máquina)
A senha está no log. O limite só truncou, e só porque o corpo era longo. A documentação avisa que o HTTP logging pode registrar informação pessoal identificável e reduzir o desempenho, principalmente com corpos. Trate o log de corpo como diagnóstico temporário, não como configuração.
Controle por endpoint e por requisição
WithHttpLogging (ou o atributo [HttpLogging]) sobrescreve os campos globais para um endpoint:
csharpapp.MapGet("/health", () => "ok").WithHttpLogging(HttpLoggingFields.None);
app.MapGet("/dur", () => "d").WithHttpLogging(HttpLoggingFields.Duration | HttpLoggingFields.ResponseStatusCode);/health não escreveu nada, que é o que você quer para uma sonda chamada a cada poucos segundos. /dur escreveu só uma mensagem de StatusCode e uma de Duration. Para decisões que dependem da requisição, registre um IHttpLoggingInterceptor com AddHttpLoggingInterceptor. A documentação lista a ordem: opções globais, depois configuração do endpoint, depois o interceptor, que pode mudar ambos. O meu removeu o corpo de /login, substituiu a query e adicionou o trace id:
csharppublic ValueTask OnRequestAsync(HttpLoggingInterceptorContext c)
{
if (c.HttpContext.Request.Path.StartsWithSegments("/login"))
c.LoggingFields &= ~HttpLoggingFields.RequestBody;
if (c.TryDisable(HttpLoggingFields.RequestQuery)) c.AddParameter("Query", "[hidden]");
c.AddParameter("TraceId", c.HttpContext.TraceIdentifier);
return default;
}textQuery: [hidden] TraceId: 0HNPAULADS3DJ:00000001 Method: POST Path: /login ResponseBody: {"ok":true}
A redação tem que cobrir a resposta também: GET /search?q=secret-token devolveu o valor, e ResponseBody: q=secret-token foi logado mesmo com a query escondida. A documentação também descreve um caminho de redação de dados (AddHttpLoggingRedaction com AddRedaction); não o executei.
W3C logging: um arquivo, não um logger
AddW3CLogging mais UseW3CLogging escreve um arquivo de log W3C estendido, não mensagens de ILogger. Apontado para uma pasta de rascunho, uma requisição e um 404 produziram:
text#Version: 1.0 #Fields: date time c-ip s-computername s-ip s-port cs-method cs-uri-stem cs-uri-query sc-status time-taken cs-version cs-host cs(User-Agent) cs(Referer) 2026-10-15 19:48:59 ::1 <machine> ::1 5401 GET /search ?q=secret-token 200 4.3882 HTTP/1.1 localhost:5401 curl/8.7.1 - 2026-10-15 19:48:59 ::1 <machine> ::1 5401 GET /nope - 404 0.1161 HTTP/1.1 localhost:5401 curl/8.7.1 -
O nome do arquivo foi w3clog-20261015.0000.txt. Note o que difere do HTTP logging: a query string entra por padrão, e o nome do host do servidor também. A documentação diz que os padrões excluem UserName e Cookie e dá estes padrões: ./logs/ sob a raiz de conteúdo, limite de 10 MB, 4 arquivos retidos, flush de 1 segundo. Como escreve em disco, garanta que a pasta exista e seja rotacionada, principalmente numa imagem de container somente leitura.
Correlação e LoggerMessage
Suas próprias linhas de log precisam dos mesmos identificadores. Os scopes do console vêm desligados no template, então habilitei (Logging:Console:IncludeScopes=true) e loguei a partir de um método gerado por source generator:
csharpstatic partial class Msgs
{
[LoggerMessage(EventId = 100, Level = LogLevel.Information,
Message = "Handled order for {User} with {Items} items")]
public static partial void Handled(ILogger logger, string user, int items);
}Enviando um header traceparent do W3C, tanto a minha linha quanto a do middleware carregaram o scope abaixo, e o TraceId é igual ao que enviei:
text=> SpanId:729c0cc2e7b2a4cf, TraceId:0af7651916cd43dd8448eb211c80319c, ParentId:b7ad6b7169203331 => ConnectionId:0HNPAULI8S6MD => RequestPath:/corr RequestId:0HNPAULI8S6MD:00000001 Handled order for alice with 3 items
Repare também que o próprio traceparent saiu como [Redacted], porque não está na lista de permitidos; o trace id continua disponível pelo scope. Os placeholders {User} e {Items} continuam nomeados, então um destino estruturado consegue consultá-los.
Trade-offs
- Corpos são um vazamento esperando acontecer. Logue só para um diagnóstico curto e dirigido, com limite pequeno.csharp
o.RequestBodyLogLimit = 1024; // ainda loga o primeiro 1 KB de um formulário de senha - A lista de permitidos é uma rede de segurança, não uma política. Adicionar um nome em
RequestHeadersloga o valor dali em diante, inclusiveAuthorizationse você adicionar. - Dois logs, dois padrões. O HTTP logging omite a query string, o W3C inclui. Saiba em qual arquivo seus segredos podem cair.
- Interceptors custam código em toda requisição. Dão controle por requisição, mas um bug neles afeta todo o tráfego. Mantenha-os minúsculos.
- Sondas e arquivos estáticos geram ruído. Desligue com
WithHttpLogging(HttpLoggingFields.None)em vez de filtrar depois. - Corpos de resposta vazam também. Redigir a requisição não basta quando a resposta ecoa a entrada.