When it comes to logging in .NET applications, Serilog is a favorite among developers for its structured logging capabilities and seamless integration with the .NET ecosystem. In this blog post, we’ll enhance a sample console application (which we created here) to include robust logging using Serilog.

Why Use Serilog?
#

Logging is critical in modern applications for debugging, monitoring, and understanding system behavior. While .NET’s built-in logging framework is excellent, combining it with Serilog takes your logs to the next level by offering:

  • Structured Logs: Logs are emitted in a format like JSON, enabling easier querying and analysis.
  • Rich Ecosystem: Choose from dozens of sinks to send logs to files, databases, cloud platforms (there is no better and easier way to log to Azure Application Insights), or even specialized tools like Seq or Elasticsearch.
  • Dynamic Contextual Data: Attach custom properties to your logs, improving traceability.

Setting Up the Application
#

Let’s start with a sample console application using .NET 9. First, create a new console application:

1
2
dotnet new console -n SerilogSampleApp
cd SerilogSampleApp

Add the necessary NuGet packages:

1
2
3
4
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Serilog.Extensions.Hosting
dotnet add package Serilog.Sinks.Console

Configuring Serilog
#

In your Program.cs, configure Serilog as early as possible to capture all logs, even during application startup:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using Serilog;
using Serilog.Context;

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext() // Adds contextual data to every log entry
    .WriteTo.Console()       // Outputs logs to the console
    .CreateLogger();
try
{
    Log.Information("Starting the application...");
    var builder = Host.CreateApplicationBuilder(args);

    var logger = new LoggerConfiguration()
        .ReadFrom.Configuration(builder.Configuration)
        .CreateLogger();
    
    builder.Logging.AddSerilog(logger);       // Adds Serilog as the logging provider

    // Register a hosted service
    builder.Services.AddHostedService<PrintTimeService>();

    var app = builder.Build();
    await app.RunAsync();
    return 0;
}
catch (Exception ex)
{
    // Log critical errors during startup
    Log.Fatal(ex, "An unhandled exception occurred during application startup.");
    return 1;
}
finally
{
    // Flush any remaining log entries
    await Log.CloseAndFlushAsync();
}

This setup ensures Serilog is the primary logging provider, and logs are flushed before the application exits.


Adding Context to Your Logs
#

One of Serilog’s most powerful features is log enrichment. You can dynamically add properties to logs, making them more informative:

1
2
3
4
using (LogContext.PushProperty("ApplicationName", "SerilogSampleApp"))
{
    Log.Information("This log includes additional context!");
}

This is particularly useful in scenarios like distributed systems, where you might want to include properties such as RequestId or UserId.


Configuring Serilog with appsettings.json
#

To make the logging configuration environment-specific, move the Serilog configuration to appsettings.json:

  1. Create a appsettings.json file in the project root:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
   "Serilog":{
      "Using":[
         "Serilog.Sinks.Console"
      ],
      "MinimumLevel":{
         "Default":"Information",
         "Override":{
            "Microsoft":"Warning",
            "System":"Warning"
         }
      },
      "Enrich":[
         "FromLogContext"
      ],
      "WriteTo":[
         {
            "Name":"Console"
         }
      ]
   }
}

Install the Serilog.Settings.Configuration NuGet Package and update the Serilog configuration in Program.cs:

1
2
3
Log.Logger = new LoggerConfiguration()     
  .ReadFrom.Configuration(builder.Configuration) // Loads configuration from appsettings.json     
  .CreateLogger();

Now you can have different configurations for Debug and Release environments by creating appsettings.Development.json and appsettings.Production.json.


Testing the Setup
#

Add a sample hosted service to test logging:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class PrintTimeService : IHostedService, IDisposable
{
    private readonly ILogger<PrintTimeService> _logger;
    private Timer? _timer;

    public PrintTimeService(ILogger<PrintTimeService> logger)
    {
        _logger = logger;
    }
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting PrintTimeService.");
        _timer = new Timer(PrintTime, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
        return Task.CompletedTask;
    }
    private void PrintTime(object? state)
    {
        _logger.LogInformation("Current time: {Time}", DateTime.Now);
    }
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Stopping PrintTimeService.");
        _timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }
    public void Dispose()
    {
        _timer?.Dispose();
    }
}

Run the application, and you’ll see Serilog’s structured logs in your console.

Structured Logging
Structured Logging


Key Takeaways
#

  • Exception Handling: Ensure exceptions are logged properly using Log.Fatal().
  • Contextual Logging: Use LogContext to enrich your logs with dynamic properties.
  • Environment-Specific Configurations: Leverage appsettings.json for flexible setups.
  • Integration: Serilog integrates seamlessly with .NET’s logging framework.

Logging is the backbone of any reliable application. By combining .NET’s built-in logging framework with the power of Serilog, you can build a logging system that’s maintainable, scalable, and insightful. Effective logging not only helps you pinpoint issues quickly but also provides valuable insights into your application’s behavior over time. Think of it as leaving breadcrumbs for your future self — making troubleshooting smoother and resolutions faster. Trust me, future you will thank you!

Ready to take your logging to the next level? Try it out in your next project, and remember to log your progress along the way!

P.S. I’ve also leveraged this logging mechanism in my Blazor blog posts. Curious to see it in action? Get started here:
An Adventure in Modern Web Hosting