Phil Friedman
Phil Friedman

Reputation: 85

How to change the LogLevel of specific log events in Serilog?

Is there a configurable way to change the log level of specific log events? Maybe with serilog-expressions?

My idea is that we have configuration that lists methods/source contexts and regular expressions for matching transient errors. For exceptions that match, log a warning not an error. Logging an error triggers our triage notifications in Seq. If we get an unexpected exception that does not match, we do want to log an error. We're using Hangfire to retry, so we want to throw the exception. If it's not transient, Hangfire will complain.

Here's a hard coded example of the behavior we'd like:

void CurrentCode() {
    try {
        throw new Exception("Log a warning for this transient exception.");
    } catch (Exception e) {
        var regex = new Regex(@"^Log a warning");
        var logLevel = regex.IsMatch(e.Message) ? LogLevel.Warning : LogLevel.Error;
        logger.Log(logLevel, e, "{Method}() {ExceptionMessage}"
            , "CurrentCode", e.Message);
        throw;
    }
}

Or is there a completely different approach we should consider?

Upvotes: 4

Views: 2677

Answers (1)

C. Augusto Proiete
C. Augusto Proiete

Reputation: 27868

A possible approach with Serilog would be to create a Sink wrapper where you can intercept logs before they reach the real sinks and modify the log event level depending on exceptions that match your criteria.

Here is an example of a sink wrapper that modifies log events based on some logic:

public class LogLevelModifierSink : ILogEventSink, IDisposable
{
    private readonly ILogEventSink _targetSink;

    public LogLevelModifierSink(ILogEventSink targetSink)
    {
        _targetSink = targetSink ?? throw new ArgumentNullException(nameof(targetSink));
    }

    public void Emit(LogEvent logEvent)
    {
        if (LogLevelMustChange(logEvent, out var newLogLevel))
        {
            // Clone the original log event, but with the new log level
            var newLogEvent = new LogEvent(
                logEvent.Timestamp,
                newLogLevel,
                logEvent.Exception,
                logEvent.MessageTemplate,
                logEvent.Properties
                    .Select(kv => new LogEventProperty(kv.Key, kv.Value)));

            _targetSink.Emit(newLogEvent);
        }
        else
        {
            // Pass-through the original event
            _targetSink.Emit(logEvent);
        }
    }

    private bool LogLevelMustChange(LogEvent logEvent, out LogEventLevel newLogLevel)
    {
        if (logEvent.Level == LogEventLevel.Error /*&& some other logic*/)
        {
            newLogLevel = LogEventLevel.Warning;
            return true;
        }
    
        newLogLevel = default;
        return false;
    }

    public void Dispose()
    {
        (_targetSink as IDisposable)?.Dispose();
    }
}

// Extension method to hook the wrapper into the configuration syntax
public static class LoggerSinkConfigurationLogLevelModifierExtensions
{
    public static LoggerConfiguration LogLevelModifier(
        this LoggerSinkConfiguration loggerSinkConfiguration,
        Action<LoggerSinkConfiguration> configure)
    {
        return LoggerSinkConfiguration.Wrap(loggerSinkConfiguration, sink =>
            new LogLevelModifierSink(sink), configure, LevelAlias.Minimum, null);
    }
}

Example usage of the sink:

void Main()
{
    Log.Logger = new Serilog.LoggerConfiguration()
        .MinimumLevel.Verbose()
        .WriteTo.LogLevelModifier(writeTo =>
            writeTo.Console())
        .CreateLogger();

    Log.Error("Hello, {Name}", "Augusto");

    Log.CloseAndFlush();
}

Upvotes: 3

Related Questions