动手造*:写一个日志框架
动手造*:写一个日志框架
intro
日志框架有很多,比如 log4net
/ nlog
/ serilog
/ microsoft.extensions.logging
等,如何在切换日志框架的时候做到不用修改代码,只需要切换不同的 loggingprovider
就可以了,最低成本的降低切换日志框架的成本,处于这个考虑自己写了一个日志框架,为不同的日志框架写一个适配,需要用到什么日志框架,配置一下就可以了,业务代码无需变动。
v0
最初的日志强依赖于 log4net,log4net 是我使用的第一个日志框架,所以很长一段时间都在使用它来做日志记录,但是由于是强依赖,在想换日志框架时就会很难受,大量代码要改动,不符合开放封闭的基本原则,于是就有了第一个版本的日志。
v1
第一版的日志参考了微软的日志框架的实现,大概结构如下:
public interface iloghelperlogfactory { ilogger createlogger(string categoryname); bool addprovider(iloghelperprovider provider); } public interface iloghelperlogger { bool isenabled(loghelperloglevel loglevel); void log(loghelperloglevel loglevel, exception exception, string message); } public enum loghelperloglevel { /// <summary> /// all logging levels /// </summary> all = 0, /// <summary> /// a trace logging level /// </summary> trace = 1, /// <summary> /// a debug logging level /// </summary> debug = 2, /// <summary> /// a info logging level /// </summary> info = 4, /// <summary> /// a warn logging level /// </summary> warn = 8, /// <summary> /// an error logging level /// </summary> error = 16, /// <summary> /// a fatal logging level /// </summary> fatal = 32, /// <summary> /// none /// </summary> none = 64 } public interface iloghelperprovider { iloghelperlogger createlogger(string categoryname); }
为了方便 logger 的使用,定义了一些扩展方法,使得可以直接使用 logger.info
/logger.error
等方法,扩展定义如下:
public static void log(this iloghelperlogger logger, loghelperlevel loggerlevel, string msg) => logger.log(loggerlevel, null, msg); #region info public static void info(this iloghelperlogger logger, string msg, params object[] parameters) { if (parameters == null || parameters.length == 0) { logger.log(loghelperlevel.info, msg); } else { logger.log(loghelperlevel.info, null, msg.formatwith(parameters)); } } public static void info(this iloghelperlogger logger, exception ex, string msg) => logger.log(loghelperlevel.info, ex, msg); public static void info(this iloghelperlogger logger, exception ex) => logger.log(loghelperlevel.info, ex, ex?.message); #endregion info // ...其他的类似,这里就不详细展开了
如果要自定义的日志记录的话,就实现一个 iloghelperprovider
即可,实现一个 iloghelperprovider
就要实现一个 iloghelperlogger
,原本强依赖的 log4net 可以实现一个 log4netloghelperprovider
,这样换别的日志框架的时候只需要实现对应的 iloghelperprovider
即可,但是从功能性上来说还是很弱的
如果想要某些日志不记录,比如说,debug 级别的日志不记录,比如说某一个 logger 下只记录 error 级别的日志,现在是有些吃力,只能通过 log4net 的配置来限制了,于是就有了第二个版本,增加了 loggingfilter
可以针对 provider/logger/loglevel/exception 来设置 filter,过滤不需要记录的日志,这也是参考了微软的日志框架的 filter,但是实现不太一样,有兴趣的小伙伴可以自己深入研究一下。
v2
v2 版,在 ilogfactory
的接口上增加了 addfilter
的方法,定义如下:
/// <summary> /// add logs filter /// </summary> /// <param name="filterfunc">filterfunc, logprovidertype/categoryname/exception, whether to write log</param> bool addfilter(func<type, string, loghelperloglevel, exception, bool> filterfunc);
然后定义了一些扩展方法来方便使用:
public static iloghelperfactory withminimumlevel(this iloghelperfactory loghelperfactory, loghelperlevel loglevel) { return loghelperfactory.withfilter(level => level >= loglevel); } public static iloghelperfactory withfilter(this iloghelperfactory loghelperfactory, func<loghelperlevel, bool> filterfunc) { loghelperfactory.addfilter((type, categoryname, loglevel, exception) => filterfunc.invoke(loglevel)); return loghelperfactory; } public static iloghelperfactory withfilter(this iloghelperfactory loghelperfactory, func<string, loghelperlevel, bool> filterfunc) { loghelperfactory.addfilter((type, categoryname, loglevel, exception) => filterfunc.invoke(categoryname, loglevel)); return loghelperfactory; } public static iloghelperfactory withfilter(this iloghelperfactory loghelperfactory, func<type, string, loghelperlevel, bool> filterfunc) { loghelperfactory.addfilter((type, categoryname, loglevel, exception) => filterfunc.invoke(type, categoryname, loglevel)); return loghelperfactory; } public static iloghelperfactory withfilter(this iloghelperfactory loghelperfactory, func<type, string, loghelperlevel, exception, bool> filterfunc) { loghelperfactory.addfilter(filterfunc); return loghelperfactory; }
这样就方便了我们只想定义针对 logger 的 filter 以及 provider 的 filter,不必所有参数都用到,logging filter 现在已经实现了,此时已经使用了 serilog
做日志记录有一段时间,感觉 serilog
里的一些设计很优秀,很优雅,于是想把 serilog
里的一些设计用在自己的日志框架里,比如说:
-
serilog
的扩展叫做sink
,日志输出的地方,serilog
自定义一个sink
,很简单只需要实现一个接口,不需要再实现一个 logger,从这点来说,我觉得serilog
比微软的日志框架更加优秀,而且logevent
使得日志更方便的进行批量操作,有需要的可以了解一下serilog
的periodbatching
/// <summary> /// a destination for log events. /// </summary> public interface ilogeventsink { /// <summary> /// emit the provided log event to the sink. /// </summary> /// <param name="logevent">the log event to write.</param> void emit(logevent logevent); }
serilog
可以自定义一些enricher
,以此来丰富记录的日志内容,比如日志的请求上下文,日志的环境等,也可以是一些固定的属性信息messagetemplate
,其实微软的日志框架中也有类似的概念,只不过很不明显,用serilog
之前我也很少用,微软的日志框架可以这样用logger.loginfo("hello {name}", "world")
这样的写法其实就可以把第一个参数当作是messagetemplate
或者它内部的叫法format
鉴于这么多好处,于是打算将这些功能引入到我的日志框架中
v3
引入 loggingevent
说干就干,首先要引入一个 loghelperloggingevent
,对应的 serilog
的 logevent
,定义如下:
public class loghelperloggingevent : icloneable { public string categoryname { get; set; } public datetimeoffset datetime { get; set; } public string messagetemplate { get; set; } public string message { get; set; } public exception exception { get; set; } public loghelperloglevel loglevel { get; set; } public dictionary<string, object> properties { get; set; } public loghelperloggingevent copy => (loghelperloggingevent)clone(); public object clone() { var newevent = (loghelperloggingevent)memberwiseclone(); if (properties != null) { newevent.properties = new dictionary<string, object>(); foreach (var property in properties) { newevent.properties[property.key] = property.value; } } return newevent; } }
event 里定义了一个 properties 的字典用来丰富日志的内容,另外实现了 icloneable
接口,方便对对象的拷贝,为了强类型,增加了一个 copy
的方法,返回一个强类型的对象
改造 logprovider
为了减少扩展一个 ilogprovider
的复杂性,我们要对 ilogprovider
做一个简化,只需要像扩展 serilog
的 sink 一样记录日志即可,不需要关心是否要创建 logger
改造后的定义如下:
public interface iloghelperprovider { task log(loghelperloggingevent loggingevent); }
(这里返回了一个 task,可能返回类型是 void 就足够了,看自己的需要)
这样在实现 logprovider
的时候只需要实现这个接口就可以了,不需要再实现一个 logger 了
增加 enricher
enricher
定义:
public interface iloghelperloggingenricher { void enrich(loghelperloggingevent loggingevent); }
内置了一个 propertyenricher
,方便添加一些简单的属性
internal class propertyloggingenricher : iloghelperloggingenricher { private readonly string _propertyname; private readonly func<loghelperloggingevent, object> _propertyvaluefactory; private readonly bool _overwrite; private readonly func<loghelperloggingevent, bool> _logpropertypredict = null; public propertyloggingenricher(string propertyname, object propertyvalue, bool overwrite = false) : this(propertyname, (loggingevent) => propertyvalue, overwrite) { } public propertyloggingenricher(string propertyname, func<loghelperloggingevent, object> propertyvaluefactory, bool overwrite = false) : this(propertyname, propertyvaluefactory, null, overwrite) { } public propertyloggingenricher(string propertyname, func<loghelperloggingevent, object> propertyvaluefactory, func<loghelperloggingevent, bool> logpropertypredict, bool overwrite = false) { _propertyname = propertyname; _propertyvaluefactory = propertyvaluefactory; _logpropertypredict = logpropertypredict; _overwrite = overwrite; } public void enrich(loghelperloggingevent loggingevent) { if (_logpropertypredict?.invoke(loggingevent) != false) { loggingevent.addproperty(_propertyname, _propertyvaluefactory, _overwrite); } } }
为 ilogfactory
增加一个 addenricher
的方法
/// <summary> /// add log enricher /// </summary> /// <param name="enricher">log enricher</param> /// <returns></returns> bool addenricher(iloghelperloggingenricher enricher);
这样我们在记录日志的时候就可以通过这些 enricher 丰富 loggingevent
中的 properties 了
为了方便 property 的操作,我们增加了一些扩展方法:
public static iloghelperfactory withenricher<tenricher>(this iloghelperfactory loghelperfactory, tenricher enricher) where tenricher : iloghelperloggingenricher { loghelperfactory.addenricher(enricher); return loghelperfactory; } public static iloghelperfactory withenricher<tenricher>(this iloghelperfactory loghelperfactory) where tenricher : iloghelperloggingenricher, new() { loghelperfactory.addenricher(new tenricher()); return loghelperfactory; } public static iloghelperfactory enrichwithproperty(this iloghelperfactory loghelperfactory, string propertyname, object value, bool overwrite = false) { loghelperfactory.addenricher(new propertyloggingenricher(propertyname, value, overwrite)); return loghelperfactory; } public static iloghelperfactory enrichwithproperty(this iloghelperfactory loghelperfactory, string propertyname, func<loghelperloggingevent> valuefactory, bool overwrite = false) { loghelperfactory.addenricher(new propertyloggingenricher(propertyname, valuefactory, overwrite)); return loghelperfactory; } public static iloghelperfactory enrichwithproperty(this iloghelperfactory loghelperfactory, string propertyname, object value, func<loghelperloggingevent, bool> predict, bool overwrite = false) { loghelperfactory.addenricher(new propertyloggingenricher(propertyname, e => value, predict, overwrite)); return loghelperfactory; } public static iloghelperfactory enrichwithproperty(this iloghelperfactory loghelperfactory, string propertyname, func<loghelperloggingevent, object> valuefactory, func<loghelperloggingevent, bool> predict, bool overwrite = false) { loghelperfactory.addenricher(new propertyloggingenricher(propertyname, valuefactory, predict, overwrite)); return loghelperfactory; }
messagetemplate
从上面的 loggingevent
中已经增加了 messagetemplate
,于是我们引入了微软日志框架中日志的格式化,将 messagetemplate 和 parameters 转换成 message 和 properties,具体参考 https://github.com/weihanli/weihanli.common/blob/276cc49cfda511f9b7b3bb8344ee52441c4a3b23/src/weihanli.common/logging/loggingformatter.cs
internal struct formattedlogvalue { public string msg { get; set; } public dictionary<string, object> values { get; set; } public formattedlogvalue(string msg, dictionary<string, object> values) { msg = msg; values = values; } } internal static class loggingformatter { public static formattedlogvalue format(string msgtemplate, object[] values) { if (values == null || values.length == 0) return new formattedlogvalue(msgtemplate, null); var formatter = new logvaluesformatter(msgtemplate); var msg = formatter.format(values); var dic = formatter.getvalues(values) .todictionary(x => x.key, x => x.value); return new formattedlogvalue(msg, dic); } }
这样我们就可以支持 messagetemplate 了,然后来改造一下我们的 logger
public interface iloghelperlogger { void log(loghelperloglevel loglevel, exception exception, string messagetemplate, params object[] parameters); bool isenabled(loghelperloglevel loglevel); }
与上面不同的是,我们增加了 parameters
再来更新一下我们的扩展方法,上面的扩展方法是直接使用 string.format
的方式的格式化的,我们这里要更新一下
public static void info(this iloghelperlogger logger, string msg, params object[] parameters) { logger.log(loghelperloglevel.info, null, msg, parameters); } public static void info(this iloghelperlogger logger, exception ex, string msg) => logger.log(loghelperloglevel.info, ex, msg); public static void info(this iloghelperlogger logger, exception ex) => logger.log(loghelperloglevel.info, ex, ex?.message);
至此,功能基本完成,但是从 api 的角度来说,感觉现在的 ilogfactory
太重了,这些 addprovider
/addenricher
/addfilter
都应该属性 ilogfactory
的内部属性,通过配置来完成,不应该成为它的接口方法,于是就有了下一版
v4
这一版主要是引入了 loggingbuilder
, 通过 loggingbuilder
来配置内部的 logfactory
所需要的 provider
/enricher
/filter
,原来他们的配置方法和扩展方法均变成iloghelperloggingbuilder
public interface iloghelperloggingbuilder { /// <summary> /// adds an iloghelperprovider to the logging system. /// </summary> /// <param name="provider">the iloghelperprovider.</param> bool addprovider(iloghelperprovider provider); /// <summary> /// add log enricher /// </summary> /// <param name="enricher">log enricher</param> /// <returns></returns> bool addenricher(iloghelperloggingenricher enricher); /// <summary> /// add logs filter /// </summary> /// <param name="filterfunc">filterfunc, logprovidertype/categoryname/exception, whether to write log</param> bool addfilter(func<type, string, loghelperloglevel, exception, bool> filterfunc); ///// <summary> ///// config period batching ///// </summary> ///// <param name="period">period</param> ///// <param name="batchsize">batchsize</param> //void periodbatchingconfig(timespan period, int batchsize); /// <summary> /// build for logfactory /// </summary> /// <returns></returns> iloghelperfactory build(); }
增加 logging 的配置:
public static class loghelper { private static iloghelperfactory logfactory { get; private set; } = nullloghelperfactory.instance; public static void configurelogging(action<iloghelperloggingbuilder> configureaction) { var loggingbuilder = new loghelperloggingbuilder(); configureaction?.invoke(loggingbuilder); logfactory = loggingbuilder.build(); } public static iloghelperlogger getlogger<t>() => logfactory.getlogger(typeof(t)); public static iloghelperlogger getlogger(type type) => logfactory.getlogger(type); public static iloghelperlogger getlogger(string categoryname) { return logfactory.createlogger(categoryname); } }
最后的使用方式:
internal class loggingtest { private static readonly iloghelperlogger logger = loghelper.getlogger<loggingtest>(); public static void maintest() { var abc = "1233"; loghelper.configurelogging(builder => { builder .addlog4net() //.addserilog(loggerconfig => loggerconfig.writeto.console()) .withminimumlevel(loghelperloglevel.info) .withfilter((category, level) => level > loghelperloglevel.error && category.startswith("system")) .enrichwithproperty("entry0", applicationhelper.applicationname) .enrichwithproperty("entry1", applicationhelper.applicationname, e => e.loglevel >= loghelperloglevel.error)// 当 loglevel 是 error 及以上级别时才增加 property ; }); logger.debug("12333 {abc}", abc); logger.trace("122334334"); logger.info($"122334334 {abc}"); logger.warn("12333, err:{err}", "hahaha"); logger.error("122334334"); logger.fatal("12333"); } }
more
增加 loggingevent
还想做一个批量提交日志,如上面定义的 periodbatchingconfig
一样,批量同步到 provider 但是实际使用下来,有些 provider 不支持设置日志的时间,时间是内部记录的,这样一来日志记录的时间就不准了,而且大多都不支持批量写日志,所以后面放弃了,但是如果只是用自己的扩展,不用 log4net 之类的外部的日志框架的话,我觉得还是可以做的,可以提高效率,目前主要用 serilog
和 log4net
,暂时不更新了,就先这样吧
下一版本要解决的事情
-
ilogprovider
记录日志返回一个 task 感觉有些鸡肋,没太大意义,后面再改一下吧 -
serilog
的 filter 是基于logevent
的,后面看是否需要改一下,基于 logevent 的话更简洁,而且可以根据 logevent 内的 properties 做过滤,所以 addfilter 的api 可以更新一下addfilter(func<loghelperloggingevent, bool> filter)
reference
上一篇: 昌鱼价格,常见的鲳鱼品种你都知道多少