Beego Logs 使用
先大致了解怎么使用,再进行剖析。
// Test console without color
func TestConsoleNoColor(t *testing.T) {
log := NewLogger(100)
log.SetLogger("console", `{"color":false}`)
bl.Error("error")
bl.Warning("warning")
}
// NewLogger returns a new BeeLogger.
// channelLen means the number of messages in
// chan(used where asynchronous is true).
// if the buffering chan is full, logger adapters write to file or other way.
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
}
上面有一句代码:
bl.msgChanLen = append(channelLens, 0)[0]
往 channelLens 切片添加一个值为零的元素后再取头个元素,这个技巧有以下好处:
- Go 不支持可选参数,但 Go 支持可变参数,这样做变相达到了可选参数的效果。
- 如果 chanelLens 原来为空的话也能拿出一个值为零的元素出来,不用再去判断参数是否为空数组。
loggerFuncCallDepth 的值应设为多少
这个变量表示函数调用的栈深度,用于记录日志时同时打印出当时执行语句的位置,包括文件名和行号。
虽然 NewLogger 方法里面默认将 loggerFuncCallDepth 置为2,但是如果你单独使用logs包时应根据情况设置不同值。举个栗子:
···
bl.Error("error") // ----------a 语句
···
// Error Log ERROR level message.
func (bl *BeeLogger) Error(format string, v ...interface{}) {
if LevelError > bl.level {
return
}
bl.writeMsg(LevelError, format, v...) // ----------b 语句
}
func (bl *BeeLogger) writeMsg(logLevel int, msg string, v ...interface{}) error {
···
if bl.enableFuncCallDepth {
_, file, line, ok := runtime.Caller(bl.loggerFuncCallDepth) // ----------c 语句
···
}
···
}
func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
···
}
关于 Caller 方法的 skip 参数:
The argument skip is the number of stack frames to ascend, with 0 identifying the caller of Caller. (For historical reasons the meaning of skip differs between Caller and Callers.)
即,skip 为零的时候,表示 Caller 方法本身,而我们需要的是 a 语句的所在的行号和文件名,所以这种情境下需要提升 2 个栈帧数。
工厂方法模式自定义日志输出引擎
以下是添加 console 输出引擎的用法,直接调用 SetLogger 方法即可。
func TestConsole(t *testing.T) {
···
log.SetLogger("console", `{"color":false}`)
···
}
type newLoggerFunc func() Logger
var adapters = make(map[string]newLoggerFunc)
func (bl *BeeLogger) SetLogger(adapterName string, configs ...string) error {
···
return bl.setLogger(adapterName, configs...)
}
func (bl *BeeLogger) setLogger(adapterName string, configs ...string) error {
···
log, ok := adapters[adapterName]
if !ok {
return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName)
}
lg := log() //--------- c 语句
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
}
func Register(name string, log newLoggerFunc) {
···
adapters[name] = log
}
在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。
上面 c 语句可以看到,具体需要用到什么输出引擎,BeeLogger 不负责它们的创建,而是由这些输出引擎自己去做。从 adapters 这个 map 结构里找到该输出引擎的构造方法, 并且执行这个构造方法。
例如 file.go 里面定义了如何构造一个文件输出引擎,并通过 init 方法注册:
func init() {
Register(AdapterFile, newFileWriter)
}
// newFileWriter create a FileLogWriter returning as LoggerInterface.
func newFileWriter() Logger {
w := &fileLogWriter{
Daily: true,
MaxDays: 7,
Rotate: true,
RotatePerm: "0440",
Level: LevelTrace,
Perm: "0660",
}
return w
}
为什么要用到互斥锁?
直接找到以下四处代码段:
func (bl *BeeLogger) Async(msgLen ...int64) *BeeLogger {
bl.lock.Lock()
defer bl.lock.Unlock()
···
}
func (bl *BeeLogger) SetLogger(adapterName string, configs ...string) error {
bl.lock.Lock()
defer bl.lock.Unlock()
···
}
func (bl *BeeLogger) DelLogger(adapterName string) error {
bl.lock.Lock()
defer bl.lock.Unlock()
···
}
func (bl *BeeLogger) writeMsg(logLevel int, msg string, v ...interface{}) error {
if !bl.init {
bl.lock.Lock()
bl.setLogger(AdapterConsole)
bl.lock.Unlock()
}
···
}
可以看出,在进行 SetLogger 、 DelLogger 这些操作时涉及到临界资源 bl *BeeLogger 相关配置字段的更改,必须操作前加锁保证并发安全。
临界资源是指每次仅允许一个进程访问的资源。
Asynchronous 选项为什么能提升性能
func (bl *BeeLogger) writeMsg(logLevel int, msg string, v ...interface{}) error {
···
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
}
如果开启 asynchronous 选项,将日志信息写进 msgChan 就完事了,可以继续执行其他的逻辑代码,除非 msgChan 缓存满了,否则不会发生阻塞,同时,还开启一个 goroutine 监听 msgChan,一旦 msgChan 不为空,将日志信息输出:
func (bl *BeeLogger) Async(msgLen ...int64) *BeeLogger {
···
go bl.startLogger()
···
}
// start logger chan reading.
// when chan is not empty, write logs.
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
}
}
}
从 logs package 外的 log.go 文件了解 beego 如何解耦
在 logs 包(package)外面还有一个 beego package 下的 log.go 文件,截取一段代码:
// github.com/astaxie/beego/log.go
package beego
import "github.com/astaxie/beego/logs"
// BeeLogger references the used application logger.
var BeeLogger = logs.GetBeeLogger()
// SetLevel sets the global log level used by the simple logger.
func SetLevel(l int) {
logs.SetLevel(l)
}
// github.com/astaxie/beego/logs/log.go
// beeLogger references the used application logger.
var beeLogger = NewLogger()
// GetBeeLogger returns the default BeeLogger
func GetBeeLogger() *BeeLogger {
return beeLogger
}
// SetLevel sets the global log level used by the simple logger.
func SetLevel(l int) {
beeLogger.SetLevel(l)
}
beego 为什么还在外面包了一层调用 logs 包里面的方法呢?其实 beego 本身是一个 Web 框架,那么本质就是一个服务端程序,服务端程序需要一个日志记录器来记录服务器的运行状况,那么调用 logs 包的代码以及其他一些配置、初始化的逻辑,就在 log.go 中处理。
这里其实也没有什么,就是一开始笔者在读源码的时候老是被这里疑惑,认为多此一举。其实要实现一个功能单一的 logs 包并与其他模块解耦,这么做的确不错。
再如, beego 的 session 模块,为了不与 logs 模块耦合,所以 session 模块也造了一个仅供自己模块内使用的日志记录器 SessionLog 。代码如下:
// Log implement the log.Logger
type Log struct {
*log.Logger
}
// NewSessionLog set io.Writer to create a Logger for session.
func NewSessionLog(out io.Writer) *Log {
sl := new(Log)
sl.Logger = log.New(out, "[SESSION]", 1e9)
return sl
}
不妨看看 Beego 官方的架构图:
beego 是基于八大独立的模块构建的,是一个高度解耦的框架。用户即使不使用 beego 的 HTTP 逻辑,也依旧可以使用这些独立模块,例如:你可以使用 cache 模块来做你的缓存逻辑;使用日志模块来记录你的操作信息;使用 config 模块来解析你各种格式的文件。所以 beego 不仅可以用于 HTTP 类的应用开发,在你的 socket 游戏开发中也是很有用的模块,这也是 beego 为什么受欢迎的一个原因。大家如果玩过乐高的话,应该知道很多高级的东西都是一块一块的积木搭建出来的,而设计 beego 的时候,这些模块就是积木,高级机器人就是 beego。