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.
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.
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.