.NET Core日志系统: 针对不同渠道的日志输出!
我们在上一系列中介绍了.NET Core中四种常用的诊断日志框架,除此之外,我们还有太多第三方框架可供选择,比如Log4Net、NLog和Serilog 等,.NET Core提供了独立的日志模型使我们可以采用统一的API来完成针对日志记录的编程,我们同时也可以利用其扩展点对这个模型进行定制,比如可以将上述这些成熟的日志框架整合到我们的应用中。
01
日志模型三要素
针对日志的编程模型主要涉及到ILogger、ILoggerFactory和ILoggerProvider这三个核心对象,这三个核心对象以及它们之间的关系是我们下一节着重介绍的内容,目前我们只需要对它们有一个大致的了解。应用程序通过ILoggerFactory创建的ILogger对象来记录日志,而注册到ILoggerFactory之上的一个或者多个ILoggerProvider则完成针对各种渠道的日志输出。
日志模型承载于NuGet包“Microsoft.Extensions.Logging”这个NuGet包中,但是上述的这三个接口定义在NuGet包“Microsoft.Extensions.Logging.Abstractions”NuGet包,至于针对接口ILoggerProvider的具体实现类型则由对应的NuGet包来提供。
02
日志事件
一般来说,写入的每一条日志消息总是针对某个具体的事件(Event),所以每一条日志消息(Log Entry或者Log Message)都具有一个标识事件的ID。日志事件本身重要程度或者反映出的问题严重性不尽相同,这一点则通过日志消息的等级来标识,英文的“日志等级”可以表示成“Log Level”、“Log Verbosity Level”或者是“Log Severity Level”等。日志事件ID和日志等级分别通过如下所示的两个类型来表示。
public struct EventId
{
public int Id { get; }
public string Name { get; }
public EventId(int id, string name = null);
public static implicit operator EventId(int i);
public override string ToString();
}
public enum LogLevel
{
Trace,
Debug,
Information,
Warning,
Error,
Critical,
None
}
表示EventId的结构体分别通过只读属性Id和Name表示事件的ID和名称,前者是必需的,后者是可选的。EventId重写了ToString方法,如果表示事件名称的Name属性存在,那么该方法会将该名称作为返回值,否则这个方法会返回其Id属性。从上面提供的代表片段还可以看出EventId定义了针对整型的隐式转化器,所以任何涉及到使用EventId的地方都可以直接用表示事件ID的整数来替换。
如果忽略选项“None”,枚举LogLevel实际上为我们定义了6个日志等级,枚举成员的顺序体现了等级的高低,所以Trace最低,Critical最高。表1给出了这6种日志等级的基本说明,我们可以在发送日志事件的时候可以根据它来决定当前日志消息应该采用何种等级。
表1 日志等级
03
针对控制台和调试器的输出
在对日志的基本编程模型有了大致了解之后,我们接下来通过一个简单的实例来演示如何将具有不同等级的日志消息输出两种不同的渠道,其中一种是直接将格式化的日志消息输出到当前控制台,另一种则是将日志作为调试信息提供给附加到当前进程的调试器(Debugger)。支持这两种日志输出的ILoggerProvider分别为ConsoleLoggerProvider和DebugLoggerProvider,它们分别由如下两个NuGet包来提供:
Microsoft.Extensions.Logging.Console
Microsoft.Extensions.Logging.Debug
我们创建了一个.NET Core控制台应用,在添加了如上两个NuGet包的依赖之后,我们编写了如下这段程序。我们首先创建了一个LoggerFactory对象(该类型是对ILoggerFactory接口的默认实现),并分别调用扩展方法AddConsole和AddDebug完成对ConsoleLoggerProvider和DebugLoggerProvider的注册。我们接下来调用CreateLogger方法创建出用来分发日志事件的ILogger对象,该方法提供的参数表示对日志进行归类的类别,我们倾向于将当前写入日志的组件、服务或者类型名称作为日志类别,所我们指定的是当前类型的全名(“App.Program”)。
namespace App
{
class Program
{
static void Main()
{
var logger = new LoggerFactory()
.AddConsole()
.AddDebug()
.CreateLogger("App.Program");
var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel));
levels = levels.Where(it => it != LogLevel.None).ToArray();
var eventId = 1;
Array.ForEach(levels, level => logger.Log(level, eventId++,
"This is a/an {0} log message.", level));
Console.Read();
}
}
}
我们针对每个有效的日志等级通过调用ILogger的Log方法分发了6个日志事件,事件的ID分别被设置成1到6的整数。与上一章介绍的基于TraceSource的跟踪日志框架类似,分发的日志事件的内容负载最终体现为一个格式化的字符串,所以我们在调用Log方法的时候通过指定一个包含占位符(“{0}”)的消息模板和对应参数的方式来格式化最终输出的消息内容。
由于ConsoleLoggerProvider被注册到创建ILogger的LoggerFactory对象上,所以在执行这个实例程序之后,我们可以直接在 控制台上看到输出的日志内容。如图1所示,格式化的日志消息不仅仅包含格式化的消息内容,日志的等级、类别和事件ID同样包含其中。
图1 针对控制台和Debugger的日志输出
不仅如此,表示日志等级的文字还会采用不同的前景色和背景色来显示。由于涉及到针对分发日志事件的过滤,对于由ILogger对象发出的针对不同等级的6个日志事件,只有4条日志被真正地输出到控制台上。由于LoggerFactory上还注册了另一个DebugLoggerProvider对象,它会完成针对调试器的日志输出,所以Visual Studio的调试输出窗口也会输出这四个日志消息。
上面的实例通过指定日志类别(“App.Program”)调用LoggerFactory的CreateLogger方法创建的是一个ILogger对象,实际上我们还可以调用另一个泛型的CreateLogger<T>方法创建一个ILogger<T>对象来分发日志事件。如果调用这个方法,我们就不需要额外提供日志类别了,因为创建出来的ILogger<T>对象将会使用泛型类型的全名(命名空间+类型名称)作为日志类别。
除此之外,作为日志负载内容的消息模板除了采用“{0}”、“{1}”、…、“{N}”这样的占位符之外,占位符还可以使用任意字符串来表示。如果我们将上面的程序改写成如下的程序,最终写入到控制台和调试输出窗口的内容是完全一样的。
namespace App
{
class Program
{
static void Main()
{
var logger = new LoggerFactory()
.AddConsole()
.AddDebug()
.CreateLogger<Program>();
var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel));
levels = levels.Where(it => it != LogLevel.None).ToArray();
var eventId = 1;
Array.ForEach(levels, level => logger.Log(level, eventId++,
"This is a/an {level} log message.", level));
Console.Read();
}
}
}
04
针对TraceSource和EventSource的输出
除了控制台和调试器这两种日志输出渠道之外,日志框架还通过一系列预定义的ILoggerProvider实现针对其他输出渠道的支持。我们在上一系列重点介绍了针对TraceSource和EventSource的日志框架,实际上它们也可以直接作为日志消息的输出渠道,针对这两种日志输出渠道的支持分别是通过TraceSourceLoggerProvider和EventSourceLoggerProvider提供的,它们所在的NuGet包分别是:
Microsoft.Extensions.Logging.TraceSource
Microsoft.Extensions.Logging.EventSource
接下来我们就来演示一下如何利用注册的TraceSourceLoggerProvider和EventSourceLoggerProvider来记录日志。在为项目添加了如上两个NuGet包的依赖之后,我们将上面的代码改写成如下的形式。为了捕捉由EventSource分发的日志事件,我们自定义了一个FoobarEventListener类型。在程序启动的时候,我们创建了这个FoobarEventListener对象并注册了EventSourceCreated和EventWritten事件。被EventSourceLoggerProvider用来处理日志的EventSource被命名为“Microsoft-Extensions-Logging” ,所以我们可以根据这个名称来过滤作为注册目标的EventSource。
class Program
{
static void Main()
{
var listener = new FoobarEventListener();
listener.EventSourceCreated += (sender, args) =>
{
if (args.EventSource.Name == "Microsoft-Extensions-Logging")
{
listener.EnableEvents(args.EventSource, EventLevel.LogAlways);
}
};
listener.EventWritten += (sender, args) =>
{
if (args.EventName == "FormattedMessage")
{
var payload = args.Payload;
var payloadNames = args.PayloadNames;
var indexOfLevel = payloadNames.IndexOf("Level");
var indexOfCategory = args.PayloadNames.IndexOf("LoggerName");
var indexOfEventId = args.PayloadNames.IndexOf("EventId");
var indexOfMessage = args.PayloadNames.IndexOf("FormattedMessage");
Console.WriteLine($"{payload[indexOfLevel],-11}:{payload[indexOfCategory]}[{payload[indexOfEventId]}]");
Console.WriteLine($"{"",-13}{payload[indexOfMessage]}");
}
};
var logger = new LoggerFactory()
.AddTraceSource(new SourceSwitch("default","All"),new DefaultTraceListener { LogFileName = "trace.log"})
.AddEventSourceLogger()
.CreateLogger<Program>();
var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel));
levels = levels.Where(it => it != LogLevel.None).ToArray();
var eventId = 1;
Array.ForEach(levels, level => logger.Log(level, eventId++, "This is a/an {level} log message.", level));
Console.Read();
}
public class FoobarEventListener : EventListener { }
}
在日志事件分发处理过程中,EventSource会针对负载内容的不同输出结构分发多个日志事件,上面代码注册的是一个名为“FormattedMessage”的事件,它的内容负载会包含格式后的日志消息。对于该日志事件的内容负载,除了作为针对格式化消息的负载成员之外,还包括日志等级、事件ID和日志类别等,我们在EventWritten事件处理程序中将它们从负载对象中提取出来,在经过相应格式化之后,这些数据最终被打印到控制台上。
针对TraceSourceLoggerProvider和EventSourceLoggerProvider的注册通过调用ILoggerFactory的两个对应的扩展方法AddTraceSource和AddEventSourceLogger来完成。当我们在调用AddTraceSource方法注册TraceSourceLoggerProvider的时候,我们提供了两个参数,前者是作为全局过滤器的SourceSwitch对象,后者则是注册的TraceListener。由于我们提供的DefaultTraceListener指定了日志文件的路径,所以输出的日志消息最终会被写入指定的文件中。
日志事件的分发与前面演示的实例没有什么两样,我们依然分发这针对6种不同等级的日志事件。程序运行后,由TraceSourceLoggerProvider提供的TraceSource会将日志消息输出到指定的文件中( “trace.log” ),而EventSourceLoggerProvider会利用EventSource对象对原始的日志事件作相应处理之后按照自己的方式对日志事件进行继续分发,后者会被我们注册的FoobarEventListener捕获并将格式化的内容打印到控制台上。图2所示的就是输出到日志文件和控制台上的日志内容。
图2 针对TraceSource和EventSource的日志输出
副业刚需:这个开源小程序外卖红包项目,有人月入5000+!
卧槽!微信可以改彩色昵称了!!!