65.9K
CodeProject 正在变化。 阅读更多。
Home

J4JLogger:提供源代码信息的Serilog包装器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (3投票s)

2021年9月30日

GPL3

5分钟阅读

viewsIcon

6667

一个C#库,可自动将源代码信息(例如,调用方法)添加到Serilog事件中

引言

我喜欢Serilog并将其用于我的所有项目。但开箱即用,它没有能力记录日志事件的源代码信息(例如,源文件、调用方法、行号)。J4JLogger就是一个可以做到这一点的Serilog包装器。

它还提供了一种通过SMS发送日志事件的方式(对于真正严重的问题很有用)。虽然目前只包含一个Twilio提供商,但为其他SMS库添加其他提供商也很简单。

最后,J4JLogger提供了一个缓存日志记录器,可用作日志记录目标,直到“真正”的日志记录系统设置好为止(例如,在IHostBuilder构造IHost时)。缓存的条目可以在日志系统建立后倾倒到该系统中,从而提供在调试启动问题时可能很有用的信息。

您可以在J4JLogger的Github页面上找到更多文档。

背景

当我在编写——以及调试!——代码时,我通常喜欢能够直接转到引起问题的地点。由于我几乎总是包含日志记录,查看日志事件将是一个不错的起点……但是,开箱即用的Serilog不提供包含该信息的方式。

包含它的一个方法是编写一个 enriquecedor(丰富器),在Serilog的世界中,它允许你在日志记录过程中插入额外的信息。但真正的挑战是首先获取信息。在所有日志调用中指定方法调用名称、源代码文件名和行号会非常繁琐。而且由于在开发过程中名称和数字经常变化,它也容易出错。

有一个更好的方法:C#编译器允许你在任何方法调用中自动包含该信息,如果你在参数列表中包含某些参数,并用特定的属性装饰它们并分配特定的默认值。这是一个例子

public void Write( 
     LogEventLevel level, 
     string template, 
     [CallerMemberName] string memberName = "", 
     [CallerFilePath] string srcPath = "",
     [CallerLineNumber] int srcLine = 0 );

当你调用这个方法时,关于方法被调用上下文的信息——调用成员的名称、源代码文件的路径以及调用的行号——都可以在Write()方法中获得。这就是J4JLogger包含在日志事件中的信息。

在某些情况下,这可能需要对Serilog调用各种方法的“Serilog方式”进行微小更改。问题出现在许多Serilog日志方法包含泛型参数,这些参数可能被编译器误认为是memberNamesrcPath和/或srcLine

考虑以下方法,对其进行了增强以包含这些额外的上下文参数

public void Error<T0>( 
    string template, 
    T0 propertyValue,
    [CallerMemberName] string memberName = "",
    [CallerFilePath] string srcPath = "",
    [CallerLineNumber] int srcLine = 0);

如果你要记录涉及字符串参数的内容

_logger.Error( "This is a Serilog template with a parameter '{0}'", "some text");

编译器将不知道“some text”是propertyValue还是memberName

为了避免这种歧义,你必须像这样调用方法

_logger.Error<string>( "This is a Serilog template with a parameter '{0}'", "some text");

现在编译器知道“some text”是propertyValue,正如你所希望的那样。

这个问题并不总是出现。通常在第一个参数是string时会出现。

Using the Code

有许多方法可以设置J4JLogger(请参阅Github页面了解详细信息)。这里有一个使用了Serilog的IConfiguration的附加库

using System;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using J4JSoftware.Logging;
using Microsoft.Extensions.Configuration;
using Serilog;

namespace ConfigurationBasedExample
{
    // shows how to use J4JLogger with a configuration file
    // see the ConfigurationBasedExample project for other files and details
    class Program
    {
        static void Main(string[] args)
        {
            var loggerConfig = new J4JLoggerConfiguration();

            var configRoot = new ConfigurationBuilder()
                .AddJsonFile( Path.Combine( Environment.CurrentDirectory, 
                              "appConfig.json" ), true )
                .Build();

            loggerConfig.SerilogConfiguration
                .ReadFrom
                .Configuration( configRoot );

            var logger = loggerConfig.CreateLogger();
            logger.SetLoggedType(typeof(Program));

            logger.Information("This is an Informational logging message");
            logger.Fatal("This is a Fatal logging message");
        }
    }
}

你可以在Github网站上看到控制台输出的样子。日志文件看起来是这样的

2021-09-15 10:14:59.173 -07:00 [INF] This is an Informational logging message 
ConfigurationBasedExample.Program::Main 
(C:\Programming\J4JLogging\examples\ConfigurationBasedExample\Program.cs:32) 
2021-09-15 10:14:59.238 -07:00 [FTL] This is a Fatal logging message 
ConfigurationBasedExample.Program::Main 
(C:\Programming\J4JLogging\examples\ConfigurationBasedExample\Program.cs:33) 

附加功能

短信消息

偶尔,我发现我想要立即收到某些日志事件的通知(例如,如果我依赖的正在运行的服务出现问题)。为了满足这种用例,我添加了将日志事件作为SMS消息发送的功能。

要做到这一点,你必须向日志记录器添加一个Serilog接收器(sink),该接收器在Serilog和SMS服务之间进行接口。该库包含一个可以与Twilio配合使用的接收器。但你可以添加其他的,这并不难(请参阅Github文档了解详细信息)。

SMS功能是通过调整日志记录器的SmsHandling属性来触发的。这可以通过设置它或通过以下便捷方法来完成

public void SendNextEventToSms() => SmsHandling = SmsHandling.SendNextMessage;
public void SendAllEventsToSms() => SmsHandling = SmsHandling.SendUntilReset;
public void StopSendingEventsToSms() => SmsHandling = SmsHandling.DoNotSend;

我通常会逐条消息地使用SMS功能(即SendNextMessage),以免使我的SMS收件箱过载。SendNextMessage会将下一个日志事件,并且仅下一个日志事件,发送到所有Serilog SMS接收器。

缓存日志记录器

我是Microsoft的IHostBuilder/IHost API的忠实粉丝。它允许你在启动时以开发人员定义的方式配置应用程序,然后将执行交给任何你声明为运行时代理的方法。它支持使用IConfigurationBuilder/IConfiguration API和依赖注入。在我的项目中,我经常使用它来设置J4JLogging子系统。

考虑到大多数启动例程的复杂性,在初始化期间记录事件会很方便,以便研究问题。不幸的是,试图通过简单地使用J4JLogger(或者任何其他日志记录器,我猜)来实现这一点会产生一个鸡生蛋还是蛋生鸡的问题:你无法记录到一个尚未配置和构建的日志记录器。

我绕过这个问题的方法是定义一个J4JLogger的特殊版本,J4JCachedLogger,它不包装Serilog日志记录器。事实上,它根本不发出任何日志事件。相反,它将它们存储在内存中,直到“真正”的日志记录器设置好。届时,你可以通过调用以下方法将所有缓存的日志事件倾倒到J4JLogger

public abstract bool OutputCache( J4JCachedLogger cachedLogger );

J4JLogger中所实现的,该方法按创建顺序将所有缓存的事件写入日志(在J4JCachedLogger中,该实现什么都不做,因为事件已经在缓存中了)。

这并不是鸡生蛋问题的完整解决方案,因为如果初始化失败,在J4JLogger实例设置好之前,就没有东西可以发出缓存的事件了。我正在努力为这种情况提供一种查看/输出这些缓存事件的替代方法。

普通事件

我有时需要在一个UI中显示日志消息,或者其中的一部分。为了简化这一点,我在IJ4JLogger 中添加了一个C#事件,你可以监听它

event EventHandler<NetEventArgs>? LogEvent;

NetEventArgs 是一个简单的类,它只是回显一个日志事件

public class NetEventArgs
{
    public NetEventArgs( LogEventLevel level, string mesg )
    {
        Level = level;
        LogMessage = mesg;
    }

    public LogEventLevel Level { get; }
    public string LogMessage { get; }
}

请注意,除非你将NetEventSink添加到你的Serilog配置中,否则你不会收到任何事件。最简单的方法是使用提供的扩展方法(该方法在J4JLoggerConfiguration的实例上调用,而不是在底层Serilog配置对象上调用)

public static LoggerConfiguration NetEvent(
    this J4JLoggerConfiguration loggerConfig,
    string outputTemplate = NetEventSink.DefaultTemplate,
    LogEventLevel restrictedToMinimumLevel = LogEventLevel.Verbose
)

你可以提供自己的Serilog输出模板和要报告的最小事件级别。

历史

  • 2021年10月1日:添加了关于通过C#事件监听日志事件的材料
  • 2021年9月30日:添加了额外的细节和经验教训
  • 2021年9月29日:在CodeProject上首次发布
© . All rights reserved.