beego的log模块源码阅读笔记
整体原理
- 程序启动的时候,每个日志接口的实现类都通过init(),将构造函数的地址注册到adapters的mapper结构中。
- 初始化一个BeeLogger对象,主要作用设置日志输出等级,设置日志输出目标对象(控制台或文件等),维护一个日志接口的实例列表。
- 通过BeeLogger的setLogger(),将日志实现类的实例加入到BeeLogger对象的的日志接口实例列表中。
- 输出日志。调用BeeLogger输出日志,遍历日志接口实例列表,调用实例的WriteMsg()输出日志。
源码分析
一、构建BeeLogger对象
//log.go
type BeeLogger struct {
//读写锁
lock sync.Mutex
//日志等级
level int
//初始化标志
init bool
//输出是否显示调用的文件名和文件行号
enableFuncCallDepth bool
//日志显示的调用栈的深度
loggerFuncCallDepth int
//是否异步
asynchronous bool
//消息前缀
prefix string
//消息通道大小
msgChanLen int64
//消息通过
msgChan chan *logMsg
//标志消息 输入"close"时候关闭日志管理器
signalChan chan string
//等待gouroutine完成的计数器
wg sync.WaitGroup
//日志输出实现的实例列表
outputs []*nameLogger
}
func NewLogger(channelLens ...int64) *BeeLogger {
bl := new(BeeLogger)
bl.level = LevelDebug
bl.loggerFuncCallDepth = 2
bl.msgChanLen = append(channelLens, 0)[0]
if bl.msgChanLen <= 0 {
bl.msgChanLen = defaultAsyncMsgLen
}
bl.signalChan = make(chan string, 1)
bl.setLogger(AdapterConsole)
return bl
}
NewLogger()的主要作用:
- 指定日志默认等级为Debug
- 日志显示文件栈的深度是2
- 默认消息通道大小是1000
- 默认是日志输出是控制台输出
二、设置日志输出类
//log.go
func (bl *BeeLogger) SetLogger(adapterName string, configs ...string) error {
bl.lock.Lock()
defer bl.lock.Unlock()
if !bl.init {
bl.outputs = []*nameLogger{}
bl.init = true
}
return bl.setLogger(adapterName, configs...)
}
func (bl *BeeLogger) setLogger(adapterName string, configs ...string) error {
config := append(configs, "{}")[0]
for _, l := range bl.outputs {
if l.name == adapterName {
return fmt.Errorf("logs: duplicate adaptername %q (you have set this logger before)", adapterName)
}
}
logAdapter, ok := adapters[adapterName]
if !ok {
return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName)
}
lg := logAdapter()
err := lg.Init(config)
if err != nil {
fmt.Fprintln(os.Stderr, "logs.BeeLogger.SetLogger: "+err.Error())
return err
}
bl.outputs = append(bl.outputs, &nameLogger{name: adapterName, Logger: lg})
return nil
}
setLogger()作用:
- 查找日志管理的输出实例列表outputs中是否存在同名的日志输出类的实例。
- 如果不存在该名字的日志输出实例的,就在adapters中取出对应的日志输出类的函数指针。
- 调用日志实现类的构造函数的指针实例化一个对象。
- 对象调用Init(string)设置初始化信息。
- 调用append函数将实例添加日志管理器的outputs列表中。
这里有2个疑问?
- 日志实现类的构造函数的指针是什么时候放在adapters中的?
- 构造函数实现的对象各不相同,为什么可以放在一个outputs列表中?
问题1:实现类构造函数的指针什么时候放在adapters
在log.go中查找adpters的使用,发现有func Register(name string, log newLoggerFunc)
这样一个函数,函数的目的是放入指定输出日志方式的实现类的构造函数指针。
//log.go
func Register(name string, log newLoggerFunc) {
if log == nil {
panic("logs: Register provide is nil")
}
if _, dup := adapters[name]; dup {
panic("logs: Register called twice for provider " + name)
}
adapters[name] = log
}
type newLoggerFunc func() Logger
可以看到newLoggerFunc是一个返回是Logger的指针。
再次查找func Register(name string, log newLoggerFunc)
的调用,会发现每个日志实现类中都有一个init()函数,将函数地址放入adapters中。
//console.log
func init() {
Register(AdapterConsole, NewConsole)
}
//file.go
func init() {
Register(AdapterFile, newFileWriter)
}
//multifile.go
func init() {
Register(AdapterMultiFile, newFilesWriter)
}
AdapterConsole,AdapterFile,AdapterMultiFile分别对应输出为控制台,文件,多文件。
init()在程序启动的时候首先调用,并保证只执行一次。
问题2:不同的实例为什么可以放在一个列表中
所有的实现类都是接口Logger的实现,都是同一种类对象的实例。
type Logger interface {
//导入初始化信息
Init(config string) error
//写入消息 when:日志的时间 msg: 消息体 level: 消息等级
WriteMsg(when time.Time, msg string, level int) error
Destroy()
Flush()
}
三、输出日志
通过输出Debug日志来分析源码。
//log.go
func (bl *BeeLogger) Debug(format string, v ...interface{}) {
if LevelDebug > bl.level {
return
}
bl.writeMsg(LevelDebug, format, v...)
}
判断日志等级是小于Debug,小于就不显示。
//日志等级比较
LevelEmergency < LevelAlert < LevelCritical < LevelError < LevelWarning < LevelNotice < LevelInformational < LevelDebug
//log.go
func (bl *BeeLogger) writeMsg(logLevel int, msg string, v ...interface{}) error {
if !bl.init {
bl.lock.Lock()
bl.setLogger(AdapterConsole)
bl.lock.Unlock()
}
if len(v) > 0 {
msg = fmt.Sprintf(msg, v...)
}
msg = bl.prefix + " " + msg
when := time.Now()
if bl.enableFuncCallDepth {
_, file, line, ok := runtime.Caller(bl.loggerFuncCallDepth)
if !ok {
file = "???"
line = 0
}
_, filename := path.Split(file)
msg = "[" + filename + ":" + strconv.Itoa(line) + "] " + msg
}
//set level info in front of filename info
if logLevel == levelLoggerImpl {
// set to emergency to ensure all log will be print out correctly
logLevel = LevelEmergency
} else {
msg = levelPrefix[logLevel] + " " + msg
}
if bl.asynchronous {
lm := logMsgPool.Get().(*logMsg)
lm.level = logLevel
lm.msg = msg
lm.when = when
bl.msgChan <- lm
} else {
bl.writeToLoggers(when, msg, logLevel)
}
return nil
}
输出日志的步骤:
- 判断日志管理是否初始化,如果没有初始化,就初始化一个输出到Console的实例。
- 通过日志时间,日志内容,前缀,日志等级构造一个消息体。
- 判断输出消息的模式是异步还是同步
3.1 同步模式下,直接调用writeToLoggers
输出消息。
3.2 异步模式下,从logMsgPool获取一个logMsg,并将消息信息放入。并将改消息对象放入到消息通道中。
同步模式下的日志输出
遍历输出日志实现实例的列表,分别调用对应实例的WriteMsg的接口输出日志。
func (bl *BeeLogger) writeToLoggers(when time.Time, msg string, level int) {
for _, l := range bl.outputs {
err := l.WriteMsg(when, msg, level)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to WriteMsg to adapter:%v,error:%v\n", l.name, err)
}
}
}
异步模式下日志输出
异步模式下输出日志,在函数调用SetLogger()后必须调用Async()函数,才能异步输出日志。
func (bl *BeeLogger) Async(msgLen ...int64) *BeeLogger {
bl.lock.Lock()
defer bl.lock.Unlock()
if bl.asynchronous {
return bl
}
bl.asynchronous = true
if len(msgLen) > 0 && msgLen[0] > 0 {
bl.msgChanLen = msgLen[0]
}
bl.msgChan = make(chan *logMsg, bl.msgChanLen)
logMsgPool = &sync.Pool{
New: func() interface{} {
return &logMsg{}
},
}
bl.wg.Add(1)
go bl.startLogger()
return bl
}
- 判断是否已经异步模式。是说明已经调用过该函数,直接返回;否说明未调用,需要进行后面的操作。
- 设置消息通道的大小,默认为1000。
- 通过make(chan *logMsg, msgChanLen)构建指定大小的消息通道。
- 构建logMsgPool消息池, logMsgPool主要用于异步输出消息,在消息产生时,放入其中,输出日志时候,从logMsgPool中取出使用。
- wg+1。
- 在goroutine中调用startLogger()。
//log.go
func (bl *BeeLogger) startLogger() {
gameOver := false
for {
select {
case bm := <-bl.msgChan:
bl.writeToLoggers(bm.when, bm.msg, bm.level)
logMsgPool.Put(bm)
case sg := <-bl.signalChan:
// Now should only send "flush" or "close" to bl.signalChan
bl.flush()
if sg == "close" {
for _, l := range bl.outputs {
l.Destroy()
}
bl.outputs = nil
gameOver = true
}
bl.wg.Done()
}
if gameOver {
break
}
}
}
函数主要有2个作用:
- 监听日志管理器中msgChan中的消息。有就调用writeToLoggers,并将消息放入logMsgPool中的消息。
- 监听日志管理器中signalChan中的消息。接收到"close"消息时候,将消息队列中的消息清空,并销毁日志管理器中outputs中的对象。然后消息管理器的wg计数器-1。
wg的作用
wg的主要作用在异步日志模式下,阻塞程序直到msgChan中所有的消息都被处理掉。
使用wg的地方主要有4处:
//log.go
func (bl *BeeLogger) Flush() {
if bl.asynchronous {
bl.signalChan <- "flush"
bl.wg.Wait()
bl.wg.Add(1)
return
}
bl.flush()
}
func (bl *BeeLogger) startLogger() {
...
for {
select {
...
case sg := <-bl.signalChan:
...
bl.wg.Done()
}
...
}
}
func (bl *BeeLogger) Async(msgLen ...int64) *BeeLogger {
...
bl.wg.Add(1)
...
}
func (bl *BeeLogger) Close() {
if bl.asynchronous {
bl.signalChan <- "close"
bl.wg.Wait()
close(bl.msgChan)
}...
}
sync.WaitGroup主要有3个函数:
- Add(n)把计数器设置为n。
- Wait()阻塞代码运行知道计数器设减为0。
- Done()将计数器-1。
在Async()中将计数器设置1。
在flush()中,通过wait()函数先将缓冲区中的日志输出,在重新将计数器设置为1。
在Close()中,先调用将"close"->signChan中,然后wait()阻塞代码运行。
在startLogger()中,监听到signChan的"close"信号的时候,将计数器-1。