适用于 .NET 的全面日志记录包






4.91/5 (111投票s)
2003 年 10 月 14 日
11分钟阅读

700514

4534
适用于 .NET 的全面日志记录包
引言
我最近开始认真研究 .NET 框架。我发现需要一个 .NET 日志框架,它能提供比 System.Diagnostics
命名空间中的类更强大的功能和更好的可扩展性。在 .NET 类库或网络上都找不到符合要求的东西,所以我硬着头皮自己动手做了一个。经过一个月的业余时间编写了无数行代码,又花了两周时间编写文档,我觉得终于到了一个可以喘息的阶段。(有关帮助信息论坛、项目 Bug 跟踪系统、项目最新代码和文档以及联系方式,请访问 http://sourceforge.net/projects/nspring。)
背景
我过去七八年的背景主要是在 Java 开发领域;然而,随着 .NET 框架的发布以及我所在公司被一家以 Microsoft 为中心的收购,我决心尽我所能学习有关竞争对手的一切。我花了一些时间获得了 MCSD .NET 认证,并在此过程中学到了很多关于 Microsoft 的做事方式。我发现,到目前为止,.NET 框架提供了令人愉悦的性能和功能组合。然而(我怀疑这可能主要是由于平台尚处于早期阶段),它仍然缺少关键功能,日志记录支持就是其中之一。
日志记录为什么如此重要?
首先,我并不是说 System.Diagnostics
类 Debug
和 Trace
不好。它们对于快速故障排除来说很不错,并且“diagnostics”这个名字也反映了这些类的意图。但这与全功能的日志记录支持并不完全相同;真正的日志记录不仅是诊断性的,而且是历史性的。例如,我绝不会想编写一个重要的服务器端应用程序,而该应用程序完全依赖 Debug
和 Trace
来记录有关配置、客户端连接等信息。它们的性能不够,并且我多年来使用不同语言处理过的优秀日志包所期望的可编程性功能也缺失了。
如果您愿意,可以跳过下一节,直接阅读编程示例,然后下载 .zip 文件。它包含大量的 HTML 文档,其中包括一个编程示例,详细说明了每种日志记录器、事件格式化器和数据格式化器的使用方法。文档的编写实际上比代码花费了我更长的时间。
日志包功能
新的日志包提供以下功能,但不限于此
高性能: Debug
和 Trace
类速度很慢;这些类未缓冲的使用可能会导致应用程序代码出现显著的性能下降。此框架中的日志记录器提供缓冲能力(异步输出)以及其他缓存功能;在 P4-M 2.0GHz 笔记本电脑上,这使得每条消息的日志记录时间约为 500 纳秒。后续版本计划进行进一步的性能优化。
稳定性: 日志包中的每个类都是线程安全的,并且经过性能调优,适用于多线程环境。此外,可以通过实现 ExceptionHandler
接口的类来提供日志记录失败支持;由于 Logger
基类本身实现了此接口,所有日志记录器都可以为彼此提供故障转移日志记录支持。
易用性: 本包中提供的日志记录器易于理解和使用。每个日志记录器代表一种不同的输出形式;提供了许多不同的预先编写的日志记录器,涵盖从电子邮件日志记录到数据库日志记录再到各种基于文件的日志记录,其中大多数都可以通过调用其构造函数方法轻松配置。已采取措施使每个接口尽可能直观易用。
易于扩展: 用户可以轻松定义自己的日志记录器、过滤器、异常处理程序、事件格式化器、数据格式化器和日志级别,通常只需实例化一个对象或实现一两个方法。例如,可以通过实现单个方法来创建自定义 Logger
子类,并仍然获得 Logger
类提供的所有优势,包括缓冲支持。
级别: 每条日志消息(Event
)都附带一个“优先级”级别。每个 Logger
都有一个关联的级别,并丢弃低于此默认级别的所有事件,从而提供简单但细粒度的消息过滤能力。用户还可以定义自己的级别,只要它们包含唯一的信息。
灵活的过滤: 优秀日志记录支持的一个标志是能够根据比级别使用更深入的条件来选择性地丢弃日志数据。NSpring.Logging.Filters
命名空间中不仅提供了许多预先构建的过滤器,而且可以通过继承 Filter
类轻松实现自定义过滤器。
灵活的格式化: NSpring.Logging.EventFormatters
命名空间中的类提供了有用的日志记录格式化类型。XMLEventFormatter
是高度可配置的,并且用户还可以通过继承 EventFormatter
类来提供自己的格式化逻辑。简单的格式化语言也提供了极大的格式化灵活性。
数据支持: 每条日志事件都可以携带一个对象数据“负载”。内置支持正确输出所有数组、列表和字典类,并能够为每种原始类型以及 DateTime
和 TimeSpan
等结构提供格式化字符串。用户可以通过继承 DataFormatter
来扩展日志记录器的数据格式化能力。开箱即用地提供了三种数据格式化实现。
分区访问: Debug
和 Trace
只为应用程序中的日志记录设施提供两个全局访问点。这在许多方面都很糟糕,最重要的是,默认的区分内容的方式(Indent()
和 Unindent()
静态方法)在设计时根本没有考虑多线程使用。换句话说,两个线程尝试同时使用 Debug
和 Trace
可能会相互干扰。此外,虽然您可以为这两个类创建各种侦听器,但类必须实现自己的过滤逻辑(每次添加侦听器时,在最坏情况下会导致性能呈指数级下降),因为所有输出都通过 Debug
或 Trace
进行。这引出了下一一点。
命名: 可以使用 LoggerRegistry
对象将日志记录器注册为一个或多个名称。Logger
类本身通过使用内部静态默认 LoggerRegistry
提供对日志记录器的全局命名访问。
组合: 此框架中的日志记录器可以作为子项添加,接收传递的事件。可以为不同的需求创建不同的日志记录结构,所有这些结构都组织在一个易于使用的名称下。过滤器还可以通过 CompositeFilter
类组合其他过滤器。
事件分类: Debug
和 Trace
只为每条日志事件提供四种可能的数据字段:一个字符串消息,一个更详细的字符串消息,一个字符串类别和一个对象数据负载。没有为不同应用程序/模块使用同一日志子系统进行日志记录等情况做任何规定。NSpring 日志框架记录的每个 Event 对象都包含时间戳、文本消息、数据对象、事件类别、事件代码、事件 ID、事件严重级别、应用程序名称和系统名称等字段。Logger
类 Log
方法的许多不同变体都很容易使用,并且提供了(通过 Filter
类及其子类)与每个字段配合使用的日志过滤器。某些日志记录器(如 FileLogger
和 MessageQueueLogger
)能够根据通过它们记录的事件的属性将输出“分散”到许多不同的文件、队列等。将输出发送到不同的命名日志记录器的能力也提供了有用的分类能力。
对现有代码的支持: 可以使用 DebugLogger
和 TraceLogger
类将此日志包与依赖于 Debug
和 Trace
的现有代码一起使用;通过使用日志记录器组合,可以将相同的输出轻松重定向到其他输出形式。(另外正在开发中:一系列事件源类,它们可以侦听各种数据源,包括 Debug
和 Trace
,并将生成的 PUSH 数据通过日志记录类。)
编程示例 #1:日志记录到文件
(此示例和以下两个示例是随意从项目文档中选取的。)
FileLogger
实例将其输出写入文件。文件路径可以是相对的或完整的,可以是硬编码的,也可以是格式化模式。
日志记录器将创建写入任何文件所需的任何目录。在不涉及大量内存开销的情况下,FileLogger
会缓存已知目录的信息以提高性能。强烈建议在此日志记录器中使用缓冲/异步输出,因为每个文件在日志记录操作之间都会关闭。在未缓冲模式下的性能对于大多数情况来说是可以接受的,但缓冲模式下的性能非常快。
可以通过在文件路径中使用格式化模式将文件输出“分散”到多个文件中。例如,这可以用于实现“滚动”日志文件,其中输出会按周期性(例如,每小时)进行重定向。在分散模式下的性能仍然非常出色,因为日志记录器会以缓冲模式保持多个流打开(除非路径包含经常变化的信息,如事件 ID 或任何包含毫秒的时间标记值。使用这种易变路径的写法也很好,但会导致为每个事件打开和关闭一个流,从而降低性能)。
通过使用 IsArchivingEnabled
、ArchiveDirectoryPath
和 MaximumFileSize
属性来支持存档。如果启用了存档,任何达到最大尺寸的文件都会被移动到指定目录,并使用包含时间戳数据的额外扩展名(以区分同一文件的多个存档)以及可选的额外文件扩展名进行命名,以便于读取存档。此版本不支持存档压缩。
默认使用的事件格式化器是 PatternEventFormatter
的实例,它使用 FlatDataFormatter
进行数据格式化。在下面的示例中,使用了一个工厂方法来创建日志记录器,但也可以使用构造函数方法来创建。
using System;
using NSpring.Logging;
public class FileLoggerExample {
public static void Main(string[] args) {
// The first parameter here is the file path; the second, the output
// pattern
Logger logger = Logger.CreateFileLogger("c:\\temp\\nspring.log", +
"{ts}{z} [{ln:1w}] {msg}");
logger.IsBufferingEnabled = true;
logger.BufferSize = 1000;
logger.Open();
logger.Log(Level.Debug, "My hamster's name is Wilhelmina");
logger.Log(Level.Verbose, "She has two tails");
logger.Log(Level.Config, "She wears a green mohawk");
logger.Log(Level.Info, "She's studying to be an accountant");
logger.Log(Level.Warning, "Chrysanthemums are her favorite food");
logger.Log(Level.Exception, "Underneath that tough biker exterior," +
" she's just a sweet hamster");
logger.Close();
}
}
输出写入 c:\temp\nspring.log
2003-10-13 12:25:25.895-04:00 [D] My hamster's name is Wilhelmina
2003-10-13 12:25:25.895-04:00 [V] She has two tails
2003-10-13 12:25:25.895-04:00 [C] She wears a green mohawk
2003-10-13 12:25:25.895-04:00 [I] She's studying to be an accountant
2003-10-13 12:25:25.895-04:00 [W] Chrysanthemums are her favorite food
2003-10-13 12:25:25.895-04:00 [E] Underneath that tough biker exterior,
she's just a sweet hamster
编程示例 #2:使用日志记录器组合
这展示了一个 CompositeLogger
类的简单使用示例。请注意消息是如何按子日志记录器分组的。其解释是,在缓冲模式下,日志记录器始终以批次将事件传递给每个子项,希望它们能够利用批量写入的增强功能。如果父日志记录器未缓冲,则语句会交错。
using System;
using NSpring.Logging;
public class CompositeLoggerExample {
public static void Main(string[] args) {
// This very simple pattern will help differentiate output
Logger cl1 = Logger.CreateConsoleLogger("Logger 1: {message}");
// of the two child loggers
Logger cl2 = Logger.CreateConsoleLogger("Logger 2: {message}");
Logger logger = Logger.CreateCompositeLogger(cl1, cl2);
logger.IsBufferingEnabled = true;
// If the buffer reaches this size, the logger's thread will
// automatically awaken
logger.BufferSize = 10000;
logger.AutoFlushInterval = 15000; // This value is in milliseconds
logger.Open(); // This causes the opening of the children
logger.Log("My hamster's name is Wilhelmina");
logger.Log("She drives a tiny dump truck");
logger.Log("She always wins at arm-wrestling");
logger.Close(); // This causes the closing of the children
}
}
输出
Logger 1: My hamster's name is Wilhelmina
Logger 1: She drives a tiny dump truck
Logger 1: She always wins at arm-wrestling
Logger 2: My hamster's name is Wilhelmina
Logger 2: She drives a tiny dump truck
Logger 2: She always wins at arm-wrestling
编程示例 #3:XMLDataFormatter 示例
每个 XMLDataFormatter
实例都将数据对象格式化为 XML。如果格式化了一个集合,则它被视为两种类型之一:映射(如字典类)或列表(如数组或任何 IEnumerable)。
用于构造 XML 名称的大小写和分隔符选项可以使用 NSpring.Logging.XML.XMLFormatOptions
类进行设置。字符串、原始类型以及某些结构(如 DateTime
、TimeSpan
和 Decimal
)的格式化通过设置超类的属性中的格式化字符串来控制。有关更多详细信息,请参阅 DataFormatter
超类的文档。
using System;
using System.Collections;
using NSpring.Logging;
using NSpring.Logging.Loggers;
using NSpring.Logging.DataFormatters;
using NSpring.Logging.EventFormatters;
public class XMLDataFormatterExample {
public static void Main(string[] args) {
ConsoleLogger logger = new ConsoleLogger();
EventFormatter eventFormatter =
new PatternEventFormatter("{message}\n{data}\n");
eventFormatter.DataFormatter = new XMLDataFormatter();
eventFormatter.IsIndentationEnabled = true;
logger.EventFormatter = eventFormatter;
string s = "schmaltz";
int i = 5;
float f = 1.234567F;
TimeSpan ts = new TimeSpan(1, 2, 3, 4, 555);
DateTime dt = DateTime.Now;
Hashtable h = new Hashtable();
h["key1"] = "value1";
h["key2"] = "value2";
object[] oa = {s, i, f, ts, dt, h};
logger.Log("A single string value:", (object)s);
logger.Log("A single integer value:", i);
logger.Log("A single float value:", f);
logger.Log("A single TimeSpan value:", ts);
logger.Log("A single DateTime value:", dt);
logger.Log("A Hashtable value:", h);
logger.Log("An array of the above:", oa);
}
}
输出
A single string value:
<data type="string">schmaltz</data>
A single integer value:
<data type="integer">5</data>
A single float value:
<data type="float">1.234567</data>
A single TimeSpan value:
<data type="duration">P1DT2H3M4.555S</data>
A single DateTime value:
<data type="dateTime">2003-10-09T18:47:05.181-04:00</data>
A Hashtable value:
<data type="map">
<entry>
<key type="string">key1</key>
<value type="string">value1</value>
</entry>
<entry>
<key type="string">key2</key>
<value type="string">value2</value>
</entry>
</data>
An array of the above:
<data type="list">
<value type="string">schmaltz</value>
<value type="integer">5</value>
<value type="float">1.234567</value>
<value type="duration">P1DT2H3M4.555S</value>
<value type="dateTime">2003-10-09T18:47:05.181-04:00</value>
<value type="map">
<entry>
<key type="string">key1</key>
<value type="string">value1</value>
</entry>
<entry>
<key type="string">key2</key>
<value type="string">value2</value>
</entry>
</value>
</data>
关注点
日志框架速度快的主要原因有两个:尽可能避免对象创建和同步。上述时间的大部分(在笔记本电脑上缓冲模式下记录一条消息约 500 纳秒)用于构造单个 Event
对象,这是记录消息所必需的唯一对象创建。我还没有找到一种可行的对象缓存方案,可以避免创建此对象,同时还能提供可扩展 API 所需的开放性。但是,我还没有探索 .NET 弱引用,这是一个显示出明显潜力的领域。
在进一步减少(到小于一!)每条消息的对象创建开销的情况下,我至少计划*延迟*创建此对象。因此,应用程序线程不再驱动 Event
对象的创建并将其推入缓冲区,我可能会更改为使应用程序代码将事件相关数据推送到一系列数组中,让日志记录器自己的线程在它有空时构造 Event
对象。这应该会大大加快速度。
有助于减少同步开销的一个重要因素是使用对象副本和不可变对象;如果某个对象不能被另一个线程更改,则无需同步。例如,EventFormatter
子类的内部不需要同步,因为其结构安排使得没有两个日志记录器使用相同的事件格式化器对象,而是将任何传入对象的副本存储在内部。我认为,即使是未受争用的锁获取,在关心微秒和纳秒时也是至关重要的。
模式评估的速度非常快。事实上,测试日期模式字符串与 .NET 日期格式字符串的格式化表明,.NET 内置格式化速度较慢。我想,有时候人们说的话是真的——代码越通用,代码越慢。有关迷你格式化语言的完整参考,请参阅 HTML 文档的附录。
我强烈推荐 NDoc,这是 NDoc 项目在 sourceforge.net 上发布的一个实用工具。没有它,我永远无法制作出如此专业外观的 API 文档。
正在开发中...
一个配置框架、集中式日志记录应用程序以及其他几项增强功能。如果您有时间,我很欢迎您在这些和其他计划功能上提供帮助。
历史
- 提交于 2003-10-14