Developers must implement an efficient logging system to maintain and troubleshoot applications effectively. One popular logging library that provides powerful logging capabilities is Serilog. In this article, we will talk about how to log class and method names using Serilog. This technique can significantly enhance the granularity of our logs, making it easier to trace issues and debug our code. So, let’s get started and explore this technique in detail.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Endpoint Setup in Minimal API

In our implementation, we will use the default WebApi template. By default, the template utilizes minimal API and writes the endpoint code in the Program class.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

However, we will refactor the code by moving the endpoint to its class:

public class WeatherForecastEndpoint
{
    public IResult GetWeatherForecast()
    {
        var summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot",
            "Sweltering", "Scorching"
        };

        var forecast = Enumerable.Range(1, 5).Select(index =>
                new WeatherForecast
                (
                    DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                    Random.Shared.Next(-20, 55),
                    summaries[Random.Shared.Next(summaries.Length)]
                ))
            .ToArray();

        return TypedResults.Ok(forecast);
    }
}

We create a new class, WeatherForecastEndpoint, containing the code that fetches the weather forecast. To ensure we return a valid HttpResponse for this API endpoint, we use IResult as the return type for the GetWeatherForecast() method.

Let’s now map this endpoint:

app.MapGet("/weatherforecast", new WeatherForecastEndpoint().GetWeatherForecast)
    .WithName("GetWeatherForecast")
    .WithOpenApi();

To make our API endpoint invocable, we incorporate the MapGet() extension method on IEndpointRouteBuilder, which calls our GetWeatherForecast() method as a delegate through the handler parameter. Once we have set up our endpoint class, we will move on to integrating the Serilog library into our project. 

Integrating Serilog in Minimal API Project

Firstly, let’s install Serilog via the NuGet package manager using dotnet CLI:

dotnet add package Serilog.AspNetCore

After installing the package, the next step is configuring Serilog in our project:

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, configuration) =>
{
    configuration
        .MinimumLevel.Debug()
        .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
        .Enrich.FromLogContext()
        .WriteTo.Console(
            outputTemplate:
            "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} {Message} " + 
            "(at {ClassName} class in {MethodName} method){NewLine}{Exception}"
        );
});

To configure Serilog, we must use the UseSerilog() method with an action delegate as a parameter. This allows us to specify important properties, such as the MinimumLevel, and override default log levels. Moreover, we can set the log entries to be written to the console with a specific output template. Even though we have configured the template to display the ClassName and MethodName, we are not there yet. So, let’s next explore how we can accomplish this using C# features such as extension methods, generics, and Caller attributes.

Log Class and Method Names With a Custom Serilog Logger Extension Method

We will be writing the extension method on Serilog’s ILogger interface (don’t get confused with the name, as it is similar to ILogger available in Microsoft.Extensions.Logging namespace).

Let’s create our extension method:

using ILogger = Serilog.ILogger;

public static class LoggerExtensions
{
    public static ILogger WithClassAndMethodNames<T>(this ILogger logger, 
        [CallerMemberName] string memberName = "")
    {
        var className = typeof(T).Name;
        
        return logger.ForContext("ClassName", className).ForContext("MethodName", memberName);
    }
}

Here, we create a generic method, WithClassAndMethodNames(), with a type parameter T, extending the Serilog ILogger instance. Utilizing the [CallerMemberName] attribute on the memberName parameter allows the method to automatically infer the calling method’s name if not explicitly provided. Within the method, we derive the class name by invoking typeof(T).Name, where T represents the generic type. Finally, we employ the ForContext() method on the logger to append contextual information to the log dynamically. This includes adding the class name with the ClassName key and the method name with the MethodName.

Now that we have the extension method in place, we can incorporate it into the GetWeatherForecast() method of our WeatherForecastEndpoint class:

public IResult GetWeatherForecast()
{
    _logger.WithClassAndMethodNames<WeatherForecastEndpoint>().Information("Get WeatherForecast called");
    
   // code removed for brevity
}

At the beginning of the method, we utilize our WithClassAndMethodNames() extension method to generate an information log message. After that, we can proceed to invoke the API by running the project and observing the logging entry on the console:

2024-01-30 17:51:08.196 +05:30 [INF] Api.WeatherForecastEndpoint Get WeatherForecast called (at WeatherForecastEndpoint class in GetWeatherForecast method)

The log entries are currently displaying as per our desired output format. However, we can further optimize our logging process by implementing structured logging in JSON format using Serilog. Although we can use the default JSON formatter, we need to maintain more granular control of our custom and other default properties that we display in the log entry.

Hence, we need to create a custom JSON formatter and integrate it into our Serilog configuration. Let’s explore how we can achieve this.

Log Class and Method Names With a Custom JSON Formatter

Let’s now look at putting the structured logging in place.

Firstly, we need to create a custom JSON formatter:

public class CustomJsonFormatter : ITextFormatter
{
    private readonly JsonSerializerOptions _options;

    public CustomJsonFormatter()
    {
        _options = new JsonSerializerOptions
        {
            Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
        };
    }

    public void Format(LogEvent logEvent, TextWriter output)
    {
        var logObject = new
        {
            Timestamp = logEvent.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz"),
            Level = logEvent.Level.ToString(),
            SourceContext = GetPropertyValue(logEvent, "SourceContext"),
            Message = logEvent.RenderMessage(),
            ClassName = GetPropertyValue(logEvent, "ClassName"),
            MethodName = GetPropertyValue(logEvent, "MethodName")
        };

        output.WriteLine(JsonSerializer.Serialize(logObject, _options));
    }

    private string GetPropertyValue(LogEvent logEvent, string propertyName)
    {
        return (logEvent.Properties.ContainsKey(propertyName) ? 
            ((ScalarValue)logEvent.Properties[propertyName]).Value?.ToString() : null)!;
    }
}

Here, we create a class, CustomJsonFormatter, implementing the ITextFormatter interface, for Serilog to enhance the log event output in a JSON format. We initialize the formatter by constructing an instance of JsonSerializerOptions in the constructor. Note that we set the Encoder property to System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping for a cleaner, more readable output.

Then, in the Format() method, we actively structure a log object using relevant properties from the LogEvent instance, including timestamp, log level, source context, message, class name, and method name. The timestamp is formatted for clarity, and the log message is rendered using the RenderMessage() method. We leverage the GetPropertyValue() method to retrieve specific property values from the log event actively. Subsequently, we use the configured JSON serializer options to serialize the log object and the resulting JSON string is actively written to the output stream.

With the custom JSON formatter completed, it’s time to integrate it into the Serilog configuration seamlessly:

builder.Host.UseSerilog((context, configuration) =>
{
    configuration
        .MinimumLevel.Debug()
        .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
        .Enrich.FromLogContext()
        .WriteTo.Console(new CustomJsonFormatter());
});

To format the JSON output, we use our CustomJsonFormatter class and pass it to the WriteTo.Console() method. Let’s now review the JSON log entry:

{
  "Timestamp": "2024-01-30 18:22:01.255 +05:30",
  "Level": "Information",
  "SourceContext": "Api.WeatherForecastEndpoint",
  "Message": "Get WeatherForecast called",
  "ClassName": "WeatherForecastEndpoint",
  "MethodName": "GetWeatherForecast"
}

This looks much better, and it also includes our custom properties of  ClassName and MethodName.

Conclusion

By incorporating class and method names into our logs using Serilog, we can enhance our application’s logging capabilities simply yet effectively. This technique streamlines the debugging process, reduces the time spent identifying issues, and ultimately improves the overall maintainability of our .NET applications. We can actively identify and solve problems using these features and ensure our application runs smoothly. Please note that this technique won’t work where we need to generate the log entries via source generators as it relies on the ILogger of Microsoft.Extensions.Logging library.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!