If Serilog is your go-to logging library in the .NET ecosystem, have you ever wondered if you're fully exploiting its capabilities? Today, we'll explore some Serilog features that often go unnoticed but are powerful tools when employed correctly. So, brace yourself for a deep dive into the mighty ocean of structured logging!

1. Destructuring

Destructuring in Serilog provides you with control over how complex objects are logged. Using a custom destructuring policy, you can design this to suit your specific requirements.

Consider this code snippet:

public class CustomerDestructuringPolicy : IDestructuringPolicy
{
    public bool TryDestructure(
        object value,
        ILogEventPropertyValueFactory propertyValueFactory,
        out LogEventPropertyValue result)
    {
        if (value is Customer customer)
        {
            var structureProperties = new List<LogEventProperty>
            {
                new LogEventProperty(
                    "Id", new ScalarValue(customer.Id)),
                new LogEventProperty(
                    "Name", new ScalarValue(customer.Name)),
                new LogEventProperty(
                    "Email", new ScalarValue(customer.Email)),
                
                // Replace sensitive data value with a placeholder
                new LogEventProperty(
                    "Password", new ScalarValue("Hidden")),
                new LogEventProperty(
                    "CreditCardNumber", new ScalarValue("Hidden"))
            };

            result = new StructureValue(structureProperties);
            return true;
        }

        result = null;
        return false;
    }
}

// Registering our destructuring policy
var logger = new LoggerConfiguration()
    .Destructure.With(new CustomerDestructuringPolicy())
    .WriteTo.Console()
    .CreateLogger();

// Showcasing the actual use case
var customer = new Customer
{
    Id = 1,
    Name = "John Doe",
    Email = "john.doe@example.com",
    Password = "password",
    CreditCardNumber = "1234567812345678"
};

logger.Information("Created a new customer: {@Customer}", customer);

2. Contextual Logging

Contextual logging is a valuable tool when you aim to track and incorporate specific data across a particular scope. Here's how you might use it:

// Define the middleware that will enrich the log context
// with request data over the course of request's lifecycle.
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    public RequestLoggingMiddleware(
        RequestDelegate next,
        ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        var requestId = Guid.NewGuid().ToString();
        var requestLogger = _logger.ForContext("RequestId", requestId);
       // Add additional request data.
    }
}

// Don't forget to register the middleware.
app.UseMiddleware<RequestLoggingMiddleware>();

3. Conditional Sink Enabling

This feature becomes useful when you want to control where your logs are sent based on certain conditions, such as the environment your application is running in, the severity of the log event, or any other runtime data.

Log.Logger = new LoggerConfiguration()
    .WriteTo.Conditional(
        e => environment.IsDevelopment(),
        wt => wt.Console())
    .WriteTo.Conditional(
        e => environment.IsStaging(),
        wt => wt.File("logs\\myapp.txt"))
    .WriteTo.Conditional(
        e => environment.IsProduction(),
        wt => wt.Seq("http://localhost:5341"))
    .CreateLogger();

4. Custom Enricher

While Serilog has a lot of related enricher libraries, there might be times when you need to build one of your own. Here's an example of a custom enricher that tracks the elapsed time since the application started.

// Custom enricher that tracks how much time has
// elapsed since the application started.
public class ExecutionTimeEnricher : ILogEventEnricher
{
    private readonly Stopwatch _stopwatch;

    public ExecutionTimeEnricher()
    {
        _stopwatch = Stopwatch.StartNew();
    }

    public void Enrich(
        LogEvent logEvent,
        ILogEventPropertyFactory propertyFactory)
    {
        logEvent.AddPropertyIfAbsent(
            new LogEventProperty("ExecutionTime",
            new ScalarValue(_stopwatch.Elapsed)));
    }
}

// Register your custom enricher
Log.Logger = new LoggerConfiguration()
    .Enrich.With(new ExecutionTimeEnricher())
    .WriteTo.Console()
    .CreateLogger();

5. Logging Level Switch

This feature enables you to control a specific segment's log level dynamically. It proves especially beneficial when you aim to alter the log level live without necessitating an application restart.

// In the sample below, when the configuration changes,
// the minimum log level of LoggingLevelSwitch is updated,
// which effectively changes the log level of Serilog

var levelSwitch = new LoggingLevelSwitch();

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

var optionsMonitor = configuration
    .GetSection("Logging")
    .Get<LoggingOptions>();

optionsMonitor.OnChange(options =>
{
    levelSwitch.MinimumLevel = options.LogLevel;
    Log.Information("Log level switched to {LogLevel}", options.LogLevel);
});

This ends our exploration of some of the lesser-known but potent features of Serilog. Remember, a tool's true power lies in the user's hands. Hence, you can enhance your logging and debugging capabilities by understanding and employing these features.