详解.Net Core中的日志组件(Logging)
1、介绍
logging组件是微软实现的日志记录组件包括控制台(console)、调试(debug)、事件日志(eventlog)和tracesource,但是没有实现最常用用的文件记录日志功能(可以用其他第三方的如nlog、log4net。之前写过nlog使用的文章)。
2、默认配置
新建.net core web api项目,添加下面代码。
[route("api/[controller]")] public class valuescontroller : controller { ilogger<valuescontroller> logger; //构造函数注入logger public valuescontroller(ilogger<valuescontroller> logger) { this.logger = logger; } [httpget] public ienumerable<string> get() { logger.logwarning("warning"); return new string[] { "value1", "value2" }; } }
运行结果如下:
我刚开始接触的时候,我就有一个疑问我根本没有配置关于logger的任何代码,仅仅写了注入,为什么会起作用呢?最后我发现其实是在program类中使用了微软默认的配置。
public class program { public static void main(string[] args) { buildwebhost(args).run(); } public static iwebhost buildwebhost(string[] args) => webhost.createdefaultbuilder(args)//在这里使用了默认配置 .usestartup<startup>() .build(); }
下面为createdefaultbuilder方法的部分源码,整个源码在 https://github.com/aspnet/metapackages ,可以看出在使用模板创建项目的时候,默认添加了控制台和调试日志组件,并从appsettings.json中读取配置。
builder.usekestrel((buildercontext, options) => { options.configure(buildercontext.configuration.getsection("kestrel")); }) .configureappconfiguration((hostingcontext, config) => { var env = hostingcontext.hostingenvironment; //加载appsettings.json文件 使用模板创建的项目,会生成一个配置文件,配置文件中包含logging的配置项 config.addjsonfile("appsettings.json", optional: true, reloadonchange: true) .addjsonfile($"appsettings.{env.environmentname}.json", optional: true, reloadonchange: true); ....... }) .configurelogging((hostingcontext, logging) => { //从appsettings.json中获取logging的配置 logging.addconfiguration(hostingcontext.configuration.getsection("logging")); //添加控制台输出 logging.addconsole(); //添加调试输出 logging.adddebug(); })
3、建立自己的logging配置
首先修改program类
public class program { public static void main(string[] args) { //指定配置文件路径 var config = new configurationbuilder() .setbasepath(directory.getcurrentdirectory())//设置基础路径 .addjsonfile($"appsettings.json", true, true)//加载配置文件 .addjsonfile($"appsettings.{environmentname.development}.json", true, true) .build(); var host = new webhostbuilder() .usekestrel() .usestartup<startup>() .usecontentroot(directory.getcurrentdirectory()) .useconfiguration(config)//使用配置 .useurls(config["appsettings:url"])//从配置中读取 程序监听的端口号 .useenvironment(environmentname.development)//如果加载了多个环境配置,可以设置使用哪个配置 一般有测试环境、正式环境 //.configurelogging((hostingcotext, logging) => //第一种配置方法 直接在webhostbuilder建立时配置 不需要修改下面的startup代码 //{ // logging.addconfiguration(hostingcotext.configuration.getsection("logging")); // logging.addconsole(); //}) .build(); host.run(); } }
修改startup类如下面,此类的执行顺序为 startup构造函数 > configureservices > configure
public class startup { public iconfiguration configuration { get; private set; } public ihostingenvironment hostingenvironment { get; private set; } //在构造函数中注入 ihostingenvironment和iconfiguration,配置已经在program中设置了,注入后就可以获取配置文件的数据 public startup(ihostingenvironment env, iconfiguration config) { hostingenvironment = env; configuration = config; } public void configureservices(iservicecollection services) { services.addmvc(); //第二种配置 也可以这样加上日志功能,不用下面的注入 //services.addlogging(builder => //{ // builder.addconfiguration(configuration.getsection("logging")) // .addconsole(); //}); } //注入iloggerfactory public void configure(iapplicationbuilder app, ihostingenvironment env, iloggerfactory loggerfactory) { if (env.isdevelopment()) { app.usedeveloperexceptionpage(); } //第三种配置 注入ilogggerfactory,然后配置参数 //添加控制台输出 loggerfactory.addconsole(configuration.getsection("logging")); //添加调试输出 loggerfactory.adddebug(); app.usemvc(); } }
这种结构就比较清晰明了。
4、logging源码解析
三种配置其实都是为了注入日志相关的服务,但是调用的方法稍有不同。现在我们以第二种配置来详细看看其注入过程。首先调用addlogging方法,其实现源码如下:
public static iservicecollection addlogging(this iservicecollection services, action<iloggingbuilder> configure) { services.addoptions();//这里会注入最基础的5个服务 option相关服务只要是跟配置文件相关,通过option服务获取相关配置文件参数参数 services.tryadd(servicedescriptor.singleton<iloggerfactory, loggerfactory>()); services.tryadd(servicedescriptor.singleton(typeof(ilogger<>), typeof(logger<>))); services.tryaddenumerable(servicedescriptor.singleton<iconfigureoptions<loggerfilteroptions>>(new defaultloggerlevelconfigureoptions(loglevel.information))); configure(new loggingbuilder(services)); return services; }
接着会调用addconfiguration
public static iloggingbuilder addconfiguration(this iloggingbuilder builder, iconfiguration configuration) { builder.addconfiguration(); //下面为addconfiguration的实现 public static void addconfiguration(this iloggingbuilder builder) { builder.services.tryaddsingleton<iloggerproviderconfigurationfactory, loggerproviderconfigurationfactory>(); builder.services.tryaddsingleton(typeof(iloggerproviderconfiguration<>), typeof(loggerproviderconfiguration<>)); } builder.services.addsingleton<iconfigureoptions<loggerfilteroptions>>(new loggerfilterconfigureoptions(configuration)); builder.services.addsingleton<ioptionschangetokensource<loggerfilteroptions>>(new configurationchangetokensource<loggerfilteroptions>(configuration)); builder.services.addsingleton(new loggingconfiguration(configuration)); return builder; }
下面来看打印日志的具体实现:
public void log<tstate>(loglevel loglevel, eventid eventid, tstate state, exception exception, func<tstate, exception, string> formatter) { var loggers = loggers; list<exception> exceptions = null; //loggers为loggerinformation数组,如果你在startup中添加了console、deubg日志功能了,那loggers数组值有2个,就是它俩。 foreach (var loggerinfo in loggers) { //循环遍历每一种日志打印,如果满足些日子的条件,才执行打印log方法。比如某一个日志等级为info, //但是console配置的最低打印等级为warning,debug配置的最低打印等级为debug //则console中不会打印,debug中会被打印 if (!loggerinfo.isenabled(loglevel)) { continue; } try { //每一种类型的日志,对应的打印方法不同。执行对应的打印方法 loggerinfo.logger.log(loglevel, eventid, state, exception, formatter); } catch (exception ex) { if (exceptions == null) { exceptions = new list<exception>(); } exceptions.add(ex); } } }
下面具体看一下console的打印实现:
首先consolelogger实现了ilogger的log方法,并在方法中调用writemessage方法
public void log<tstate>(loglevel loglevel, eventid eventid, tstate state, exception exception, func<tstate, exception, string> formatter) { //代码太多 我就省略一些判空代码 var message = formatter(state, exception); if (!string.isnullorempty(message) || exception != null) { writemessage(loglevel, name, eventid.id, message, exception); } } public virtual void writemessage(loglevel loglevel, string logname, int eventid, string message, exception exception) { ....... if (logbuilder.length > 0) { var haslevel = !string.isnullorempty(loglevelstring); //这里是主要的代码实现,可以看到,并没有写日志的代码,而是将日志打入到一个blockingcollection<logmessageentry>队列中 _queueprocessor.enqueuemessage(new logmessageentry() { message = logbuilder.tostring(), messagecolor = defaultconsolecolor, levelstring = haslevel ? loglevelstring : null, levelbackground = haslevel ? loglevelcolors.background : null, levelforeground = haslevel ? loglevelcolors.foreground : null }); } ...... }
下面看日志被放入队列后的具体实现:
public class consoleloggerprocessor : idisposable { private const int _maxqueuedmessages = 1024; private readonly blockingcollection<logmessageentry> _messagequeue = new blockingcollection<logmessageentry>(_maxqueuedmessages); private readonly thread _outputthread; public iconsole console; public consoleloggerprocessor() { //在构造函数中启动一个线程,执行processlogqueue方法 //从下面processlogqueue方法可以看出,是循环遍历集合,将集合中的数据打印 _outputthread = new thread(processlogqueue) { isbackground = true, name = "console logger queue processing thread"public virtual void enqueuemessage(logmessageentry message) { if (!_messagequeue.isaddingcompleted) { try { _messagequeue.add(message); return; } catch (invalidoperationexception) { } } writemessage(message); } internal virtual void writemessage(logmessageentry message) { if (message.levelstring != null) { console.write(message.levelstring, message.levelbackground, message.levelforeground); } console.write(message.message, message.messagecolor, message.messagecolor); console.flush(); } private void processlogqueue() { try { //getconsumingenumerable()方法比较特殊,当集合中没有值时,会阻塞自己,一但有值了,知道集合中又有元素继续遍历 foreach (var message in _messagequeue.getconsumingenumerable()) { writemessage(message); } } catch { try { _messagequeue.completeadding(); } catch { } } } }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
下一篇: 前端MVVM框架解析之双向绑定