在.NET环境中,日志库的选择非常多样。有NLog、Log4Net、Enterprise Logging、Serilog和Common.Logging等,这些都是由才华横溢的开发者们投入大量时间和努力开发的,功能丰富且健壮的软件。
然而,尽管市场上有如此多的选择,作者在几个月前发现自己需要一个满足特定需求的日志库。作者开发了一个通过互联网分发的商业桌面应用程序,因此对日志库有以下三个硬性要求:
作者认为这些要求相当直接,但事实证明他找不到满足需求的东西。特别是,许多日志库的许可条款作者无法接受,因为他不想被迫分发“另一个”许可。
因此,作者编写了自己的日志库——Clearcove.Logging。它非常轻量级,只有83行代码。完整的实现也在一个单独的.cs文件中,因此可以在不导入库的情况下重用。代码是用VS 2017编写的,但作者尽量编写与早期版本兼容的代码。日志库针对.NET2.0,以吸引更广泛的受众。
作者认为这种日志方法对于以下情况是一个很好的选择:
首先,作者考虑了想要记录的信息。作者想要一个简单的API,可以用来记录时间戳、记录器名称、线程ID和消息等信息。作者非常熟悉Log4Net API,并大量借鉴了它。
声明和使用记录器的语法如下:
var log = new Logger(typeof(Program));
// 类级别声明。
log.Error("My error message", exception);
// 方法内记录。
log.Info("My info message");
如果过去使用过其他日志库,这种语法应该是熟悉的。
接下来,作者想要将日志条目表示为一个简单的对象。这样做的主要原因是作者希望记录器能够引发日志事件。作者有时在创建单元和集成测试时使用这些事件,因为作者发现这很有帮助。这只是个人偏好。如果对引发日志事件不感兴趣,那么这段代码可以简化。
日志事件封装在LogMessageInfo对象中,实现如下:
public sealed class LogMessageInfo
{
public readonly DateTime Timestamp;
public readonly string ThreadId;
public readonly string Level;
public readonly string Logger;
public readonly string Message;
}
现在,来谈谈代码的核心部分。上面讨论的API实现和数据封装部分的代码很冗长,但非常简单。异步日志记录则有些微妙。例如,如果抛出异常导致应用程序关闭,会发生什么?怎么知道所有日志条目都会按照接收的顺序写入?有几种方法可以解决这个问题。
Clearcove.Logger以一种简单但有些不优雅的方式解决了这个问题:
static void Main(string[] args)
{
var targetLogFile = new FileInfo("./MyApp.log");
Logger.Start(targetLogFile);
// 如果跳过初始化,记录器会抱怨
try
{
Run(args);
}
finally
{
Logger.ShutDown();
// 移除此行可能会导致丢失日志条目。
}
}
这是一个Clearcove.Logging与其他实现(如Log4Net)不同的地方。必须告诉记录器何时开始和停止记录。在尝试将任何日志条目写入日志文件之前,必须这样做。将Logger.ShutDown()调用放在finally语句中应该让记录器有机会在应用程序关闭之前将所有待处理的日志条目写入日志文件。当然,会有一些情况,日志条目不会被写入。例如,如果机器失去电源。如果这些边缘情况对来说是一个问题,可能需要考虑同步日志记录。
Clearcove.Logging通过使用单个System.Thread.Timer实例实现异步日志写入。线程计时器的周期没有设置,因此计时器只会触发一次。在所有待处理的日志条目成功写入日志文件后,计时器将被重置以在下一个间隔触发。这种行为将类似于设置计时器周期,但将防止计时器在间隔延迟的情况下被多次触发。
最后,日志条目使用简单的File.AppendAllText调用写入文件。这个调用可能不是对日志文件进行多次写入的最有效方式,但基于保持代码尽可能简单的考虑而选择。
就这样。一个非常简单的日志实现,完全能够满足大多数应用程序的日志需求。它对作者来说效果很好,解决了所有的日志问题,同时将依赖性降到最低。