C# Logging with Microsoft.Extensions.Logging
Logging Overview
Microsoft.Extensions.Logging provides a unified logging abstraction used across .NET applications. It supports structured logging, multiple output providers, and integrates with dependency injection.
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public void ProcessOrder(int orderId)
{
_logger.LogInformation("Processing order {OrderId}", orderId);
try
{
// Process...
_logger.LogInformation("Order {OrderId} completed successfully", orderId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);
throw;
}
}
}
Log Levels
// Levels from least to most severe
_logger.LogTrace("Detailed diagnostic info"); // 0 - Trace
_logger.LogDebug("Debugging information"); // 1 - Debug
_logger.LogInformation("General information"); // 2 - Information
_logger.LogWarning("Something unexpected"); // 3 - Warning
_logger.LogError(ex, "An error occurred"); // 4 - Error
_logger.LogCritical(ex, "System failure"); // 5 - Critical
// Check if level is enabled (avoid expensive operations)
if (_logger.IsEnabled(LogLevel.Debug))
{
var expensiveData = ComputeDebugInfo();
_logger.LogDebug("Debug info: {Data}", expensiveData);
}
Log Level Guidelines
| Level | When to Use |
|---|---|
| Trace | Detailed diagnostic info, method entry/exit |
| Debug | Information useful during development |
| Information | General operational events |
| Warning | Unexpected but handled situations |
| Error | Errors that prevent specific operations |
| Critical | Failures requiring immediate attention |
Structured Logging
Log data as structured properties, not just text.
// BAD: Interpolated strings lose structure
_logger.LogInformation($"User {userId} purchased product {productId}");
// GOOD: Structured with named parameters
_logger.LogInformation(
"User {UserId} purchased product {ProductId}",
userId, productId);
// Properties become searchable in log systems:
// { "UserId": 123, "ProductId": "SKU-456", "Message": "User 123 purchased..." }
// Complex objects
_logger.LogInformation("Order created: {@Order}", order); // @ for destructuring
_logger.LogInformation("Order created: {$Order}", order); // $ for ToString()
Logging Configuration
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"MyApp.Services": "Debug"
},
"Console": {
"LogLevel": {
"Default": "Information"
},
"FormatterName": "json"
},
"Debug": {
"LogLevel": {
"Default": "Debug"
}
}
}
}
Programmatic Configuration
builder.Logging
.ClearProviders()
.AddConsole()
.AddDebug()
.SetMinimumLevel(LogLevel.Debug)
.AddFilter("Microsoft", LogLevel.Warning)
.AddFilter<ConsoleLoggerProvider>("MyApp", LogLevel.Debug);
Log Scopes
Use Scopes for Correlation
Log scopes add context to all log entries within a block—like request IDs, user IDs, or transaction IDs. This makes correlated events searchable across distributed systems.
Add contextual information to a group of log entries.
public async Task ProcessOrderAsync(Order order)
{
using (_logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = order.Id,
["CustomerId"] = order.CustomerId
}))
{
_logger.LogInformation("Starting order processing");
await ValidateOrderAsync(order);
await ChargePaymentAsync(order);
await FulfillOrderAsync(order);
_logger.LogInformation("Order processing complete");
}
// All logs within scope include OrderId and CustomerId
}
// Simpler scope syntax
using (_logger.BeginScope("Processing order {OrderId}", orderId))
{
// Logs include OrderId
}
Enable Scope Logging
{
"Logging": {
"Console": {
"IncludeScopes": true
}
}
}
Built-in Providers
Console
builder.Logging.AddConsole(options =>
{
options.FormatterName = "json"; // or "simple", "systemd"
});
// Custom formatter
builder.Logging.AddConsole(options =>
{
options.FormatterName = "custom";
}).AddConsoleFormatter<CustomFormatter, CustomFormatterOptions>();
Debug
// Outputs to System.Diagnostics.Debug
builder.Logging.AddDebug();
EventSource
// For ETW/EventPipe integration
builder.Logging.AddEventSourceLogger();
EventLog (Windows)
builder.Logging.AddEventLog(settings =>
{
settings.SourceName = "MyApplication";
});
High-Performance Logging
Source-Generated Logging (C# 9+)
Source-generated logging eliminates all reflection, boxing, and string parsing at runtime. For high-throughput applications, this dramatically reduces allocation pressure.
Eliminates boxing and parsing overhead at runtime.
public static partial class LogMessages
{
[LoggerMessage(
EventId = 1000,
Level = LogLevel.Information,
Message = "Processing order {OrderId} for customer {CustomerId}")]
public static partial void OrderProcessing(
ILogger logger,
int orderId,
string customerId);
[LoggerMessage(
EventId = 1001,
Level = LogLevel.Warning,
Message = "Order {OrderId} has {ItemCount} items exceeding limit")]
public static partial void OrderItemsExceeded(
ILogger logger,
int orderId,
int itemCount);
[LoggerMessage(
EventId = 1002,
Level = LogLevel.Error,
Message = "Failed to process order {OrderId}")]
public static partial void OrderFailed(
ILogger logger,
int orderId,
Exception exception);
}
// Usage
LogMessages.OrderProcessing(_logger, order.Id, order.CustomerId);
LogMessages.OrderFailed(_logger, order.Id, ex);
Benefits:
- No boxing of value types
- No string parsing at runtime
- Compile-time validation of parameters
- Better performance when log level is disabled
Avoid Expensive Operations
// BAD: Always evaluates ToString() even if level disabled
_logger.LogDebug("Complex data: " + complexObject.ToString());
// GOOD: Only evaluated if Debug is enabled
_logger.LogDebug("Complex data: {Data}", complexObject);
// BEST: Check level for expensive operations
if (_logger.IsEnabled(LogLevel.Debug))
{
var debugData = GenerateExpensiveDebugData();
_logger.LogDebug("Debug data: {Data}", debugData);
}
Third-Party Providers
Serilog
// Install: Serilog.AspNetCore
builder.Host.UseSerilog((context, config) =>
{
config
.ReadFrom.Configuration(context.Configuration)
.WriteTo.Console()
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
.WriteTo.Seq("http://localhost:5341")
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithThreadId();
});
// appsettings.json for Serilog
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning"
}
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "File",
"Args": {
"path": "logs/log-.txt",
"rollingInterval": "Day"
}
}
]
}
}
NLog
// Install: NLog.Web.AspNetCore
builder.Logging.ClearProviders();
builder.Host.UseNLog();
Correlation and Distributed Tracing
Activity and Trace Context
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public async Task ProcessOrderAsync(Order order)
{
// Activity provides TraceId and SpanId automatically
using var activity = new Activity("ProcessOrder").Start();
_logger.LogInformation("Processing order {OrderId}", order.Id);
// Log includes Activity.Current.TraceId
await _paymentService.ChargeAsync(order);
// Payment service logs will share the same TraceId
}
}
Manual Correlation
public async Task HandleRequestAsync(HttpContext context)
{
var correlationId = context.Request.Headers["X-Correlation-Id"]
.FirstOrDefault() ?? Guid.NewGuid().ToString();
using (_logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = correlationId
}))
{
_logger.LogInformation("Handling request");
// Process request...
}
}
Logging in Different Contexts
Static/Startup Logging
// Before DI is available
var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Debug);
});
var logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("Application starting");
Background Services
public class WorkerService : BackgroundService
{
private readonly ILogger<WorkerService> _logger;
public WorkerService(ILogger<WorkerService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
_logger.LogInformation("Worker started");
while (!ct.IsCancellationRequested)
{
using (_logger.BeginScope("Iteration at {Time}", DateTime.UtcNow))
{
try
{
await DoWorkAsync(ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during work iteration");
}
}
await Task.Delay(TimeSpan.FromMinutes(1), ct);
}
_logger.LogInformation("Worker stopped");
}
}
Testing with Logging
public class OrderServiceTests
{
[Fact]
public void ProcessOrder_LogsInformation()
{
// Arrange
var logger = new FakeLogger<OrderService>();
var service = new OrderService(logger);
// Act
service.ProcessOrder(123);
// Assert
Assert.Contains(logger.LogEntries,
e => e.LogLevel == LogLevel.Information
&& e.Message.Contains("123"));
}
}
// Simple fake logger
public class FakeLogger<T> : ILogger<T>
{
public List<LogEntry> LogEntries { get; } = new();
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception? exception, Func<TState, Exception?, string> formatter)
{
LogEntries.Add(new LogEntry
{
LogLevel = logLevel,
Message = formatter(state, exception),
Exception = exception
});
}
public bool IsEnabled(LogLevel logLevel) => true;
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
=> null;
}
Best Practices
Message Templates
// Use consistent naming
_logger.LogInformation("User {UserId} logged in from {IpAddress}", userId, ip);
// PascalCase for property names
_logger.LogInformation("Processing {ItemCount} items", items.Count);
// Be specific
_logger.LogError(ex, "Failed to save user {UserId} to database {Database}",
userId, databaseName);
What to Log
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
_logger.LogInformation("Creating order for customer {CustomerId}", request.CustomerId);
// Log important business events
var order = new Order(request);
_logger.LogInformation("Order {OrderId} created with {ItemCount} items, total {Total}",
order.Id, order.Items.Count, order.Total);
// Log integration points
_logger.LogDebug("Sending order {OrderId} to fulfillment service", order.Id);
await _fulfillmentService.QueueOrderAsync(order);
// Log completion
_logger.LogInformation("Order {OrderId} queued for fulfillment", order.Id);
return order;
}
What NOT to Log
// DON'T log sensitive data
_logger.LogInformation("User {Email} with password {Password}"); // NO!
_logger.LogInformation("Credit card {CardNumber}"); // NO!
// DON'T log at wrong levels
_logger.LogError("User not found"); // Should be Warning or Info
_logger.LogInformation("Exception: " + ex); // Should be Error with exception
// DON'T log too much in hot paths
foreach (var item in millionsOfItems)
{
_logger.LogDebug("Processing {Item}", item); // Too much logging
}
Version History
| Feature | Version | Significance |
|---|---|---|
| ILogger abstraction | .NET Core 1.0 | Unified logging API |
| Log scopes | .NET Core 1.0 | Contextual grouping |
| Configuration integration | .NET Core 1.0 | appsettings.json support |
| Source generators | .NET 6 | High-performance logging |
| JSON console formatter | .NET 5 | Structured console output |
| W3C trace context | .NET Core 3.0 | Distributed tracing |
Key Takeaways
Use structured logging: Pass parameters separately, don’t interpolate into message strings.
Choose appropriate levels: Information for business events, Warning for handled issues, Error for failures.
Use source-generated logging: For high-performance scenarios, eliminate runtime overhead.
Add context with scopes: Group related logs with correlation IDs and contextual data.
Don’t log sensitive data: Never log passwords, tokens, credit cards, or PII.
Consider log volume: Trace and Debug levels can be extremely verbose. Use IsEnabled checks for expensive operations.
Test your logging: Verify critical events are logged correctly with fake loggers in tests.
Found this guide helpful? Share it with your team:
Share on LinkedIn