beego的log模块源码阅读笔记

整体原理

  1. 程序启动的时候,每个日志接口的实现类都通过init(),将构造函数的地址注册到adapters的mapper结构中。
  2. 初始化一个BeeLogger对象,主要作用设置日志输出等级,设置日志输出目标对象(控制台或文件等),维护一个日志接口的实例列表。
  3. 通过BeeLogger的setLogger(),将日志实现类的实例加入到BeeLogger对象的的日志接口实例列表中。
  4. 输出日志。调用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()作用:

  1. 查找日志管理的输出实例列表outputs中是否存在同名的日志输出类的实例。
  2. 如果不存在该名字的日志输出实例的,就在adapters中取出对应的日志输出类的函数指针。
  3. 调用日志实现类的构造函数的指针实例化一个对象。
  4. 对象调用Init(string)设置初始化信息。
  5. 调用append函数将实例添加日志管理器的outputs列表中。

这里有2个疑问?

  1. 日志实现类的构造函数的指针是什么时候放在adapters中的?
  2. 构造函数实现的对象各不相同,为什么可以放在一个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
}

输出日志的步骤:

  1. 判断日志管理是否初始化,如果没有初始化,就初始化一个输出到Console的实例。
  2. 通过日志时间,日志内容,前缀,日志等级构造一个消息体。
  3. 判断输出消息的模式是异步还是同步
    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个作用:

  1. 监听日志管理器中msgChan中的消息。有就调用writeToLoggers,并将消息放入logMsgPool中的消息。
  2. 监听日志管理器中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个函数:

  1. Add(n)把计数器设置为n。
  2. Wait()阻塞代码运行知道计数器设减为0。
  3. Done()将计数器-1。

在Async()中将计数器设置1。

在flush()中,通过wait()函数先将缓冲区中的日志输出,在重新将计数器设置为1。

在Close()中,先调用将"close"->signChan中,然后wait()阻塞代码运行。

在startLogger()中,监听到signChan的"close"信号的时候,将计数器-1。

posted @ 2020-03-08 16:14  菜园小火车  阅读(571)  评论(0)    收藏  举报