前言
这篇文章将着重于分析字节跳动开源的RPC
框架Kitex的日志库klog
的源码,通过对比Go原生日志库log
的实现,探究其作出的改进。
为了平滑学习曲线,我写下了这篇分析Go原生log
库的文章,希望你可以对比阅读:https://juejin.cn/post/7103790667595268126
本文的分析基于:github.com/cloudwego/kitex/pkg/klog
的源码。
klog库的使用
结果如下:
klog.xxx能直接打印日志的原因
通过观察源码,klog
包的default.go
文件中,封装了三类日志的打印的函数提供直接使用:普通日志、格式化的日志、格式化的Context日志。
每一类包含了7个的日志输出级别的函数可使用:Info
、Debug
、Notice
、Warn
、Error
、Fatal
、Trace
。
并且这21个函数中频繁使用到了一个logger
实例,只要我们引入klog
包,logger
就会完成初始化,并且作为默认的log
实例。
可以看到logger
实例默认的日志打印级别是LevelInfo
,klog
通过常量计数器定义了0~6
种日志级别:
FullLogger接口
默认的logger
实例是通过defaultLogger
结构初始化的,且defaultLogger
结构实现了FullLogger
接口定义的所有函数(接口定义了上面说的三类,每一类7种日志打印函数)
并且观察defaultLogger
结构的属性stdlog
,就是Go原生的日志库log
定义的Logger
类型,因此klog
的所有日志操作,最终都是借助Go原生log
库实现的。
相当于klog
在Go原生log库
的基础上对格式化输出和日志打印级别作了封装,便于直接使用。
串联一下日志打印函数执行的过程:
- •
main函数
中调用:klog.Info("一条普通的日志")
- • 进一步调用初始化好的
defaultLogger
实例(名为logger
)的实现自FullLogger
接口的函数:logger.Info()
- • 进一步调用
ll.logf()
函数(下面重点分析)
ll.logf()
上面的这三类共21个日志打印函数最终都调用了ll.logf()
方法,因此ll.logf()
也是klog
库的核心函数,看一下代码:
- • 日志过滤:如果调用的打印函数代表的日志级别低于
logger
实例初始化的日志级别,则不会打印(如默认级别是LevelInfo == 2
,则调用klog.Trace()
将被过滤) - • 格式化打印信息存入
msg
- • 调用Go原生日志库
log
的Output()
函数,打印日志(这一部分在上一篇分析Go的log库的文章中已经充分讲解)
关于calldepth的问题
calldepth
表示调用层数,这里声明了4,是为了配合获取调用日志打印函数的文件名和所在行数。
- •
calldepth == 0
,表示获取调用runtime.Caller(calldepth)
的文件名和行数 - •
calldepth == 1
,表示获取调用std.Output()
的文件名和行数 - •
calldepth == 2
,表示获取调用ll.logf()
的文件名和行数 - •
calldepth == 3
,表示获取调用logger.Info()
的文件名和行数 - •
calldepth == 4
,表示获取调用klog.Info()
的文件名和行数(也就是main.go
文件)
基于klog再度进行封装,在打印日志获取文件名时可能会有问题,下面是摘自Kitex文档的一句描述:
猜测原因就是klog
的封装,固定了calldepth == 4
,确保其在获取文件信息时能定位到main.go
文件中,而如果对klog
再封几层,会导致calldepth
需要更大才能定位到最外层main.go
文件,而这个值并不能通过klog
的提供的实现进行修改。
在初始化时通过log.New()
函数指定了日志输出的位置和需要打印的前置信息(文件名、行数、日期)
定制自己的Logger
可以使用klog.SetLogger()
来替换掉默认的logger
实现,需要传入一个实现了所有FullLogger
接口中定义的方法的实例。
值得注意的是:SetLogger()
函数并非是并发安全的,这个方法不应该在你使用了默认的defaultLogger
定义实例之后再去使用(会覆盖掉默认的logger
实例)。
当然完全重新定制比较复杂,大多数时候,我们只需要在默认的logger
基础上重定向日志输出或者修改默认日志级别即可:
下面修改日志打印级别为Notice
(高于Info
),并且重定向日志的输出:
这里指定了日志输出到文件log.txt
中,并且因为Info
级别低于声明的Notice
,因此日志输出操作被忽略:
小结
通过分析,我们发现klog
在Go原生log
库的基础上,进行了精简的二次封装,一定程度上约束了打印的日志的内容为:日期 时间微秒级 调用文件名 所在行数 日志级别 格式化的日志内容,使用十分便捷。
当然它也提供了SetLogger()
方法去供你自己实现logger
实例,以满足更多的定制化需求。