Enhance logging in .NET Core Web API

Enhance logging in .NET Core Web API writing logs with Async Console. Learn structured logging benefits for better app insights

Hey, listen… In modern web development, effective logging is crucial for monitoring, diagnosing, and troubleshooting applications. Serilog is a powerful logging library for .NET applications, providing a range of features that make logging structured and readable. In this post, I’ll show how to enhance logging in a .NET Core Web API using Serilog. By the end of this post, you’ll have a robust logging setup that provides clear insights into your application’s behavior, making it easier to maintain and debug.

What is Serilog?

Serilog is a structured logging library for .NET applications. It allows developers to capture detailed log data in a structured format, which can be easily filtered and queried. Unlike traditional logging frameworks primarily focusing on plain text logs, Serilog supports outputting logs in formats like JSON, thus enabling more sophisticated log analysis and visualization.

It is a highly extensible tool and supports a wide range of “sinks”, which can be translated as “destinations for log data”. These sinks include file-based logs, console output, databases, and specialized log management systems like Datalust Seq. By providing rich context and flexible log management options, Serilog helps developers diagnose issues more efficiently and maintain better oversight of their applications’ behavior. You can find more about Serilog at serilog.net

What is “Structured Logging”?

It is a technique that writes logs in a structured format, such as JSON, rather than plain text. This allows more advanced and efficient querying and analysis of log data. Structured logging captures the context and structure of the data, which can be used to extract powerful insights and diagnostics.

Why use Structured Logging?

  • Enhanced Search: you can search and filter log data more effectively using properties or values without relying on text matching.
  • Better Visualization: tools like Seq and ELK (Elasticsearch, Logstash, Kibana) could be used to visualize structured logs more effectively. Such tools provide dashboards and graphs that help with monitoring and analysis.
  • Improved Context: you can add log context information, such as user IDs, and other metadata making it easier to trace issues.
  • Automation and Alerts: you can set up automated log processing and alerting based on patterns or thresholds, improving your system’s reliability.

Install Serilog Packages

You need to add the necessary Serilog packages to your project. Run the following commands in the Package Manager Console:

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Enrichers.GlobalLogContext
dotnet add package Serilog.Expressions
dotnet add package Serilog.Sinks.Async
dotnet add package Serilog.Sinks.Console

Serilog.AspNetCore is the main package. You will need the others only if you wish to explore Serilog’s secret powers.

Coding

I’ll use the same code used in this post about boilerplate code and in this one about versioning. You may also like these posts, take a look!

If you just want to use the simple log, jump straight to Barebone Logging.

Commons Boot changes

Go to the class Boot.cs and change the method Run. Add another parameter called customServices, like the code below.

public static void Run(string[] args, string serviceName, Action<IServiceCollection> customServices = null)

At the same method add a singleton of the the type IConfiguration for the builder.Configuration and call the customServices action as shown.

builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
customServices?.Invoke(builder.Services);

Here is the code with the modifications in bold:

//other usgings
using Commons.Extensions;

namespace Commons;

public static void Run(string[] args, string serviceName, Action<IServiceCollection> customServices = null)
{
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
    customServices?.Invoke(builder.Services);
    
    builder.Services.AddControllers();
    builder.Services.RegisterSwaggerServices(serviceName);

    var app = builder.Build();

    app.ConfigureSwaggerPipeline(serviceName);

    app.UseHttpsRedirection();
    app.UseAuthorization();
    app.MapControllers();
    app.Run();
}

Now let’s create an extension class to handle the logger service configuration.

Create a class called LogginigExtensions.cs inside the folder Extensions and put the following code.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Extensions.Hosting;
using Serilog.Extensions.Logging;

namespace Commons.Extensions;
public static class LogginigExtensions {
    public static IServiceCollection RegisterLogingServices(this IServiceCollection serviceCollection) {

        var cfgs = (IConfiguration)serviceCollection.FirstOrDefault(x =>
                typeof(IConfiguration) == x.ServiceType 
                && x.ImplementationInstance != null
            )?.ImplementationInstance;

        if (cfgs?.GetSection("Serilog").Exists() == true)
        {
            var slogBuilder = new LoggerConfiguration();
            slogBuilder.ReadFrom.Configuration(cfgs);
        
            slogBuilder.Enrich.FromLogContext();
            slogBuilder.Enrich.FromGlobalLogContext();

            var sloger = new SerilogLoggerFactory(slogBuilder.CreateLogger());
            serviceCollection.AddSingleton<ILoggerFactory>((Func<IServiceProvider, ILoggerFactory>) (services => (ILoggerFactory) sloger));
            var implementationInstance = new DiagnosticContext(null);
            serviceCollection.AddSingleton<DiagnosticContext>(implementationInstance);
            serviceCollection.AddSingleton<IDiagnosticContext>((IDiagnosticContext) implementationInstance);
        }
        else
        {
            serviceCollection.AddSingleton(LoggerFactory.Create(b =>
            {
                b.AddConsole(options =>
                {
                    options.FormatterName = "Simple";
                    options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
                    options.IncludeScopes = false;
                    options.DisableColors = false;
                });
            }));
        }

        return serviceCollection;
    }
}

This method will configure the Serilog service only if the Serilog configuration section is added to the appsettings.json, the default Microsoft logging mechanism will be used otherwise.

Simple API1 changes

Now let’s change the Program.cs inside the project SimpleApi1 as follows in bold text.

using Commons;
using Commons.Extensions;

Boot.Run(args, "SimpleApi1", svcs => {
    svcs.RegisterLogingServices();
});

The Run method allows us to configure customized services as we added the action to do so. To configure the logging services is a matter of calling RegisterLogingServices. Beautiful, isn’t it?

barebone Logging

If you are planning to use a fast and simple login coding, here is the code you want.

If you are using the code from the example, jump to Changing Settings.

In .NET Core, the Program.cs file is the entry point of your application. There you will set up and configure Serilog.

using Serilog;

var builder = WebApplication.CreateBuilder(args);

// Configure Serilog
Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(builder.Configuration)
    .Enrich.FromLogContext()
    .CreateLogger();

builder.Host.UseSerilog();

var app = builder.Build();

//app.UseSerilogRequestLogging();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();
app.UseAuthorization();

app.MapControllers();

app.Run();

changing settings

To manage Serilog settings, you can add a Serilog section in your appsettings.json file as shown below.

{
  //other configurations
  
  "Serilog": {
    "Using": [
      "Serilog.Sinks.Async"
    ],
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft": "Information"
      }
    },
    "WriteTo": [
      {
        "Name": "Async",
        "Args": {
          "configure": [
            {
              "Name": "Console",
              "Args": {
                "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
                "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
              }
            }
          ]
        }
      }
    ]
  }
  //end of serilog config
}

Serilog uses Sinks to write logs at different locations. Each Sink has its configurations and options. For this post, I’m using only the Console Sink through the Async Sink but you could add as many Sinks as you wish. Take a look at https://github.com/serilog/serilog/wiki/Provided-Sinks.

Using logging

Now that you configured Serilog, you can use it in your controllers or any other part of your application.

Here’s an example of a simple controller using Serilog:

using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;

namespace SimpleApi1.Controllers;

[ApiController]
[Route("api/v{version:apiVersion}/[controller]/[action]")]
public class MyApiController : ControllerBase
{
    private readonly ILogger<MyApiController> _logger;

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

    [ApiVersion("1.0")]
    [HttpGet]
    public async Task<IActionResult> Get()
    {
        _logger.LogInformation("log from Get");
        return Ok("SimpleApi1 - GET");
    }
    
    [HttpGet, ApiVersion("2.0")]
    public async Task<IActionResult> GetV2()
    {
        _logger.LogInformation("log from GetV2");
        return Ok("SimpleApi1 V2 - GET");
    }
}

Log Example

I configured the Console Sink in the appsettings.json, so we can see the log of the application on the console screen.

[16:13:20 INF] log from Get
[16:13:27 INF] log from GetV2

Conclusion

By following these steps, you’ve configured Serilog in a .NET Core Web API with support for the Async Console sink. Serilog’s flexibility and powerful features make it an excellent choice for logging in .NET applications!

Get the sources at: https://github.com/raffsalvetti/EnhanceLogging

See ya!