👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Getting Started with Serilog in .NET - From Installation to Production-Ready Logging

Getting Started with Serilog in .NET - From Installation to Production-Ready Logging

Author - Abdul Rahman (Bhai)

Logging

5 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?

If there is one logging library every .NET developer should know, it is Serilog. With nearly a billion NuGet downloads and widespread adoption across companies of every size, Serilog has earned its reputation as the gold standard for application logging in .NET. In this article, you will learn what Serilog is, how to integrate it into both console and ASP.NET Core applications, and how to use its most powerful features to build production-ready observability.

Serilog is an open-source, structured logging library for .NET. It was the library that popularised message templates and semantic logging — concepts that Microsoft later adopted into its own ILogger abstraction. Even today, Serilog's feature set goes further than the built-in provider: richer structured data handling, a vast ecosystem of sinks, log enrichment, timed operations, and surgical control over sensitive data masking.

By the end of this article you will be able to:

  • Create a Serilog logger and write to multiple sinks simultaneously
  • Replace the built-in Microsoft.Extensions.Logging provider with Serilog in ASP.NET Core
  • Drive all configuration from appsettings.json
  • Enrich logs with machine name, thread ID, environment, and custom properties
  • Handle structured and complex objects with the destructuring operator
  • Time operations and automatically detect slow paths

Why we gonna do?

The built-in ILogger in .NET is a solid abstraction, but its concrete providers have real gaps that hurt production observability. Here is where those gaps show up.

Lost logs in containerised environments

The default console provider writes plain text. In a Kubernetes cluster that recycles pods frequently, plain-text console output disappears the moment the container is replaced. There is no built-in way to flush in-flight buffered logs before the process exits.

Serilog solves this with Log.CloseAndFlushAsync(). Every sink that buffers in memory for performance — such as the Application Insights or Elasticsearch sinks — is guaranteed to drain its buffer before the process closes. No logs are silently swallowed.

Object serialisation in log messages

With the built-in provider, logging a complex object produces its ToString() representation — typically just a type name:


// Built-in logger — unhelpful output
logger.LogInformation("Payment received: {Payment}", payment);
// Output: Payment received: MyApp.Models.Payment
            

Serilog's @ destructuring operator serialises the object into structured key-value pairs automatically, making every property searchable in downstream log stores:


// Serilog — full structured output
logger.Information("Payment received: {@Payment}", payment);
// Output: Payment received: { PaymentId: 1, UserId: "abc", OccurredAt: "2026-01-15T10:00:00Z" }
            

Configuration spread across multiple places

The built-in logging system uses the Logging section of appsettings.json. Serilog replaces it with its own Serilog section that covers minimum levels, sink configuration, enrichers, and output templates — all in one place. Changing log levels at runtime requires touching only one file, not hunting down provider-specific settings scattered across environments.

No easy timing for slow operations

Measuring how long a code path takes requires manually capturing a Stopwatch, writing two log statements, and remembering to log even when an exception is thrown midway. The SerilogTimings package collapses all of that into a single using block and automatically promotes slow operations to warnings.

How we gonna do?

Step 1: Install Serilog

Add the core package and the sinks you need. For most applications, start with the console and file sinks:


dotnet add package Serilog
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
            

Step 2: Create a logger in a console application

Creating a Serilog logger requires no factory or builder ceremony. Configure it with LoggerConfiguration, add sinks, and call CreateLogger():


using Serilog;

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console(theme: Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code)
    .WriteTo.File(
        "logs/app.txt",
        rollingInterval: RollingInterval.Day,
        rollOnFileSizeLimit: true)
    .CreateLogger();

Log.Information("Hello from Serilog!");
Log.Warning("This is a warning");
Log.Error("Something went wrong");

await Log.CloseAndFlushAsync();
            

The Log.Logger static property is the central logger instance. Assigning to it registers your configured logger globally, which Serilog uses internally for buffered sink operations. Always call CloseAndFlushAsync before the process exits so that in-flight buffered messages are not lost.

Serilog's log levels map to the .NET equivalents like so:


// Serilog level  →  Microsoft equivalent
Log.Verbose(...);   // Trace
Log.Debug(...);     // Debug
Log.Information(..);// Information
Log.Warning(...);   // Warning
Log.Error(...);     // Error
Log.Fatal(...);     // Critical
            

Step 3: Understand sinks

A sink is Serilog's term for a log provider — the destination where log entries are written. The ecosystem has hundreds of community-maintained sinks. A few popular ones:

  • Serilog.Sinks.Console — terminal output with optional ANSI colour themes
  • Serilog.Sinks.File — file output with daily or size-based rolling
  • Serilog.Sinks.ApplicationInsights — Azure Application Insights
  • Serilog.Sinks.Elasticsearch — Elasticsearch / OpenSearch
  • Serilog.Sinks.Seq — Seq structured log server
  • Serilog.Sinks.MSSqlServer — SQL Server

Multiple sinks can be chained in a single configuration. Serilog writes to each one independently:


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

Step 4: Integrate Serilog into ASP.NET Core

For ASP.NET Core, install the hosting integration package:


dotnet add package Serilog.AspNetCore
            

Then replace the built-in logging pipeline in Program.cs using UseSerilog(). This single call replaces every provider that was registered by the default host — console, debug, event source, and event log — with Serilog:


using Serilog;

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateLogger();

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();

var app = builder.Build();
// ...
            

After this, every call to the Microsoft.Extensions.Logging.ILogger<T> interface — whether in controllers, minimal API handlers, or injected services — is routed through Serilog. Your existing code does not need to change.

Step 5: Drive configuration from appsettings.json

Hard-coding log levels in Program.cs makes them impossible to change without a redeployment. Serilog can read its entire configuration — levels, sinks, enrichers — from appsettings.json.

Install the configuration package:


dotnet add package Serilog.Settings.Configuration
            

Update Program.cs to load from configuration:


Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(builder.Configuration)
    .CreateLogger();

builder.Host.UseSerilog();
            

Add a Serilog section to appsettings.json (remove the built-in Logging section — it is no longer used):


{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft.AspNetCore": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      { "Name": "Console" }
    ]
  }
}
            

Environment-specific overrides work exactly as you expect via appsettings.Production.json, allowing you to increase verbosity in staging or switch sinks without touching code.

Step 6: Enrich logs with metadata

Enrichers attach additional properties to every log event. Install the standard enricher packages:


dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread
dotnet add package Serilog.Enrichers.Process
            

Register them with .Enrich calls in LoggerConfiguration:


Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(builder.Configuration)
    .Enrich.FromLogContext()
    .Enrich.WithMachineName()
    .Enrich.WithThreadId()
    .Enrich.WithProperty("Application", "MyApp")
    .WriteTo.Console(new JsonFormatter())
    .CreateLogger();
            

With the JsonFormatter, each log line is a JSON object. The output includes the enriched properties alongside the message:


{
  "Timestamp": "2026-06-07T10:23:45.123Z",
  "Level": "Information",
  "MessageTemplate": "User {UserId} logged in",
  "Properties": {
    "UserId": "abc123",
    "MachineName": "web-01",
    "ThreadId": 12,
    "Application": "MyApp"
  }
}
            

These properties are indexed by every sink that supports structured storage, making them instantly filterable in dashboards such as Application Insights, Seq, or Kibana.

Step 7: Handle structured data with the destructuring operator

When you pass an object to a Serilog message template, the default behaviour calls ToString() on it — exactly the same as the built-in logger. Use the @ prefix to instruct Serilog to destructure (serialize) the object instead:


var payment = new Payment
{
    PaymentId = 1,
    UserId = Guid.NewGuid(),
    OccurredAt = DateTime.UtcNow
};

// Plain string representation:
Log.Information("Payment: {Payment}", payment);
// → Payment: MyApp.Models.Payment

// Full structured serialisation:
Log.Information("Payment: {@Payment}", payment);
// → Payment: { PaymentId: 1, UserId: "...", OccurredAt: "2026-06-07T10:00:00Z" }
            

Use $ prefix to force the string representation explicitly, and pass a dictionary directly to get automatic key-value destructuring:


// Force string representation
Log.Information("Payment: {$Payment}", payment);
// → Payment: MyApp.Models.Payment

// Dictionary is auto-destructured
var dict = new Dictionary<string, object>
{
    ["PaymentId"] = 1,
    ["UserId"] = "abc"
};
Log.Information("Payment: {Payment}", dict);
// → Payment: [("PaymentId": 1), ("UserId": "abc")]
            

Step 8: Transform structured data with destructuring policies

Sometimes you want to log only specific properties of an object — for example, you have a Payment type but must never log the full card number. Serilog's Destructure.ByTransforming<T> lets you project to a safe anonymous shape before the log entry is written:


Log.Logger = new LoggerConfiguration()
    .Destructure.ByTransforming<Payment>(
        p => new { p.PaymentId, p.UserId })  // OccurredAt and card number excluded
    .WriteTo.Console()
    .CreateLogger();

Log.Information("Payment: {@Payment}", payment);
// → Payment: { PaymentId: 1, UserId: "abc" }
            

Additional scalar transformations are available: Destructure.ToMaximumStringLength(100) truncates long strings and Destructure.ToMaximumCollectionCount(10) limits large collection output.

Step 9: Manually enrich individual log events

Sometimes you need to attach a property for a limited scope — like the current PaymentId for the duration of a payment processing method. Use LogContext.PushProperty from Serilog.Context:


using Serilog.Context;

public async Task ProcessPaymentAsync(Payment payment)
{
    using (LogContext.PushProperty("PaymentId", payment.PaymentId))
    {
        Log.Information("Received payment from user {UserId}", payment.UserId);
        await ChargeAsync(payment);
        Log.Information("Payment charged successfully");
    }
    // PaymentId property is removed from context here
}
            

Both log statements inside the using block will carry PaymentId as a structured property without it appearing in the message template. This is the structured-logging equivalent of ILogger scopes — and it works with .Enrich.FromLogContext() configured in the logger.

To push multiple properties at once, use the overload that accepts IEnumerable<LogEventProperty>, or nest multiple using blocks.

Step 10: Time operations with SerilogTimings

Install the timings package:


dotnet add package SerilogTimings
            

Wrap any operation in a using block created by Operation.Time to automatically log a completion entry with the elapsed milliseconds:


using SerilogTimings;

public async Task ProcessPaymentAsync(Payment payment)
{
    using var op = Operation.Time(
        "Processing payment {PaymentId}", payment.PaymentId);

    await Task.Delay(50); // simulate work
    Log.Information("Payment processed for user {UserId}", payment.UserId);

    // Completion is logged automatically when the using block exits:
    // Completed "Processing payment 1" in 52.3 ms
}
            

If the operation takes longer than a threshold, promote it to a warning automatically:


using var op = Operation.At(LogEventLevel.Warning).Time(
    "Processing payment {PaymentId}", payment.PaymentId);
            

For operations that can fail, call op.Abandon() to log an abandonment entry at warning level rather than a normal completion:


using var op = Operation.Time(
    "Processing payment {PaymentId}", payment.PaymentId);
try
{
    await ChargeAsync(payment);
    // op completes normally on dispose
}
catch (Exception ex)
{
    op.Abandon();   // logs "Abandoned Processing payment 1 after 15.2 ms"
    Log.Error(ex, "Payment failed");
    throw;
}
            

Step 11: Integrate with Application Insights

Install the Application Insights sink:


dotnet add package Serilog.Sinks.ApplicationInsights
            

Add it alongside the console sink in appsettings.json:


{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft.AspNetCore": "Warning"
      }
    },
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "ApplicationInsights",
        "Args": {
          "connectionString": "<YOUR_AI_CONNECTION_STRING>",
          "telemetryConverter":
            "Serilog.Sinks.ApplicationInsights.TelemetryConverters.TraceTelemetryConverter, Serilog.Sinks.ApplicationInsights"
        }
      }
    ]
  }
}
            

Every log entry now flows to both destinations simultaneously. Enriched properties — machine name, thread ID, application name — appear as Custom Dimensions in Application Insights, making them filterable in Kusto queries and dashboards.

Summary

In this article, we covered Serilog from the ground up — the library that pioneered structured logging in .NET and still leads the ecosystem today. Here is what you should take away:

  • Serilog replaces the built-in console and other providers with a single call to builder.Host.UseSerilog(). Existing ILogger<T> injections continue to work unchanged.
  • Sinks are destinations for log output. You can attach as many as you need — console, file, Application Insights, Elasticsearch — and Serilog writes to all of them.
  • ReadFrom.Configuration() loads the entire Serilog setup from appsettings.json, enabling environment-specific tuning without code changes.
  • Enrichers attach structured properties — machine name, thread ID, application name — to every log entry, making them filterable in downstream stores.
  • The @ destructuring operator serialises complex objects into structured key-value pairs. Destructure.ByTransforming<T> lets you shape that output precisely, excluding sensitive fields.
  • LogContext.PushProperty attaches scoped properties to log events within a using block — the structured equivalent of ILogger scopes.
  • SerilogTimings measures operation duration automatically and promotes slow paths to warnings without any hand-rolled stopwatch code.
  • Always call Log.CloseAndFlushAsync() on shutdown to guarantee buffered log entries are not lost when the process exits.
👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • Logging
  • Logging
  • Serilog
  • Structured Logging
  • Sinks
  • Enrichers
  • Log Context
  • Application Insights
  • Destructuring
  • SerilogTimings
  • .NET