背景
最近在对logrus框架进行一些封装,以便更好的应用到项目中的时候遇到了一些问题,在分析了logrus的源码之后找到了解决方案。因此写一篇文章剖析一下logrus的源码,及实践过程中遇到的问题和解决方案
logrus源码剖析
logrus源码其实就两大部分,一个是Logger结构体,另一个就是Entry结构体
Logger
可以看到通过实现LevelHooks和Formatter接口可分别实现自定义的Hook机制及日志输出格式,还可指定Out将日志输出到标准输出,文件系统或elasticsearch等,也可指定ExitFunc在发生Panic或打印Fatal日志时指定程序退出方式。内部实现上Logger实际是借助Entry输出日志(如下图),为避免频繁的分配、回收Entry对象造成GC压力,采用了sync.Pool, 内部一些无状态的Entry操作都从entryPool池子中获取Entry实例,Logger通过MutexWrap互斥保证线程安全
Entry
每个Entry都可以找到自己的Logger。Field机制:logrus鼓励通过Field机制进行精细化的、结构化的日志记录,Filed可以用来存放唯一标识本次请求的数据,如traceid,userid等。Buffer是从Logger的BufferPool中获取,BufferPool也支持自定义。logrus的核心处理逻辑在下面这个Entry函数里面
这里新复制了一个Entry实例,并对这个实例进行赋值然后调用write,在write里面调用Format,进行输出到文件系统或ES
实战遇到的问题和解决方案
遇到的问题
在项目中,我希望每次请求的打印的日志都带上此次请求的唯一标识,规范日志输出,以及方便后续定位问题,以及日志上报之后根据特征数据做搜索 (如下图所示)。
但是却发现调用LogInfoWithCtx打印日志时每次输出的都是l.Debug(args...)所在的330行,没法输出正确的打印函数及对应的行号。查看logrus的源码,发现logrus每次都是获取调用栈中第一个非logrus函数所在的*runtime.Frame进行返回,logrus的源码如下图所示。
解决方案
方案一
实现: 不直接在LogInfoWithCtx函数中使用我们携带Fields的Entry,而是将*Entry返回出来在,对于context中没携带Entry的场景我们只能参考logrus的源码手动构建一个Entry进行返回,demo如下图:
优劣:这个方案的缺点就是,如果有大量的context没有设置Entry的情况下,系统中有可能会存在大量Entry对象,造成GC压力,在QPS较高的场景中不建议此方案
方案二
实现:看Entry.log函数中的源码可以知道,newEntry.Caller = getCaller()操作在newEntry.fireHooks()之前,所以我们可以通过定义自己的hook,在hook中重新获取正确的Caller,把hook注册到Logger中,demo如下:
优劣:这种方式是最优雅的一种解决方案,是比较推荐的
方案三
实现:同样通过源码可以发现newEntry.Caller被赋值之后,实际是在Entry.write中的Format才会去读取Caller所在的函数名及行号,进行格式化后输出。所以我们可以在定义自己的Formatter的同时,把Entry.Caller给重置成我们想要的,demo如下:
优劣:这种实现方式,稍微麻烦些,但不会造成GC压力
总结
因为logrus的可插拔,高扩展等特性,我们基于logrus做一些封装还是相对容易的。至于getMyCaller函数怎么去写,推荐大家阅读一下这篇文章就知道了colobu.com/2018/11/03/… 其实github上很多其他的开源框架实现也都很灵活,在使用过程中遇到问题,多翻翻源码,就能迎刃而解了。