在 Serilog 中更改/覆盖日志事件级别

Change / override log event level in Serilog

有没有办法动态更改某些事件的日志级别? (可能通过命名空间或谓词)

我正在寻找类似 .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 的东西,但我真正想做的是将来自 Microsoft 命名空间的 Information 事件的级别更改为 Verbose .更重要的事件应保持原样。

编辑:

Enricher 无法更改日志事件级别。

这是可能的,但并非完全简单,所以请系好安全带!

1。创建接收器包装器

您需要围绕目标接收器创建包装器,而不是增强器。包装器将从日志记录管道接收事件,(相当便宜)创建具有相同属性的新事件,并将它们转发到实际的接收器:

class LevelBoostingWrapper : ILogEventSink, IDisposable
{
    readonly ILogEventSink _wrappedSink;

    public LevelBoostingWrapper(ILogEventSink wrappedSink)
    {
        _wrappedSink = wrappedSink;
    }

    public void Emit(LogEvent logEvent)
    {
        if (logEvent.Level == LogEventLevel.Warning)
        {
            var boosted = new LogEvent(
                logEvent.Timestamp,
                LogEventLevel.Error, // <- the boost
                logEvent.Exception,
                logEvent.MessageTemplate,
                logEvent.Properties
                    .Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)));

            _wrappedSink.Emit(boosted);
        }
        else
        {
            _wrappedSink.Emit(logEvent);
        }
    }

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

当然,决定修改哪些事件的实际标准取决于您。

2。将包装器挂接到配置语法中

这个小扩展使设置包装器变得更加愉快:

static class LoggerSinkConfigurationExtensions
{
    public static LoggerConfiguration Boosted(
        this LoggerSinkConfiguration lsc,
        Action<LoggerSinkConfiguration> writeTo)
    {
        return LoggerSinkConfiguration.Wrap(
            lsc,
            wrapped => new LevelBoostingWrapper(wrapped),
            writeTo);
    }
}

3。将包装器添加到配置中

最后,在记录器配置中,应用包装器:

Log.Logger = new LoggerConfiguration()
    .WriteTo.Boosted(wt => wt.Console())
    .CreateLogger();

Log.Information("This will be unchanged");
Log.Warning("This will be boosted to Error");
       
Log.CloseAndFlush();

这个解决方案只是基于上面尼古拉斯回答的一个更具体的例子。

在我的 .NET Core 3.1 应用程序中,我收到了与 Tamas 相同的更改日志级别的要求。我还需要创建审核日志。

更具体地说,我想保留在 https://github.com/aspnet/HttpClientFactory/blob/master/src/Microsoft.Extensions.Http/Logging/LoggingHttpMessageHandler.cs 中定义的默认 http 客户端日志记录 但将其作为 DEBUG 而不是 INFO。

我尝试了中定义的 Serilog UseSerilogRequestLogging https://nblumhardt.com/2019/10/serilog-in-aspnetcore-3/ https://nblumhardt.com/2019/10/serilog-mvc-logging/ 但没有成功。

我已经创建了一个 Enricher,它获取日志级别并从中创建另一个 属性。我可以扩展这个 enricher 来报告一个较低的级别作为 httpClient 的真实日志级别,但它没有解决日志记录问题,只是以不同的方式显示它。

上面Nicholas定义的sink是正确的做法。

棘手的部分是在水槽之后得到浓缩器。 因此,为此,我们可以创建一个子记录器,如 https://github.com/serilog/serilog/wiki/Configuration-Basics

底部所述

显示具体示例的一些代码

public sealed class HttpClientLogLevelWrapper : ILogEventSink, IDisposable
{
    private const string SourceContext = "SourceContext";
    private const string HttpClientNamespace = "\"System.Net.Http.HttpClient";

    private readonly ILogEventSink _wrappedSink;
    private readonly LogEventLevel _logEventLevelTarget;

    /// <summary>
    /// Initializes a new instance of the <see cref="HttpClientLogLevelWrapper"/> class.
    /// </summary>
    /// <param name="wrappedSink">The wrapped sink.</param>
    /// <param name="logEventLevelTarget">The log event level target.</param>
    public HttpClientLogLevelWrapper(ILogEventSink wrappedSink, LogEventLevel logEventLevelTarget)
    {
        _wrappedSink = wrappedSink;
        _logEventLevelTarget = logEventLevelTarget;
    }

    public void Emit(LogEvent logEvent)
    {
        if (logEvent == null)
        {
            throw new ArgumentNullException(nameof(logEvent));
        }

        if (IsHttpClientInfoLog(logEvent))
        {
            var newLogEvent = new LogEvent(logEvent.Timestamp,
                _logEventLevelTarget,  // this is the only differnce with the original logEvent
                logEvent.Exception, logEvent.MessageTemplate,
                logEvent.Properties.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)));
            _wrappedSink.Emit(newLogEvent);
        }
        else
        {
            _wrappedSink.Emit(logEvent);
        }
    }

    private static bool IsHttpClientInfoLog(LogEvent logEvent)
    {
        if (logEvent.Properties.TryGetValue(SourceContext, out LogEventPropertyValue sourceContext))
        {
            string className = sourceContext.ToString();
            if (!string.IsNullOrEmpty(className)
                && className.StartsWith(HttpClientNamespace, StringComparison.OrdinalIgnoreCase)
                && logEvent.Level == LogEventLevel.Information)
            {
                return true;
            }
        }

        return false;
    }

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

public static class LoggerSinkConfigurationExtensions
{
    public static LoggerConfiguration LowerHttpClientLoggingSink(this LoggerSinkConfiguration lsc, Action<LoggerSinkConfiguration> writeTo)
    {
        return LoggerSinkConfiguration.Wrap(lsc, wrapped => new HttpClientLogLevelWrapper(wrapped, LogEventLevel.Verbose), writeTo, LogEventLevel.Debug, null);
    }
}

然后 Program/Main

中记录器的现在相当复杂的配置
 // First read the wished minimum logger level, read from the enviromment variable.
 LogEventLevel minimumLoggerLevel = GetMinimumLogLevelFromEnvironmentVariable();

 // global shared logger, created BEFORE the host build to be able to log starting and ending the service.
 Log.Logger = new LoggerConfiguration()
     .MinimumLevel.Is(minimumLoggerLevel) 
     .MinimumLevel.Override("Microsoft", LogEventLevel.Error)
     .Enrich.FromLogContext()
     .Enrich.WithThreadId()
     .WriteTo.LowerHttpClientLoggingSink(wt => wt  // LowerHttpClientLogging update the log level from Info to Debug for HttpClient related logs.
        .Logger(lc => lc  // require a sub logger to have the Enrich AFTER the Sink!
        .Enrich.With(new LogLevelEnricher(LoggerAudit.AuditProperty, LogLevelUpperName)) // create levelUpper property and manage AuditProperty AFTER the sink!             
         .WriteTo
         .Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [{ThreadId}] {" + LogLevelUpperName + "} - {Message:lj}{NewLine}{Exception}")))
         .CreateLogger();

上面的 outputTemplate 实际上是为了匹配其他项目中为 log4j 定义的模式,因为此模式随后会被 filebeat 考虑用于 ElasticSearch/Kibana.

这种水槽包装非常适合默认水槽。但是我想使用配置文件配置 serilog,但也包装配置的接收器以将特定调用修改为较低的日志级别。

这是我在 appsetting.json

中的接收器配置
{
"Serilog": {
    "MinimumLevel": {
        "Default": "Verbose",
        "Override": {
            "Microsoft": "Warning",
            "System": "Warning"
        }
    },
    "WriteTo": [
        {
            "Name": "Console",
            "Args": {
                "outputTemplate": "===> {Timestamp:HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}"
            }
        },
        {
            "Name": "RollingFile",
            "Args": {
                "pathFormat": "c:\path\file.txt",
                "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] - {Message}{NewLine}{Exception}"
            }
        },
        {
            "Name": "DurableHttpUsingTimeRolledBuffers",
            "Args": {
                "requestUri": "https://[elastic]",
                "bufferPathFormat": "c:\path\file.json"
            }
        }

现在我要做的是从这个配置创建一个记录器:

var configuration = new ConfigurationBuilder()
                  .SetBasePath(Directory.GetCurrentDirectory())
                  .AddJsonFile("appsettings.json", false, true)
                  .Build();
var configLoggerConfig = new LoggerConfiguration().ReadFrom.Configuration(configuration);
var configLogger = configLoggerConfig.CreateLogger();

然后尝试包装它:

var wrappedLoggerconfig = new LoggerConfiguration().WriteTo.LogLevelModifyingSink(wt => wt.Sink(configLogger), modifiers, levelSwitch);
        Log.Logger = wrappedLoggerconfig.CreateLogger();

修饰符是一个 class,其中包含要修改的特定事件的逻辑。 LogModifyingSink 扩展方法看起来像这样:

public static LoggerConfiguration LogLevelModifyingSink(
         this LoggerSinkConfiguration loggerConfiguration,
         Action<LoggerSinkConfiguration> writeTo,
         ILogLevelModifiers logLevelModifiers,
         LoggingLevelSwitch levelSwitch)
    {
        return LoggerSinkConfiguration.Wrap(
            loggerConfiguration,
            wrapped => new LogLevelModifyingSink(wrapped, logLevelModifiers),
            writeTo,
            LogEventLevel.Verbose,
            levelSwitch); 
    }

其中LogLevelModifyingSink是发出修改日志的wrappersink:

public class LogLevelModifyingSink : ILogEventSink, IDisposable
{
    readonly ILogEventSink wrappedSink;
    readonly IEnumerable<LogLevelModifier> logLevelModifiers;

    public LogLevelModifyingSink(ILogEventSink wrappedSink, ILogLevelModifiers logLevelModifiers)
    {
        this.wrappedSink = wrappedSink;
        this.logLevelModifiers = logLevelModifiers?.GetLevelModifiers();
    }

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

    public void Emit(LogEvent logEvent)
    {
        var message = logEvent.RenderMessage();
        Console.WriteLine(DateTimeOffset.Now.ToString() + " " + message);

        if (wrappedSink != null && logLevelModifiers != null && logLevelModifiers.Any())
        {
            foreach(var modifier in logLevelModifiers)
            {
                if (modifier.ShouldModify(logEvent))
                {
                    wrappedSink.Emit(modifier.ModifyEvent(logEvent));
                    return;
                }
            }
        }
        wrappedSink.Emit(logEvent);
    }
}

现在这部分工作了。日志消息由所有 3 个接收器的包装器处理,但是,通过从新配置创建记录器,minumumlevel 和 minimumlevel 覆盖的设置不会从配置文件中传达,并且没有办法在运行时获取这些设置。 我觉得这个解决方案不是可行的方法,因为我创建了两次记录器。所以我的问题是,有没有更好的方法从配置中包装接收器?有可能吗?我怎样才能保留我配置的设置?