消息接收器!
一个非常小的日志库。
引言
多年来,我一直在构建一个名为“应该内置到 .NET 框架中但尚未内置”的库。但我一直推迟撰写关于这些应该内置但尚未内置的东西的文章。现在不会了!它名为 Loyc.Essentials(以 Loyc 命名,但这并不重要)。
背景
多年前(至今仍然如此),log4net 是 C#/.NET 中一个非常流行的日志库。多年前,我萌生了一个想法,想将最重要、最受欢迎的库提炼成一个单一的库。于是我查看了 log4net——诚然,我之前从未真正使用过它——目标是在 Loyc.Essentials 中创建一个小得多的日志库,它提供了 log4net 最常用的功能。
但这并没有成功。事实证明,log4net 不仅仅是一个大型库(比 Loyc.Essentials 的所有内容都大),而且它也非常复杂,是交织在一起的接口和依赖项的混乱集合。很难理解它内部的工作原理,而且我不确定在哪里可以找到保留与最常用功能兼容性的有用“核心”。
相反,我只是设计了一个小型日志库,而不考虑与 log4net 的兼容性——一个不仅可用于日志记录,还可用于其他产生消息的情况,例如编译器中的错误消息。
我称之为“消息接收器”——一个可以倾倒消息的出口。术语“接收器”与 Loyc.Essentials 中其他“源”和“接收器”的命名约定一致,例如 ICollectionSink<in T>
是 ICollection<T>
的一个子集,它允许你添加或删除项,但不能获取项,而 IListSource<out T>
类似于 IList<T>
,但你只能获取项,不能添加或删除项。
界面
IMessageSink
是一个简单的接口,旨在方便定义自己的实现,以便你可以根据需要轻松地扩展它以获得更多功能——同时平衡对良好性能的需求。
// Alias for IMessageSink<object>
public interface IMessageSink : IMessageSink<object>
{
}
public interface IMessageSink<in TContext>
{
/// <summary>Returns true if messages of the specified type will actually be
/// printed, or false if Write(type, ...) has no effect.</summary>
bool IsEnabled(Severity type);
/// <summary>Writes a message to a log or other target.</summary>
void Write(Severity level, TContext context, string format);
void Write(Severity level, TContext context, string format, object arg0, object arg1 = null);
void Write(Severity level, TContext context, string format, params object[] args);
}
大多数人只会使用 IMessageSink
,但你也可以自定义 context
参数的含义。注意 IMessageSink<in TContext>
中的 in
:这意味着任何 IMessageSink<object>
都可以隐式转换为任何类 C 的 IMessageSink<C>
。因此,例如,你可以像使用 IMessageSink<C>
一样使用 ConsoleMessageSink
,即使它只实现了 IMessageSink
。
第一个方法 IsEnabled(c)
允许你在打印消息之前了解该消息是否真的可以打印(如果返回 false,则类别为 c
的消息将被过滤掉)。这可以让你避免为将被丢弃的消息进行构建工作。
一条消息有四个部分
Severity type
:一个枚举,用于在数字尺度上指示消息的类型(“严重”程度或“常见”程度)。常用值包括Severity.Error
、Severity.Warning
和Severity.Debug
。TContext context
:一个代表消息相关“上下文”或“位置”的对象。通常TContext = object
,所以这个参数可以是任何东西,上下文的确切含义可以因应用程序而异。string format
:一条要记录的消息,带有可选的参数占位符,如{0}
和{1}
。- format arguments:插入到 format 字符串中的对象或字符串。
现在,log4net
允许你编写一个对象或一个字符串,但在我看来,编写一个对象和一个字符串更有意义,其中对象为消息提供某种“上下文”。除了这个接口之外,还提供了许多类似 log4net 的扩展方法,你可以用它们作为快捷方式(例如,Warn("It's cold outside!")
,它使用 level: Severity.Warning
和 context: null
)。
实际上只需要一个 Write()
方法,但人们普遍期望消息接收器会丢弃一些或所有消息而不打印它们,例如,如果消息接收器用于日志记录,则详细消息可能默认是“关闭”的。如果消息不会实际打印,则格式化消息会浪费资源,即使创建对象数组来保存参数也会浪费资源,如果它们将被丢弃。考虑到这一点,由于大多数格式化请求只需要几个参数,因此有一个 Write()
的重载,它接受最多两个参数,而无需将它们打包到 params
数组中。
所以有三个 Write
Write(Severity, TContext, string)
用于可以(也应该)在不执行替换的情况下写入的字符串。Write(Severity, TContext, string, object, object)
用于带有一个或两个参数的消息。如果只有一个参数,第二个默认为null
。许多其他库为一、二和三个参数提供单独的重载;IMessageSink
只有一个固定长度的重载,因为它旨在易于实现接口,而不仅仅是易于使用它。Write(Severity, TContext, string, params object[])
用于超过两个参数的情况。
消息接收器可以使用 Localize.Localized()
执行本地化。
基本接收器
Loyc.Essentials 内置了以下“基本”接收器(带 .Value
的是单例——你通常不需要创建新实例)
ConsoleMessageSink.Value
:将消息写入控制台,不同级别的消息有不同的颜色(例如,红色 = 错误,黄色 = 警告,青色 = 调试)。TraceMessageSink.Value
:调用System.Diagnostics.Trace.WriteLine
。NullMessageSink.Value
(又名MessageSink.Null
):丢弃所有消息。但是,有一个Count
属性,它会随着收到的每条消息而增加,还有一个ErrorCount
属性用于错误计数。new MessageHolder()
:将其List
属性中保存的所有消息。
ConsoleMessageSink
和 TraceMessageSink
产生相似的输出;例如
ConsoleMessageSink.Value.Write(Severity.Error, "Foo.csv", "Syntax error")
输出为
Error: Foo.csv: Syntax error
默认情况下,ConsoleMessageSink
(但不是 TraceMessageSink
)会省略较低级别消息(低于 Warning
)的严重性,因此文本颜色本身指示了 Severity
。
注意:消息接收器通过调用 MessageSink.ContextToString
将上下文对象转换为字符串,参见下文。
这些类非常简单;例如,这是 TraceMessageSink
的完整源代码
/// <summary>Sends all messages to System.Diagnostics.Trace.WriteLine(string).</summary>
public class TraceMessageSink : IMessageSink
{
public static readonly TraceMessageSink Value = new TraceMessageSink();
public void Write(Severity type, object context, string format)
{
WriteCore(type, context, Localize.Localized(format));
}
public void Write(Severity type, object context, string format, object arg0, object arg1 = null)
{
WriteCore(type, context, Localize.Localized(format, arg0, arg1));
}
public void Write(Severity type, object context, string format, params object[] args)
{
WriteCore(type, context, Localize.Localized(format, args));
}
public void WriteCore(Severity type, object context, string text)
{
string loc = MessageSink.ContextToString(context);
if (!string.IsNullOrEmpty(loc))
text = loc + ": " + text;
Trace.WriteLine(text, type.ToString());
}
/// <summary>Always returns true.</summary>
public bool IsEnabled(Severity type)
{
return true;
}
}
(Localized
是什么?见此处。)
包装接收器
某些接收器类型是包装器对象,它们会修改“内部”或“目标”接收器
new SeverityMessageFilter(IMessageSink target, Severity minSeverity)
:过滤掉严重性低于最低值的消息。new MessageFilter(IMessageSink target, Func<Severity, bool> filter)
:允许你单独控制每个Severity
的过滤。new MessageFilter(IMessageSink target, Func<Severity, object, string, bool> filter)
:允许你根据上下文和/或格式字符串以及严重性来过滤掉消息。当有人调用MessageFilter.IsEnabled(Severity level)
时,MessageFilter
会依次调用filter(level, null, null)
。过滤方法可以在构造函数后更改。new MessageMulticaster(params IMessageSink[] targets)
:将消息广播到接收器列表(“目标”)。列表可以在MessageMulticaster
构造后进行编辑。IsEnabled(level)
返回 true,如果任何目标在该级别返回 true。new MessageSinkWithContext(IMessageSink target, object context, string messagePrefix = null)
:如果调用Write()
时context
参数为 null,则将其设置为指定的对象。另外,如果提供了消息前缀,它将与格式字符串连接起来,然后传递给target
。(优化:如果你指定了前缀并且target.IsEnabled
返回 false,则方法将返回而不执行任何操作。)
扩展方法
IMessageSink
具有一系列类似以下的扩展方法,允许你像使用 log4net 一样使用它
public static bool IsErrorEnabled<C>(this IMessageSink<C> sink) { return sink.IsEnabled(Severity.Error); } public static void Error(this IMessageSink<object> sink, string format) { sink.Write(Severity.Error, null, format); } public static void ErrorFormat(this IMessageSink<object> sink, string format, params object[] args) { sink.Write(Severity.Error, null, format, args); } public static void Error<C>(this IMessageSink<C> sink, C context, string format) { sink.Write(Severity.Error, context, format); } /* ...more Error methods... */ public static bool IsWarnEnabled<C>(this IMessageSink<C> sink) { return sink.IsEnabled(Severity.Warning); } public static void Warn(this IMessageSink<object> sink, string format) { sink.Write(Severity.Warning, null, format); } public static void WarnFormat(this IMessageSink<object> sink, string format, params object[] args) { sink.Write(Severity.Warning, null, format, args); } public static void Warning<C>(this IMessageSink<C> sink, C context, string format) { sink.Write(Severity.Warning, context, format); } /* ...more Warn methods... */
Warn
和 WarnFormat
的名称直接来自 log4net。
注意:称为 ErrorFormat
(以及 WarnFormat
等)的方法,这些方法不接受上下文参数,实际上无法直接调用 Error
。如果该方法被命名为 Error
而不是 ErrorFormat
,那么如果你调用
messageSink.Error("", "");
调用将在 IMessageSink<C>.Error(C context, string format)
和 IMessageSink.Error(string format, object arg0)
之间产生歧义。所以要小心:需要 Format
这个词来告诉编译器没有上下文参数!不幸的是,没有办法告诉编译器上下文永远不会是字符串。
特别是对于警告,log4net
将它们称为 Warn
,而我将它们称为 Warning
。所以我决定在提供上下文参数时,该方法将被命名为 Warning
。这确保了当你调用 Warn
但你实际上打算调用 WarnFormat
时,你会得到一个编译器错误,而不是调用错误的方法。
与 log4net 的比较
log4net 通常通过 XML 文件进行配置。如果有人愿意贡献一个类似的功能到 Loyc Core,那将是很好的,但我个人不需要基于 XML 的配置,并且为了保持 Loyc.Essentials 的小巧,该功能可能会放在一个单独的程序集中,除非该功能可以以相当紧凑的方式实现。
在 log4net 中,有一种约定是定义每个类中的一个静态字段来提供日志记录
private static readonly log4net.ILog log = log4net.LogManager.GetLogger (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
你可以使用类似的方法来处理消息接收器
private static readonly IMessageSink log = MessageSink.WithContext (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
这将把消息发送到默认消息接收器(MessageSink.Default
),使用当前类的 Type
作为默认上下文参数(当传递给 Write
的上下文为 null 时)。
如果你想在已经使用 log4net 的代码中玩转消息接收器,你甚至可以添加一个“fake”log4net,这样原始代码就能继续工作
using Loyc;
namespace log4net
{
interface ILog : IMessageSink
{
// Add things from the real ILog if necessary
}
class MessageSinkAsILog : WrapperBase<IMessageSink<object>>, ILog
{
public MessageSinkAsILog(IMessageSink<object> wrappedObj) : base(wrappedObj) { }
public bool IsEnabled(Severity level)
{
return _obj.IsEnabled(level);
}
public void Write(Severity level, object context, [Localizable] string format)
{
_obj.Write(level, context, format);
}
public void Write(Severity level, object context, [Localizable] string format,
params object[] args)
{
_obj.Write(level, context, format, args);
}
public void Write(Severity level, object context, [Localizable] string format,
object arg0, object arg1 = null)
{
_obj.Write(level, context, format, arg0, arg1);
}
}
class LogManager
{
public static ILog GetLogger(object type) {
return new MessageSinkAsILog(MessageSink.WithContext(type));
}
}
}
如果你想进行特定于 Type
的过滤(例如,在某些类型中过滤掉 Debug
消息,而在其他类型中不过滤),Loyc.Essentials 目前不直接支持;你需要编写一些自定义代码。你还需要 using Loyc
才能使用扩展方法。
注意:对 IMessageSink
的调用与 log4net 的 ILog
并非完全源代码兼容。第一个原因是像 IsErrorEnabled()
这样的扩展方法是方法,而在 log4net 中它们是属性。如果 Microsoft 在 C# 中添加了 “扩展一切”,则该扩展方法最终可以更改为属性。第二个原因是 log4net 具有 Error(object)
这样的方法,它们接受一个不带字符串的对象,但 Loyc.Essentials 具有不同的“理念”,即同时传递一个对象和一个字符串。
自定义行为
当然,你总是可以实现自己的 IMessageSink
来获得自定义行为。你也可以通过调用 MessageSink.FromDelegate
快速创建一个消息接收器,而无需实现整个 IMessageSink
接口
var sink = MessageSink.FromDelgate( (level, context, fmt, args) => {}, level => /* return true if level is enabled */);
你可以通过调用 MessageSink.SetDefault()
来设置默认消息接收器。此方法返回一个兼容 using
的结构,这样如果你不想永久更改它,可以临时更改它。例如
// block all messages temporarily using (MessageSink.SetDefault(MessageSink.Null)) { DoSomething(); } // old message sink is restored here
这是 环境服务模式的一个例子。
需要将 context
转换为字符串的消息接收器应通过调用 MessageSink.ContextToString(context)
来实现。此方法的默认行为是检查对象是否实现了 IHasLocation
接口
public interface IHasLocation // in namespace Loyc { object Location { get; } }
如果实现了,则调用 Location
属性并将返回的位置转换为字符串;否则,将对上下文本身调用 ToString()
。
这很有用,例如,在我像 Enhanced C# 这样的编译器中。上下文是发生错误的语法树,但位置对象代表源文件中该语法树的位置,因此错误消息最终具有类似“Foo.ecs(123,21)”的位置。
如果此行为不符合你的需求,你可以通过调用 MessageSink.SetContextToString(context => ...)
来覆盖它。
详细说明
有时你可能想在刚刚写入的消息中添加更多详细信息。你可以使用如下代码添加一个消息接收器
MessageSink.Write(Severity.Error, location, "Expected closing brace here"); MessageSink.Write(Severity.ErrorDetail, openlocation, "(Opening brace was here)");
Severity
的每个正常值都是一个偶数,带有一个相关的 Detail
严重性,比它小一。例如,Severity.Warning
是 60,Severity.WarningDetail
是 59。
在使用 SeverityMessageFilter
时,你应该优先使用 Detail
级别作为 MinSeverity
s = new SeverityMessageFilter(c.Sink, Severity.NoteDetail);
为了防止用户不小心写 Severity.Note
而不是 Severity.NoteDetail
,或者 Severity.Warning
而不是 Severity.WarningDetail
,SeverityMessageFilter
有第三个参数,默认为 true
new SeverityMessageFilter(c.Sink, Severity.Warning, includeDetails: true);
第三个参数仅当第二个参数是偶数时才将其减一,因此细节会被包含,除非你特别将其设置为 false。但是,当你设置 SeverityMessageFilter.MinSeverity
属性时,不会发生这种技巧。
其他事项
你在 MessageHolder
中存储的消息类型为 LogMessage
。它的基本属性是 Severity
、Context
、Format
和 Args
,还有一个 Formatted
属性,用于本地化和格式化 format 字符串(不包括 Severity
和 Context
)。但是,ToString()
方法通过调用 MessageSink.FormatMessage()
将所有四个元素合并到一个消息中。
LogException
异常有四个参数,就像消息接收器一样
new LogException(severity, context, format, args)
severity
参数是可选的;默认值为 Severity.Error
。
LogException
有一个类型为 LogMessage
的 Msg
属性,用于存储此信息。(它不存储在 Exception
的 Data
字典中,因为Data 中的项必须是可序列化的,而上下文对象不一定是可序列化的。)
下载
在 Visual Studio 中,你可以使用 NuGet 安装 Loyc.Essentials,其中包含此处描述的所有内容。源代码位于此处,主页位于此处。感谢阅读!