
Understanding .NET Logging Providers - From Console to Custom Implementations
Author - Abdul Rahman (Bhai)
Logging
5 Articles
Table of Contents
What we gonna do?
In .NET logging, a log provider is the destination where your log messages are sent. When you call logger.LogInformation("Request received"), the .NET logging framework routes that message to one or more registered providers. Providers can be anything from the familiar terminal window to a cloud-based analytics service or a custom file sink you build yourself.
Providers fall into two broad categories:
- Ephemeral providers - Log output that disappears when the application stops. The console provider is the classic example: logs appear in the terminal while the app runs, then vanish.
- Persistent providers - Log output that is stored and survives process restarts. File system providers, database providers, and cloud services such as Azure Application Insights all fall into this category.
In this article you will learn which providers ship with .NET out of the box, how to integrate an external provider such as Application Insights, how to control which providers are active per environment, and how to build a minimal custom provider from scratch.
Why we gonna do?
Without a clear understanding of log providers you will encounter several painful problems in production.
Logs disappear after process restart
Relying solely on the console provider means all diagnostic information is lost the moment the application is restarted or crashes. In containerised or serverless deployments where containers are replaced frequently, this can make post-mortem debugging nearly impossible.
Unexpected providers registered implicitly
Adding a NuGet package such as Microsoft.Extensions.Hosting silently registers several providers (console, debug, event log, event source) as indirect dependencies. Developers who are unaware of this end up with duplicate log entries and unexpectedly high verbosity in production.
Sending logs everywhere in every environment
Without environment-aware provider registration you pay the performance cost of pushing logs to expensive cloud services in development, and risk missing logs in production because the only registered provider is the console — which nobody watches.
No control over where logs go
A flat logging strategy where every log goes to one destination makes it hard to route high-volume debug logs to a cheap file sink while sending critical alerts to a monitoring service. Understanding providers gives you the composability to route logs precisely.
How we gonna do?
How log messages reach their destination
Every call to ILogger.Log() goes through the ILoggerFactory, which fans the message out to every registered ILoggerProvider. Each provider then writes independently to its own destination:
ILogger.Log(level, message)
│
▼
ILoggerFactory (applies per-provider / per-category filters)
│
├──► ILoggerProvider 1 (Console) ──► stdout
├──► ILoggerProvider 2 (EventLog) ──► Windows Event Log
├──► ILoggerProvider 3 (App Insights) ──► Azure portal
└──► ILoggerProvider N (Custom) ──► any destination
Understanding this fan-out model is the key to controlling where logs go and avoiding accidental duplicate or missing entries.
Built-in providers in .NET
ASP.NET Core web projects automatically register four logging providers through their implicit NuGet package dependencies:
- Console - writes to stdout / the terminal
- Debug - writes to the attached debugger output window
- EventLog - writes to the Windows Event Log (Windows only)
- EventSource - writes ETW / EventPipe events (requires an active listener such as dotnet-trace or PerfView to capture)
A fifth provider, TraceSource, integrates with System.Diagnostics.TraceSource and requires explicit AddTraceSource() registration — it is not active by default.
In a plain console application the story is different. No implicit web SDK is present, so you must reference the NuGet packages yourself. Adding Microsoft.Extensions.Hosting is the quickest path because it transitively pulls in console, debug, event log, and event source providers. Remove that package and only the console provider remains — unless you add others explicitly.
// Console application — explicit provider setup without the Hosting package
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
var services = new ServiceCollection();
services.AddLogging(logging =>
{
logging.AddConsole(); // only console, nothing implicit
logging.SetMinimumLevel(LogLevel.Information);
});
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Application started");
Adding an external provider — Application Insights in ASP.NET Core
External providers follow a naming convention: Microsoft.Extensions.Logging.{ProviderName}. For Application Insights the package is Microsoft.Extensions.Logging.ApplicationInsights. Install it and call AddApplicationInsights inside builder.Logging:
// Program.cs — Minimal API with Application Insights logging
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddApplicationInsights(configureTelemetryConfiguration: config =>
{
// GetRequiredSection throws at startup if the key is absent — prefer this over the
// indexer (which returns null silently and causes a runtime NullReferenceException).
config.ConnectionString = builder.Configuration
.GetRequiredSection("ApplicationInsights")["ConnectionString"];
},
configureApplicationInsightsLoggerOptions: _ => { });
// Keep the default minimum level sensible — Information floods AI quickly
builder.Logging.SetMinimumLevel(LogLevel.Warning);
var app = builder.Build();
app.MapGet("/", (ILogger<Program> logger) =>
{
logger.LogWarning("Health check endpoint hit");
return Results.Ok("Running");
});
app.Run();
Because the console provider is still registered by default, logs are sent to both the console and Application Insights simultaneously. Application Insights buffers logs and pushes them to Azure in batches rather than making one network request per log entry, so allow a short delay before expecting to see entries in the portal. Once synced, you can query logs, filter by custom dimensions, and correlate them with request telemetry.
Adding Application Insights in a console application
In a console app the hosting infrastructure is absent, so you manage the telemetry channel yourself. Use Microsoft.ApplicationInsights (not the extensions variant) and create an InMemoryChannel to buffer telemetry before the process exits:
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using var channel = new InMemoryChannel();
try
{
var services = new ServiceCollection();
services.AddSingleton<ITelemetryChannel>(channel);
services.Configure<TelemetryConfiguration>(config =>
{
config.TelemetryChannel = channel;
});
services.AddLogging(logging =>
{
logging.AddApplicationInsights(configureTelemetryConfiguration: config =>
{
config.ConnectionString = "<your-connection-string>";
},
configureApplicationInsightsLoggerOptions: _ => { });
});
var serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Console app started");
logger.LogWarning("Processing complete");
}
finally
{
// Guarantee buffered logs are flushed before the process exits
channel.Flush();
await Task.Delay(TimeSpan.FromSeconds(2)); // allow async push to complete
}
The try/finally block guarantees that the channel is flushed even after an unexpected exception, so no buffered logs are silently dropped.
Controlling provider registration per environment
Rather than having every provider active everywhere, clear the default registrations and add back only what is appropriate for each environment:
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
// Development: plain console output is sufficient
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
}
else
{
// Production: send logs to Application Insights only
builder.Logging.ClearProviders();
builder.Logging.AddApplicationInsights(configureTelemetryConfiguration: config =>
{
config.ConnectionString = builder.Configuration
.GetRequiredSection("ApplicationInsights")["ConnectionString"];
},
configureApplicationInsightsLoggerOptions: _ => { });
}
var app = builder.Build();
app.Run();
ClearProviders() removes every previously registered provider — including the four implicit ASP.NET Core ones — giving you a clean slate. This pattern prevents development-only providers (e.g. Debug) from appearing in production binaries and avoids paying the cost of cloud ingestion during local development.
Fine-tuning log levels per provider via appsettings.json
You do not need to touch Program.cs to control what each provider logs. The Logging section of appsettings.json (or appsettings.{Environment}.json) lets you set minimum log levels globally and override them per provider and per category:
{
"Logging": {
"LogLevel": { // applies to ALL providers unless overridden
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
},
"Console": {
"LogLevel": {
"Default": "Warning" // console only shows Warning and above
}
},
"ApplicationInsights": {
"LogLevel": {
"Default": "Error" // send only errors to Application Insights
}
}
}
}
This is the preferred approach for log level management: configuration-based filtering requires no recompile and works seamlessly alongside ClearProviders(). Changes to appsettings.json take effect without redeploying when the file configuration provider is set to reload on change (the default for ASP.NET Core apps).
Building a custom log provider
When no built-in or third-party provider meets your needs, you can implement your own by satisfying two interfaces: ILoggerProvider and ILogger.
using Microsoft.Extensions.Logging;
// --- The provider: responsible for creating logger instances ---
public sealed class CustomLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
=> new CustomLogger(categoryName);
public void Dispose() { } // release resources here if needed
}
// --- The logger: responsible for writing log entries ---
public sealed class CustomLogger : ILogger
{
private readonly string _categoryName;
public CustomLogger(string categoryName)
=> _categoryName = categoryName;
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
=> NullScope.Instance;
public bool IsEnabled(LogLevel logLevel)
=> logLevel != LogLevel.None;
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel)) return;
// Replace this with any destination: file, socket, database, etc.
Console.WriteLine($"[{logLevel}] ({eventId}) {_categoryName}: {formatter(state, exception)}");
}
// Minimal no-op scope used by BeginScope
private sealed class NullScope : IDisposable
{
public static readonly NullScope Instance = new();
private NullScope() { }
public void Dispose() { }
}
}
Register the provider using AddProvider:
// ASP.NET Core Minimal API
builder.Logging.AddProvider(new CustomLoggerProvider());
// OR — generic host / console app
services.AddLogging(logging => logging.AddProvider(new CustomLoggerProvider()));
The real complexity in a custom provider lives in the ILogger.Log implementation: formatting, batching, async I/O, thread safety, and error handling. The provider class itself is intentionally thin — its only job is to manufacture logger instances.
Provider summary at a glance
┌──────────────────────────────┬────────────────────────┬──────────────────────────────┐
│ Provider │ Persistent │ Added by default in │
├──────────────────────────────┼────────────────────────┼──────────────────────────────┤
│ Console │ No (ephemeral) │ ASP.NET Core, Hosting │
│ Debug │ No (ephemeral) │ ASP.NET Core, Hosting │
│ EventLog │ Yes │ ASP.NET Core, Hosting │
│ EventSource │ No (needs listener) │ ASP.NET Core, Hosting │
│ TraceSource │ Varies │ Manual (AddTraceSource()) │
│ Application Insights │ Yes │ Manual installation │
│ Custom ILoggerProvider │ Varies │ Manual registration │
└──────────────────────────────┴────────────────────────┴──────────────────────────────┘
Summary
Log providers are the destinations your .NET logging infrastructure writes messages to. Here is a concise recap of what was covered:
- Providers are either ephemeral (console, debug) or persistent (file, database, cloud services).
- ASP.NET Core and the Microsoft.Extensions.Hosting package register multiple providers implicitly — be aware of what you get for free.
- External providers such as Application Insights are added via their Microsoft.Extensions.Logging.{ProviderName} NuGet package and a single AddApplicationInsights call.
- In console applications, manage the InMemoryChannel lifecycle explicitly and flush it in a finally block to avoid losing buffered logs.
- Use ClearProviders() combined with environment checks to register only the providers appropriate for each deployment environment.
- Fine-tune log levels per provider and category via the Logging section of appsettings.json — no recompile required, and changes take effect at runtime when the file configuration provider reloads on change.
- Build a custom provider by implementing ILoggerProvider and ILogger. The provider creates logger instances; the logger handles the actual write logic.