👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Introduction to Logging in .NET - From Basics to Best Practices

Introduction to Logging in .NET - From Basics to Best Practices

Author - Abdul Rahman (Bhai)

Logging

1 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

What we gonna do?

Every production application eventually faces the dreaded question: "What went wrong?" Without proper logging, you're flying blind. Logging in .NET is your application's flight recorder - capturing critical events, errors, and diagnostic information that help you understand what's happening inside your running systems.

In this comprehensive guide, we'll explore the modern .NET logging infrastructure from the ground up. You'll learn how to implement structured logging using the ILogger<T> interface, understand log levels and categories, configure multiple log providers (destinations), and apply best practices that will save you countless debugging hours.

Whether you're building APIs, web applications, microservices, or background workers, logging is a fundamental cross-cutting concern that deserves your attention from day one. Let's dive in and master the art of effective logging in .NET.

Why we gonna do?

The Hidden Cost of Poor Logging

Imagine deploying your application to production, and users start reporting intermittent failures. Without proper logging, you're left guessing - checking code, reviewing recent changes, trying to reproduce the issue. Hours or days pass before you identify the root cause. The business loses revenue, customers lose trust, and your team loses sleep.

Now consider an alternative scenario: Your monitoring dashboard alerts you to increased error rates. You quickly query your centralized logs, filter by error level, and within minutes identify the exact request that failed, the exception thrown, the user affected, and the conditions that triggered it. You deploy a fix before most users even notice the problem.

This is the power of effective logging. Here's why it matters:

Debugging and Troubleshooting

Production issues are inevitable. Structured logs act as breadcrumbs leading you directly to the problem. When exceptions occur, you need context - what was the user doing? What data was being processed? What state was the system in? Logs provide this crucial context.

Monitoring and Observability

Logs aren't just for when things go wrong. They help you understand normal application behavior, track performance patterns, identify bottlenecks, and detect anomalies before they become critical issues. Combined with metrics and traces, logs form the foundation of observability.

Audit Trails and Compliance

Many industries require audit trails of user actions, data modifications, and system access. Logging provides an immutable record of who did what and when - essential for compliance with regulations like GDPR, HIPAA, SOX, and PCI-DSS.

Performance Analysis

Strategic logging at key points in your application helps identify slow operations, resource-intensive processes, and opportunities for optimization. Timing logs can reveal that a database query takes 2 seconds when it should take 50ms.

Understanding User Behavior

Application logs reveal how users actually interact with your system - not how you think they do. This insight drives product decisions and helps prioritize features based on real usage patterns.

The Problem with Ad-Hoc Logging

Without a consistent logging strategy, developers scatter Console.WriteLine or Debug.WriteLine statements throughout the code. This creates several problems:

  • No standardization: Each developer logs differently, making logs hard to search and analyze
  • No filtering: Everything is logged at the same level, creating noise
  • No destinations: Logs disappear after the console closes or get lost in container orchestrators
  • Poor performance: String concatenation and formatting happen even when logs aren't needed
  • No structure: Free-form text messages are hard to query programmatically

The Microsoft.Extensions.Logging framework solves all these problems with a unified, performant, extensible logging infrastructure that works across all .NET application types.

How we gonna do?

Understanding the .NET Logging Architecture

The .NET logging infrastructure consists of four key components:

  • ILogger<T>: The interface you use to write log messages from your code
  • Log Levels: The severity classification system (Trace, Debug, Information, Warning, Error, Critical)
  • Log Categories: Logical groupings that help organize and filter logs
  • Log Providers: The destinations where logs are written (console, files, databases, external services)

Let's explore each of these in detail with practical examples.

Your First Log Entry - Hello World

Start with a simple console application to understand the basics. Create a new .NET console app and add the necessary packages:

dotnet new console -n LoggingDemo
cd LoggingDemo
dotnet add package Microsoft.Extensions.Logging
dotnet add package Microsoft.Extensions.Logging.Console
dotnet add package Microsoft.Extensions.DependencyInjection

Here's your first logging example:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

// Configure dependency injection and logging
var services = new ServiceCollection();

services.AddLogging(builder =>
{
    builder.AddConsole();  // Add console as a log provider
    builder.SetMinimumLevel(LogLevel.Information);
});

var serviceProvider = services.BuildServiceProvider();

// Get a logger instance
var logger = serviceProvider
    .GetRequiredService<ILogger<Program>>();

// Write your first log message
logger.LogInformation("Hello, World! Logging is working.");

// Output:
// info: Program[0]
//       Hello, World! Logging is working.

When you run this, you'll see more than just your message. The output includes:

  • Log Level: "info" indicates this is an Information-level log
  • Category: "Program" - derived from the generic type parameter ILogger<Program>
  • Event ID: [0] - an optional numeric identifier for the log event
  • Message: Your actual log message

Understanding Log Levels - The Severity Hierarchy

Log levels represent the severity or importance of a log message. .NET provides six standard levels, ordered from most to least verbose:

// 0 - Trace: Extremely detailed diagnostic information
logger.LogTrace("Entering method ProcessOrder with orderId: {OrderId}", 
    orderId);

// 1 - Debug: Information useful during development and debugging
logger.LogDebug("Cache hit for key: {CacheKey}. Retrieved in {Duration}ms", 
    cacheKey, duration);

// 2 - Information: General application flow tracking
logger.LogInformation("User {UserId} successfully logged in from {IpAddress}", 
    userId, ipAddress);

// 3 - Warning: Unexpected situations that don't stop the application
logger.LogWarning("API rate limit at 80% for user {UserId}. Current: {Count}/{Limit}", 
    userId, currentCount, rateLimit);

// 4 - Error: Failures and exceptions that need attention
logger.LogError(exception, 
    "Failed to process payment for order {OrderId}. Reason: {Reason}", 
    orderId, exception.Message);

// 5 - Critical: Catastrophic failures requiring immediate action
logger.LogCritical("Database connection pool exhausted. Application cannot continue.");

// 6 - None: Not used for writing logs; used to disable logging

Here's a practical decision tree for choosing log levels:

┌─────────────────────────────────────────────────────────────┐
│                    Log Level Decision Tree                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Is the application crashing or in unrecoverable state?     │
│  ├─ YES → CRITICAL                                          │
│  └─ NO ↓                                                    │
│                                                             │
│  Did an operation fail with an exception?                   │
│  ├─ YES → ERROR                                             │
│  └─ NO ↓                                                    │
│                                                             │
│  Is something unusual but the app continues normally?       │
│  ├─ YES → WARNING                                           │
│  └─ NO ↓                                                    │
│                                                             │
│  Is this tracking normal business flow?                     │
│  ├─ YES → INFORMATION                                       │
│  └─ NO ↓                                                    │
│                                                             │
│  Is this technical detail useful for debugging?             │
│  ├─ YES → DEBUG                                             │
│  └─ NO ↓                                                    │
│                                                             │
│  Is this extremely detailed runtime information?            │
│  └─ YES → TRACE                                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Important Log Level Guidelines

  • Trace and Debug: Disabled by default in production. May contain sensitive data. Never enable in production unless actively debugging.
  • Information: Safe for production. Tracks business events and application flow. Should be meaningful and searchable.
  • Warning: Indicates potential problems. Monitor these - too many warnings often predict upcoming errors.
  • Error: Requires investigation. Set up alerts for error spikes.
  • Critical: Wake someone up. These require immediate action.

Log Categories - Organizing Your Logs

Log categories help you organize and filter logs from different parts of your application. The category is typically the fully qualified type name of the class doing the logging.

// Using ILogger<T> automatically sets the category to the full type name
public class OrderService
{
    private readonly ILogger<OrderService> _logger;

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

    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        // Category will be "MyApp.Services.OrderService"
        _logger.LogInformation(
            "Creating order for customer {CustomerId} with {ItemCount} items", 
            request.CustomerId, 
            request.Items.Count);

        // ... order creation logic ...
    }
}

// You can also use ILoggerFactory for custom categories
public class PaymentProcessor
{
    private readonly ILogger _logger;

    public PaymentProcessor(ILoggerFactory loggerFactory)
    {
        // Custom category name
        _logger = loggerFactory.CreateLogger("Payments.Processing");
    }
}

Categories enable powerful filtering scenarios:

// In appsettings.json - configure different levels for different categories
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",                    // Reduce Microsoft framework logs
      "Microsoft.AspNetCore": "Warning",         // Reduce ASP.NET Core logs
      "Microsoft.EntityFrameworkCore": "Information", // Show EF Core queries
      "MyApp.Services": "Debug",                 // Verbose logging for your services
      "MyApp.Services.PaymentService": "Trace"   // Maximum detail for payments
    }
  }
}

Structured Logging - Beyond Simple Strings

One of the most powerful features of .NET logging is structured logging. Instead of concatenating strings, you pass parameters that are preserved as structured data.

// ❌ BAD: String concatenation - not searchable, poor performance
logger.LogInformation("Order " + orderId + " for customer " + customerId + 
    " total: $" + total);

// ❌ BAD: String interpolation - also not structured
logger.LogInformation($"Order {orderId} for customer {customerId} total: ${total}");

// ✅ GOOD: Structured logging with named placeholders
logger.LogInformation(
    "Order {OrderId} for customer {CustomerId} total: ${Total}", 
    orderId, 
    customerId, 
    total);

// The placeholders become searchable properties in log aggregation systems:
// {
//   "timestamp": "2026-04-05T10:30:00Z",
//   "level": "Information",
//   "category": "OrderService",
//   "message": "Order 12345 for customer C-789 total: $299.99",
//   "properties": {
//     "OrderId": "12345",
//     "CustomerId": "C-789",
//     "Total": 299.99
//   }
// }

Structured logging enables powerful queries in log management systems:

  • Find all orders for a specific customer: CustomerId = "C-789"
  • Find large orders: Total > 1000
  • Analyze order patterns: GROUP BY CustomerId

Log Providers - Where Logs Go

Log providers (also called sinks or destinations) determine where your logs are written. Out of the box, .NET includes several providers:

  • Console: Writes to console output (stdout/stderr)
  • Debug: Writes to debug output (Visual Studio Output window)
  • EventSource: Writes to Windows Event Tracing for Windows (ETW)
  • EventLog: Writes to Windows Event Log (Windows only)
  • ApplicationInsights: Sends telemetry to Azure Application Insights

You can configure multiple providers simultaneously - logs will be written to all of them:

// In Program.cs for minimal API or web app
var builder = WebApplication.CreateBuilder(args);

builder.Logging.ClearProviders();  // Remove default providers
builder.Logging.AddConsole();       // Add console provider
builder.Logging.AddDebug();         // Add debug provider
builder.Logging.AddEventLog();      // Add Windows Event Log (Windows only)

// Configure minimum levels per provider
builder.Logging.AddConsole(options =>
{
    options.LogToStandardErrorThreshold = LogLevel.Error;
});

var app = builder.Build();

For production applications, you'll typically use third-party providers that offer advanced features:

Popular Third-Party Logging Providers

Provider Type Best For
Serilog Framework + Sinks Structured logging, file rotation, multiple destinations
NLog Framework + Targets Flexible configuration, extensive target support
Seq Centralized Server Development team log aggregation and search
Elasticsearch Centralized Server Large-scale log aggregation (ELK stack)
Datadog Cloud SaaS Enterprise monitoring and observability
Splunk Cloud/On-Prem Enterprise log management and analytics

Example: Adding Serilog for File and Console Logging

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
using Serilog;

// Configure Serilog before building the host
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Console(
        outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
    .WriteTo.File(
        path: "logs/app-.txt",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 7,
        fileSizeLimitBytes: 10_000_000,
        rollOnFileSizeLimit: true)
    .CreateLogger();

try
{
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Host.UseSerilog();  // Replace default logging with Serilog
    
    var app = builder.Build();
    
    app.MapGet("/", (ILogger<Program> logger) =>
    {
        logger.LogInformation("Root endpoint accessed at {AccessTime}", 
            DateTime.UtcNow);
        return "Hello World";
    });
    
    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}

Best Practices for Effective Logging

1. Use Dependency Injection
// ✅ GOOD: Use constructor injection
public class OrderService
{
    private readonly ILogger<OrderService> _logger;
    
    public OrderService(ILogger<OrderService> logger)
    {
        _logger = logger;
    }
}

// ❌ BAD: Don't create loggers directly
public class BadService
{
    public void DoWork()
    {
        var loggerFactory = LoggerFactory.Create(builder => 
            builder.AddConsole());
        var logger = loggerFactory.CreateLogger<BadService>();
    }
}
2. Log Exceptions Properly
try
{
    await ProcessPaymentAsync(orderId);
}
catch (PaymentException ex)
{
    // ✅ GOOD: Pass exception as first parameter, include context
    _logger.LogError(ex, 
        "Payment processing failed for order {OrderId}. Gateway: {Gateway}", 
        orderId, 
        ex.GatewayName);
    
    // ❌ BAD: Don't convert exception to string
    // _logger.LogError("Error: " + ex.ToString());
}
3. Use Log Scopes for Context
public async Task ProcessOrderAsync(string orderId, string customerId)
{
    // Create a scope that adds context to all logs within
    using (_logger.BeginScope(new Dictionary<string, object>
    {
        ["OrderId"] = orderId,
        ["CustomerId"] = customerId
    }))
    {
        _logger.LogInformation("Starting order processing");
        
        await ValidateOrderAsync();  // Logs will include OrderId and CustomerId
        await ChargePaymentAsync();  // Logs will include OrderId and CustomerId
        await FulfillOrderAsync();   // Logs will include OrderId and CustomerId
        
        _logger.LogInformation("Order processing completed");
    }
}
4. Avoid Logging Sensitive Data
// ❌ BAD: Never log passwords, tokens, or PII without redaction
_logger.LogInformation("User {Email} logged in with password {Password}", 
    email, password);

// ❌ BAD: Don't log credit card numbers
_logger.LogDebug("Processing card {CardNumber}", cardNumber);

// ✅ GOOD: Log safely masked identifiers
_logger.LogInformation("User {Email} logged in successfully", 
    MaskEmail(email));

_logger.LogDebug("Processing card ending in {Last4Digits}", 
    cardNumber.Substring(cardNumber.Length - 4));
5. Use High-Performance Logging for Hot Paths
// For frequently called methods, use LoggerMessage source generators
public partial class OrderService
{
    private readonly ILogger<OrderService> _logger;

    [LoggerMessage(
        EventId = 1001,
        Level = LogLevel.Information,
        Message = "Processing order {OrderId} for customer {CustomerId}")]
    private partial void LogOrderProcessing(string orderId, string customerId);

    public async Task ProcessAsync(string orderId, string customerId)
    {
        // This generates compile-time optimized logging code
        LogOrderProcessing(orderId, customerId);
        
        // ... processing logic ...
    }
}

Complete Working Example - ASP.NET Core API

Here's a complete example showing logging in a real ASP.NET Core API with best practices:

// Program.cs
using Serilog;

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.File("logs/api-.log", rollingInterval: RollingInterval.Day)
    .CreateLogger();

try
{
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Host.UseSerilog();
    builder.Services.AddControllers();
    builder.Services.AddScoped<IOrderService, OrderService>();
    
    var app = builder.Build();
    
    app.UseSerilogRequestLogging();  // Log HTTP requests
    app.MapControllers();
    
    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application failed to start");
}
finally
{
    Log.CloseAndFlush();
}

// Controllers/OrdersController.cs
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;
    private readonly ILogger<OrdersController> _logger;

    public OrdersController(
        IOrderService orderService,
        ILogger<OrdersController> logger)
    {
        _orderService = orderService;
        _logger = logger;
    }

    [HttpPost]
    public async Task<IActionResult> CreateOrder(
        [FromBody] CreateOrderRequest request)
    {
        using (_logger.BeginScope(new Dictionary<string, object>
        {
            ["CustomerId"] = request.CustomerId,
            ["ItemCount"] = request.Items.Count
        }))
        {
            _logger.LogInformation(
                "Received order creation request");

            try
            {
                var order = await _orderService.CreateAsync(request);
                
                _logger.LogInformation(
                    "Order {OrderId} created successfully with total {Total:C}", 
                    order.Id, 
                    order.Total);
                
                return CreatedAtAction(
                    nameof(GetOrder), 
                    new { id = order.Id }, 
                    order);
            }
            catch (ValidationException ex)
            {
                _logger.LogWarning(ex, 
                    "Order validation failed: {Errors}", 
                    string.Join(", ", ex.Errors));
                return BadRequest(ex.Errors);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                    "Unexpected error creating order");
                return StatusCode(500, "An error occurred");
            }
        }
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetOrder(string id)
    {
        _logger.LogDebug("Fetching order {OrderId}", id);
        
        var order = await _orderService.GetByIdAsync(id);
        
        if (order == null)
        {
            _logger.LogWarning("Order {OrderId} not found", id);
            return NotFound();
        }
        
        return Ok(order);
    }
}

Troubleshooting Common Logging Issues

Logs Not Appearing?
  • Check minimum log level configuration - your logs might be filtered out
  • Verify providers are registered in dependency injection
  • Ensure the category matches your filter configuration
  • For file providers, check file permissions and disk space
Too Many Logs?
  • Increase minimum log levels (Information instead of Debug)
  • Use category filters to reduce framework logs: "Microsoft": "Warning"
  • Disable request logging for health check endpoints
  • Use log level overrides for specific namespaces
Performance Impact?
  • Use structured logging (parameters) instead of string interpolation
  • Leverage LoggerMessage source generators for hot paths
  • Avoid logging in tight loops unless absolutely necessary
  • Use async file writers to prevent I/O blocking
  • Configure appropriate log retention and rotation

Summary

Logging is a fundamental pillar of production-ready .NET applications. In this comprehensive guide, we've covered:

  • The ILogger<T> interface as the standard way to write logs in .NET
  • Six log levels (Trace, Debug, Information, Warning, Error, Critical) and when to use each
  • Log categories for organizing and filtering logs by component or namespace
  • Structured logging with named parameters for searchable, queryable log data
  • Log providers for writing logs to multiple destinations (console, files, databases, cloud services)
  • Best practices including dependency injection, exception logging, scopes, and performance optimization

Start with the built-in console provider for development, then add Serilog or NLog for production applications with file rotation and centralized log aggregation. Configure appropriate log levels for different environments - verbose in development, selective in production.

Remember: Good logging is an investment, not an expense. The time you spend implementing proper logging today will save you countless hours of debugging, troubleshooting, and production incident response tomorrow. Your future self (and your team) will thank you.

In upcoming articles, we'll dive deeper into advanced logging topics including Serilog enrichers, structured logging at scale, distributed tracing correlation, and integrating logs with modern observability platforms. Stay tuned!

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • Logging
  • Logging
  • ILogger
  • Log Levels
  • Log Categories
  • Log Providers
  • Serilog
  • Structured Logging
  • .NET