Beego Logs 源码分析 上篇

chencanxin · · 1221 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

最近参加春招,确实挺受打击,平常做项目遇到的问题,学到的知识点没有及时总结,导致在面试的时候无法清晰的描述出来,因此本专栏后续日常更新,总结编程之路的点滴。下面进入正题。

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 官方的架构图:

clipboard.png

beego 是基于八大独立的模块构建的,是一个高度解耦的框架。用户即使不使用 beego 的 HTTP 逻辑,也依旧可以使用这些独立模块,例如:你可以使用 cache 模块来做你的缓存逻辑;使用日志模块来记录你的操作信息;使用 config 模块来解析你各种格式的文件。所以 beego 不仅可以用于 HTTP 类的应用开发,在你的 socket 游戏开发中也是很有用的模块,这也是 beego 为什么受欢迎的一个原因。大家如果玩过乐高的话,应该知道很多高级的东西都是一块一块的积木搭建出来的,而设计 beego 的时候,这些模块就是积木,高级机器人就是 beego。

有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:chencanxin

查看原文:Beego Logs 源码分析 上篇

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1221 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传