BrandGhost
HttpClient Logging and Observability in .NET 10: ILogger, DelegatingHandler, and OpenTelemetry

HttpClient Logging and Observability in .NET 10: ILogger, DelegatingHandler, and OpenTelemetry

HttpClient Logging and Observability in .NET 10: ILogger, DelegatingHandler, and OpenTelemetry

When you're building distributed .NET applications, the HTTP calls your services make can be a silent source of bugs, performance problems, and production mysteries. HttpClient logging in .NET is one of the most practical ways to shine a light on those calls. Yet many teams discover too late that they're flying blind -- no trace IDs, no request durations, no way to correlate a slow response with its downstream cause.

.NET 10 ships with a genuinely rich observability stack right out of the box. You get built-in ILogger integration via IHttpClientFactory, low-level events via DiagnosticListener, distributed tracing via OpenTelemetry, and first-class metrics from the System.Net.Http meter. The good news is that each layer is independent -- you can start with something simple and add more depth as your needs grow.

This guide walks through every layer of httpclient logging in .NET, from basic log-level configuration to a full distributed tracing and metrics setup you can drop into a production service. All examples target .NET 10 and C# 13.

For a broader look at HttpClient usage patterns, check out HttpClient in C#: The Complete Guide.

What .NET 10 Logs By Default with IHttpClientFactory

Understanding HttpClient logging in .NET 10 starts with what the framework does on your behalf. When you register an HttpClient using IHttpClientFactory, the framework automatically wires up several built-in logging behaviors -- a capability available since .NET Core 2.2 that carries forward unchanged into .NET 10. You do not need to add any extra packages or write any handlers yourself -- these logs appear as long as your application has a configured logger.

The framework logs through two built-in logger categories:

  • System.Net.Http.HttpClient.[ClientName].LogicalHandler -- logs the start and end of the logical request lifecycle, including retry attempts from Polly or custom handlers.
  • System.Net.Http.HttpClient.[ClientName].ClientHandler -- logs the actual network-level request and response, including the final HTTP status code.

By default, both categories emit log entries at the Information level for the request start and stop, and at the Warning or Error level for failures. Here is what those log entries look like in a typical application:

info: System.Net.Http.HttpClient.WeatherClient.LogicalHandler[100]
      Start processing HTTP request GET https://api.example.com/weather

info: System.Net.Http.HttpClient.WeatherClient.ClientHandler[100]
      Sending HTTP request GET https://api.example.com/weather

info: System.Net.Http.HttpClient.WeatherClient.ClientHandler[101]
      Received HTTP response headers after 184.3ms - 200

info: System.Net.Http.HttpClient.WeatherClient.LogicalHandler[101]
      End processing HTTP request after 185.1ms - 200

The duration logged at the LogicalHandler level includes time spent in custom DelegatingHandler pipelines -- which makes it useful for measuring the true cost of a call from your code's perspective.

Configuring Built-In Logging Levels and Filtering HttpClient Logs

Configuring HttpClient logging in .NET is mostly a matter of appsettings.json changes. The default Information-level logging can get noisy in high-throughput services. Fortunately, you can control it precisely by targeting those built-in logger categories.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System.Net.Http.HttpClient": "Warning",
      "System.Net.Http.HttpClient.WeatherClient": "Information"
    }
  }
}

This configuration suppresses Information-level logs for all HTTP clients globally (System.Net.Http.HttpClient) while keeping them enabled for a specific named client (WeatherClient). If you have many named clients, this approach lets you tune verbosity per-client without code changes.

You can also suppress the response body logging (which is disabled by default anyway) or enable Trace-level logs if you need to see request headers during debugging:

{
  "Logging": {
    "LogLevel": {
      "System.Net.Http.HttpClient.WeatherClient.ClientHandler": "Trace"
    }
  }
}

Note that Trace logs include request and response headers. Avoid leaving this enabled in production -- especially in long-retention log sinks -- since those headers often include Authorization tokens or session cookies. The header redaction section below covers how to handle that safely.

The key insight for HttpClient logging configuration: adjust verbosity per-environment, not per-deployment. Use appsettings.Development.json for verbose output and keep production quiet.

Custom Request Logging with DelegatingHandler

For application-level logging of HttpClient requests, a DelegatingHandler is the correct approach. It is type-safe, officially recommended, and slots cleanly into the IHttpClientFactory pipeline.

Why not DiagnosticListener? The DiagnosticListener event bus is the low-level mechanism that powers built-in ILogger integration and OpenTelemetry instrumentation. When it fires System.Net.Http.HttpRequestOut.Start and System.Net.Http.HttpRequestOut.Stop, the payloads are not HttpRequestMessage and HttpResponseMessage directly. The .NET runtime wraps them in private internal structs (ActivityStartData, ActivityStopData). A guard like value.Value is HttpRequestMessage request will always evaluate to false at runtime -- the code silently does nothing and there is no compiler warning to tip you off. DiagnosticListener is an internal diagnostic mechanism, not an application logging API. Use a DelegatingHandler instead.

Here is a complete DelegatingHandler that logs method, URL, status code, and duration:

using System.Diagnostics;
using Microsoft.Extensions.Logging;

public sealed class RequestLoggingHandler : DelegatingHandler
{
    private readonly ILogger<RequestLoggingHandler> _logger;

    public RequestLoggingHandler(ILogger<RequestLoggingHandler> logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        _logger.LogInformation(
            "HTTP {Method} {Uri}",
            request.Method,
            request.RequestUri);

        var stopwatch = Stopwatch.StartNew();
        HttpResponseMessage response;

        try
        {
            response = await base.SendAsync(request, cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "HTTP {Method} {Uri} failed", request.Method, request.RequestUri);
            throw;
        }

        _logger.LogInformation(
            "HTTP {Method} {Uri} responded {StatusCode} in {ElapsedMs}ms",
            request.Method,
            request.RequestUri,
            (int)response.StatusCode,
            stopwatch.ElapsedMilliseconds);

        return response;
    }
}

Register the handler in your DI setup:

builder.Services.AddTransient<RequestLoggingHandler>();
builder.Services
    .AddHttpClient("MyClient")
    .AddHttpMessageHandler<RequestLoggingHandler>();

The DelegatingHandler approach gives you full, type-safe access to the request and response objects, is easy to unit-test in isolation, and composes naturally with other handlers in the pipeline.

Capturing Request/Response Details: URL, Method, Status, Duration

Whether you're using a DelegatingHandler or the built-in ILogger integration, the most useful fields to capture for httpclient logging in .NET are:

  • Method -- GET, POST, etc.
  • URL -- the full request URI (sanitized if needed)
  • Status code -- the HTTP response status
  • Duration -- elapsed milliseconds from send to response headers received
  • Correlation ID -- a trace or request ID to tie the call to a parent operation

Here is a minimal but complete example using Stopwatch inside a DelegatingHandler to capture all of these:

using System.Diagnostics;
using Microsoft.Extensions.Logging;

public sealed class RequestDetailsDelegatingHandler(
    ILogger<RequestDetailsDelegatingHandler> logger)
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var stopwatch = Stopwatch.StartNew();
        HttpResponseMessage? response = null;

        try
        {
            response = await base.SendAsync(request, cancellationToken);
            return response;
        }
        catch (Exception ex)
        {
            logger.LogError(
                ex,
                "HTTP {Method} {Url} failed after {ElapsedMs}ms",
                request.Method,
                SanitizeUri(request.RequestUri),
                stopwatch.ElapsedMilliseconds);
            throw;
        }
        finally
        {
            if (response is not null)
            {
                logger.LogInformation(
                    "HTTP {Method} {Url} responded {StatusCode} in {ElapsedMs}ms",
                    request.Method,
                    SanitizeUri(request.RequestUri),
                    (int)response.StatusCode,
                    stopwatch.ElapsedMilliseconds);
            }
        }
    }

    // Strip query string parameters that might contain sensitive data
    private static string? SanitizeUri(Uri? uri) =>
        uri is null ? null : $"{uri.Scheme}://{uri.Host}{uri.AbsolutePath}";
}

Notice that SanitizeUri strips the query string. This is intentional -- query parameters often contain API keys, tokens, or personal data that you should not write to logs by default.

Redacting Sensitive Headers (Authorization, Cookies) in Logs

One of the most common security mistakes in httpclient logging is accidentally writing Authorization header values or cookies to your log sink. In a structured log system like Seq or Application Insights, those logs are queryable -- meaning a compromised log viewer gives an attacker every token that has ever been sent.

The safe pattern is to allow-list which headers you will log, rather than trying to block-list the sensitive ones:

using Microsoft.Extensions.Logging;

public sealed class SafeHeaderLoggingHandler(
    ILogger<SafeHeaderLoggingHandler> logger)
    : DelegatingHandler
{
    // Only log these headers -- everything else is silently dropped
    private static readonly HashSet<string> AllowedRequestHeaders =
        new(StringComparer.OrdinalIgnoreCase)
        {
            "Content-Type",
            "Accept",
            "User-Agent",
            "X-Correlation-ID",
            "traceparent"
        };

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (logger.IsEnabled(LogLevel.Debug))
        {
            var safeHeaders = request.Headers
                .Where(h => AllowedRequestHeaders.Contains(h.Key))
                .Select(h => $"{h.Key}={string.Join(",", h.Value)}");

            logger.LogDebug(
                "Request to {Url} with allowed headers: {Headers}",
                request.RequestUri,
                string.Join("; ", safeHeaders));
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

An allow-list approach means that if a new sensitive header is introduced (say, an internal X-User-Token), it stays out of your logs automatically. A block-list requires you to remember to update it every time.

To make the difference concrete, here is what the same outgoing request looks like in each case. Without the allow-list handler, a Debug log entry might look like:

Request to https://api.example.com/data with headers: Authorization=Bearer eyJhbGciOiJSUzI1NiJ9...long-token-here, Content-Type=application/json, X-Correlation-ID=abc-123

With the SafeHeaderLoggingHandler applying the allow-list, the same request logs as:

Request to https://api.example.com/data with allowed headers: Content-Type=application/json; X-Correlation-ID=abc-123

The token never appears. Anyone with read access to your log sink -- whether that is a developer, a support engineer, or an attacker who gains access to your logging infrastructure -- cannot harvest bearer tokens from the log stream. This is not theoretical hardening. Leaked tokens in logs are a real and common source of credential exposure in production environments.

If you are building services that handle JWT authentication, the Authentication and Authorization in ASP.NET Core Web API guide covers the patterns for safely handling tokens that flow through your service boundaries.

AddHttpClient + AddHttpMessageHandler for a Logging DelegatingHandler

The DelegatingHandler is the recommended pattern for custom httpclient logging in .NET because it composes cleanly with IHttpClientFactory and participates in the same handler pipeline as resilience policies. Once you have written a DelegatingHandler, registering it with IHttpClientFactory is straightforward. The key is to register the handler itself as a transient service, then attach it to your named or typed client:

// Program.cs
builder.Services.AddTransient<RequestDetailsDelegatingHandler>();
builder.Services.AddTransient<SafeHeaderLoggingHandler>();

builder.Services.AddHttpClient<WeatherServiceClient>(client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<SafeHeaderLoggingHandler>()
.AddHttpMessageHandler<RequestDetailsDelegatingHandler>();

Handlers are executed in the order they are registered, forming a pipeline. The first handler added wraps the outermost layer -- so SafeHeaderLoggingHandler runs before RequestDetailsDelegatingHandler. Think of it like ASP.NET Core middleware -- the outermost handler sees the request first and the response last.

The typed client itself is registered as a transient service by AddHttpClient<T>. Your WeatherServiceClient can inject the HttpClient directly via constructor injection:

public sealed class WeatherServiceClient(HttpClient httpClient)
{
    public async Task<WeatherForecast?> GetForecastAsync(
        string city,
        CancellationToken cancellationToken = default)
    {
        var response = await httpClient.GetAsync(
            $"/weather/{Uri.EscapeDataString(city)}",
            cancellationToken);

        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<WeatherForecast>(cancellationToken);
    }
}

OpenTelemetry with OpenTelemetry.Instrumentation.Http -- Tracing HttpClient in .NET 10

When httpclient logging in .NET is not enough and you need full distributed traces, OpenTelemetry is the industry-standard choice. OpenTelemetry is the industry standard for distributed tracing, and the OpenTelemetry.Instrumentation.Http package provides automatic span creation for every outbound HttpClient request in .NET 10. No manual instrumentation needed.

Install the required packages:

<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.*" />

Then configure OpenTelemetry in Program.cs:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .ConfigureResource(resource => resource
            .AddService("WeatherService", serviceVersion: "1.0.0"))
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation(options =>
        {
            // Filter out health-check pings and noisy internal URLs
            options.FilterHttpRequestMessage = request =>
                request.RequestUri?.Host != "localhost" &&
                request.RequestUri?.AbsolutePath.Contains("/health") != true;

            // Sanitize the URL used as span name (strip query string)
            options.EnrichWithHttpRequestMessage = (activity, request) =>
            {
                activity.SetTag("http.url.path", request.RequestUri?.AbsolutePath);
            };
        }));

AddHttpClientInstrumentation hooks into the same low-level DiagnosticListener infrastructure that powers the built-in ILogger categories, but wraps those events in a proper OpenTelemetry Activity with standardized semantic conventions. Each outbound HTTP call becomes a child span of whatever trace is currently active.

Distributed Tracing: W3C TraceContext Propagation with HttpClient

Distributed tracing only works when trace context flows across service boundaries. The W3C TraceContext standard defines how to do this -- a traceparent header carries the trace ID and parent span ID from service to service.

In .NET 10, IHttpClientFactory + OpenTelemetry handles W3C propagation automatically when you call AddHttpClientInstrumentation. Every outbound HttpClient request gets a traceparent header injected with the current trace context.

To verify this is working, you can inspect the headers in your DelegatingHandler:

public sealed class TraceContextVerificationHandler(
    ILogger<TraceContextVerificationHandler> logger)
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (request.Headers.TryGetValues("traceparent", out var values))
        {
            logger.LogDebug(
                "Outbound request carries traceparent: {TraceParent}",
                values.FirstOrDefault());
        }
        else
        {
            logger.LogWarning(
                "Outbound request to {Url} is missing traceparent header",
                request.RequestUri);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

If the downstream service also uses OpenTelemetry, it reads the traceparent header and creates its spans as children of the same trace. This gives you a complete picture -- one trace ID spans every hop, from the browser request, through your API, into every downstream service.

For complete API setup context, ASP.NET Core Web API in .NET: The Complete Guide is a good companion for understanding how these pieces fit into a full service.

HttpClient Metrics in .NET 10 (System.Net.Http Meter)

Beyond httpclient logging in .NET, metrics give you a statistical view of your outbound HTTP traffic. The System.Net.Http meter -- introduced in .NET 8 and carried forward in .NET 9 and 10 -- exposes a rich set of built-in metrics based on the OpenTelemetry semantic conventions for HTTP client metrics. You do not need to write any instrumentation code to get them -- they are emitted automatically when the meter is observed.

The key instruments are:

Instrument Type Description
http.client.request.duration Histogram Duration of HTTP client requests (seconds)
http.client.active_requests UpDownCounter Number of active outbound requests
http.client.request.time_in_queue Histogram Time a request waits in the connection pool queue
http.client.open_connections UpDownCounter Active TCP connections in the connection pool
http.client.connection.duration Histogram Lifetime duration of HTTP connections

To expose these metrics via OpenTelemetry, add AddHttpClientInstrumentation to the metrics pipeline. AddRuntimeInstrumentation and AddPrometheusExporter are optional -- they require additional packages if used:

<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.*" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.*" />
builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .ConfigureResource(resource => resource
            .AddService("WeatherService"))
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddRuntimeInstrumentation()
        .AddPrometheusExporter());  // Or OTLP, see next section

With AddPrometheusExporter, .NET 10 adds a /metrics endpoint to your application that Prometheus can scrape. The http.client.request.duration histogram is particularly useful for SLO dashboards -- you can track P99 latency per downstream service, alert when it crosses a threshold, and correlate spikes with httpclient logging data in Jaeger or Zipkin.

Exporting to Jaeger, Zipkin, or Azure Monitor

Once you have OpenTelemetry traces and metrics configured, the final step is choosing an exporter. .NET 10 supports all major backends through separate exporter packages.

OTLP is the standard transport for OpenTelemetry and works with Jaeger (version 1.35+), Grafana Tempo, Honeycomb, Lightstep, and many others:

<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.*" />
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddHttpClientInstrumentation()
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri("http://otel-collector:4317");
        }))
    .WithMetrics(metrics => metrics
        .AddHttpClientInstrumentation()
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri("http://otel-collector:4317");
        }));

Zipkin

<PackageReference Include="OpenTelemetry.Exporter.Zipkin" Version="1.*" />
.WithTracing(tracing => tracing
    .AddHttpClientInstrumentation()
    .AddZipkinExporter(options =>
    {
        options.Endpoint = new Uri("http://zipkin:9411/api/v2/spans");
    }))

Azure Monitor (Application Insights)

If you are deploying to Azure, the Azure.Monitor.OpenTelemetry.AspNetCore package replaces the older Application Insights SDK and provides full OTLP-compatible telemetry:

<PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.*" />
builder.Services.AddOpenTelemetry()
    .UseAzureMonitor(options =>
    {
        options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
    });

UseAzureMonitor automatically configures instrumentation for ASP.NET Core, HttpClient, SQL, and more -- without requiring you to call AddHttpClientInstrumentation separately. For services already deployed to Azure, this is the fastest path to full httpclient observability in .NET 10. The Deploying ASP.NET Core Web API to Azure and Docker guide covers deployment patterns that pair well with Azure Monitor.

Complete Example: Full Observability Stack (ILogger + OTel Traces + Metrics)

Here is a complete Program.cs setup that combines every layer covered in this guide -- built-in ILogger filtering, a custom logging DelegatingHandler, OpenTelemetry traces with W3C propagation, and metrics exported via OTLP:

using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using OpenTelemetry.Metrics;

var builder = WebApplication.CreateBuilder(args);

// ── Logging ──────────────────────────────────────────────────────────────────
builder.Logging.AddJsonConsole();
// Suppress noisy default HttpClient logs; our DelegatingHandler handles the details
builder.Logging.AddFilter("System.Net.Http.HttpClient", LogLevel.Warning);

// ── HttpClient + DelegatingHandlers ──────────────────────────────────────────
builder.Services.AddTransient<SafeHeaderLoggingHandler>();
builder.Services.AddTransient<RequestDetailsDelegatingHandler>();

builder.Services.AddHttpClient<WeatherServiceClient>(client =>
{
    client.BaseAddress = new Uri(
        builder.Configuration["WeatherApi:BaseUrl"]
            ?? throw new InvalidOperationException("WeatherApi:BaseUrl not configured"));
    client.Timeout = TimeSpan.FromSeconds(30);
})
.AddHttpMessageHandler<SafeHeaderLoggingHandler>()
.AddHttpMessageHandler<RequestDetailsDelegatingHandler>();

// ── OpenTelemetry ─────────────────────────────────────────────────────────────
var otlpEndpoint = builder.Configuration["Otlp:Endpoint"] ?? "http://localhost:4317";

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService(
            serviceName: "WeatherService",
            serviceVersion: builder.Configuration["Service:Version"] ?? "1.0.0"))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation(options =>
        {
            options.RecordException = true;
        })
        .AddHttpClientInstrumentation(options =>
        {
            // Skip health check endpoints to reduce trace noise
            options.FilterHttpRequestMessage = req =>
                req.RequestUri?.AbsolutePath.StartsWith("/health") != true;
        })
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri(otlpEndpoint);
        }))
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddRuntimeInstrumentation()
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri(otlpEndpoint);
        }));

// ── App pipeline ──────────────────────────────────────────────────────────────
builder.Services.AddControllers();

var app = builder.Build();

// Map /metrics for Prometheus scraping if OTLP is not available
// app.MapPrometheusScrapingEndpoint();

app.MapControllers();
app.MapHealthChecks("/health");

app.Run();

This setup gives you structured JSON logs from your DelegatingHandler, distributed traces visible in Jaeger or Grafana Tempo, and histogram metrics for request duration and connection pool health.

For deeper coverage of handling failures gracefully in the API layer, see Error Handling in ASP.NET Core Web API: Problem Details and Global Handlers -- the error handling patterns there compose well with the observability pipeline built here.


Frequently Asked Questions

Does IHttpClientFactory log request bodies by default?

No. The built-in httpclient logging in .NET only logs request/response metadata -- method, URL, status code, and duration. Request and response bodies are never logged by default because they can be arbitrarily large and often contain sensitive data. If you need body logging for debugging, implement it explicitly in a DelegatingHandler behind a feature flag that is always disabled in production.

How do I enable httpclient logging only in development?

HttpClient logging configuration is environment-specific by design. Use environment-specific appsettings.json files -- create an appsettings.Development.json with verbose log levels under System.Net.Http.HttpClient, and keep appsettings.Production.json at Warning or above. The ILogger infrastructure in .NET 10 merges these files automatically based on ASPNETCORE_ENVIRONMENT.

What is the difference between DiagnosticListener and OpenTelemetry for httpclient observability?

DiagnosticListener is a low-level .NET runtime mechanism that fires key-value events for HTTP requests. OpenTelemetry builds on top of it -- AddHttpClientInstrumentation is itself a DiagnosticListener subscriber. Do not subscribe to DiagnosticListener directly for application-level logging. The event payloads are wrapped in private internal structs, so a type check like value.Value is HttpRequestMessage always evaluates to false at runtime -- the code compiles cleanly and silently does nothing. Use a DelegatingHandler for custom request/response logging, and use OpenTelemetry for distributed tracing and structured spans with standard semantic conventions.

Does W3C TraceContext propagation work without OpenTelemetry?

Partially -- and more than you might expect. Since .NET 6, HttpClient automatically injects a traceparent header when there is a current Activity, via the built-in SocketsHttpHandler and the default DistributedContextPropagator. AddHttpClientInstrumentation adds richer behavior on top: baggage propagation, sampling-aware span creation, and standardized semantic convention tags. If basic W3C trace context propagation is all you need, it works out of the box in .NET 6+ without OpenTelemetry.

How do I redact query string parameters that contain API keys?

The safest approach is to strip the entire query string from log entries, as shown in the SanitizeUri method in the DelegatingHandler example above. If you need some query parameters for debugging but not others, parse the query string with QueryHelpers.ParseQuery, remove the sensitive keys by name, and reconstruct the safe portion for logging.

Can I log HttpClient calls in a Minimal API project the same way?

Yes. HttpClient logging in .NET works identically in Minimal API and controller-based projects. IHttpClientFactory, DelegatingHandler, and OpenTelemetry are all wired up in Program.cs regardless of your API surface style, and the handler pipeline is completely independent of how you expose endpoints.

How do I correlate httpclient log entries with an incoming HTTP request?

Correlating httpclient logging output with an incoming request is what makes distributed systems debuggable. When a request comes into your ASP.NET Core service, AddAspNetCoreInstrumentation creates an Activity span. Any HttpClient calls made within that request handler are automatically children of that span when AddHttpClientInstrumentation is active. In structured logs, you can achieve the same correlation by extracting Activity.Current?.TraceId in your DelegatingHandler and including it as a log field.


Wrapping Up

HttpClient logging in .NET 10 is not a single switch -- it is a layered system. Built-in ILogger integration via IHttpClientFactory gets you basic visibility with zero code. DelegatingHandler gives you full control over what gets logged and how sensitive data is handled. OpenTelemetry elevates that into distributed traces and metrics that work across every service in your system.

The stack described in this guide -- filtered ILogger output, header-safe DelegatingHandler, OTel tracing with W3C propagation, and System.Net.Http metrics -- covers the vast majority of real-world httpclient observability needs. Start with the parts that solve your immediate problems, and add the rest as your system grows.

For the full picture of working with HttpClient in .NET 10, the HttpClient in C#: The Complete Guide hub covers everything from configuration to resilience patterns.

HttpClient in C#: The Complete Guide for .NET Developers

The definitive guide to HttpClient in C# and .NET 10 -- covering correct usage patterns, IHttpClientFactory, DNS pitfalls, resilience, streaming, HTTP/3, testing, and observability. No other article you'll need.

Logging in .NET: The Complete Developer's Guide

Master logging in .NET with ILogger, structured logging, log levels, Serilog, and OpenTelemetry. Complete guide for .NET 9 and .NET 10 developers.

Mock HttpClient C#: DelegatingHandler, Mock Handlers, and Integration Testing

Mock HttpClient C# in .NET 10: custom HttpMessageHandler stubs, DelegatingHandler interceptors, IHttpClientFactory wiring, and WebApplicationFactory tests.

An error has occurred. This application may no longer respond until reloaded. Reload