欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

动手造*:写一个日志框架

程序员文章站 2023-11-02 15:04:52
日志组件有很多,比如 `log4net` / `nlog` / `serilog` / `microsoft.extensions.logging` 等,如何在切换日志组件的时候做到不用修改代码,只需要切换不同的 `loggingProvider` 就可以了,最低成本的降低切换日志框架的成本,处于这... ......

动手造*:写一个日志框架

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 里的一些设计用在自己的日志框架里,比如说:

  1. serilog 的扩展叫做 sink,日志输出的地方,serilog 自定义一个 sink,很简单只需要实现一个接口,不需要再实现一个 logger,从这点来说,我觉得 serilog 比微软的日志框架更加优秀,而且 logevent 使得日志更方便的进行批量操作,有需要的可以了解一下 serilogperiodbatching

    /// <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);
    }
  2. serilog 可以自定义一些 enricher,以此来丰富记录的日志内容,比如日志的请求上下文,日志的环境等,也可以是一些固定的属性信息

  3. messagetemplate,其实微软的日志框架中也有类似的概念,只不过很不明显,用 serilog 之前我也很少用,微软的日志框架可以这样用 logger.loginfo("hello {name}", "world") 这样的写法其实就可以把第一个参数当作是 messagetemplate 或者它内部的叫法 format

鉴于这么多好处,于是打算将这些功能引入到我的日志框架中

v3

引入 loggingevent

说干就干,首先要引入一个 loghelperloggingevent,对应的 seriloglogevent,定义如下:

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 之类的外部的日志框架的话,我觉得还是可以做的,可以提高效率,目前主要用 seriloglog4net,暂时不更新了,就先这样吧

下一版本要解决的事情

  • ilogprovider 记录日志返回一个 task 感觉有些鸡肋,没太大意义,后面再改一下吧
  • serilog 的 filter 是基于 logevent 的,后面看是否需要改一下,基于 logevent 的话更简洁,而且可以根据 logevent 内的 properties 做过滤,所以 addfilter 的api 可以更新一下 addfilter(func<loghelperloggingevent, bool> filter)

reference