65

Beego Logs 源码分析 上篇

 5 years ago
source link: https://studygolang.com/articles/18845?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

bVbpAnn?w=572&h=364

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK