Go beego的logs源碼解讀

beego的日志處理支持多種引擎、多種日志級(jí)別的輸出,也可以設(shè)置輸出方式(包括是否輸出文件名和行號(hào)、同步輸出或者異步輸出等)。

本文將主要介紹一下beego是如何實(shí)現(xiàn)控制臺(tái)和文件兩個(gè)引擎的日志輸出、日志消息如何構(gòu)造以及如何實(shí)現(xiàn)的異步日志傳輸。

安裝與使用

使用以下命令進(jìn)行安裝:

$ go get github.com/astaxie/beego/logs

安裝后在源文件中引入包便可以使用了:

import ( "github.com/astaxie/beego/logs" )

輸出引擎

注冊(cè)引擎

beego支持的引擎有:console、file等,具體如下:

// Name for adapter with beego official support
const (
    AdapterConsole   = "console"
    AdapterFile      = "file"
    AdapterMultiFile = "multifile"
    AdapterMail      = "smtp"
    AdapterConn      = "conn"
    AdapterEs        = "es"
    AdapterJianLiao  = "jianliao"
    AdapterSlack     = "slack"
    AdapterAliLS     = "alils"
)

每個(gè)引擎都需要實(shí)現(xiàn)以下接口:

// Logger defines the behavior of a log provider.
type Logger interface {
    Init(config string) error
    WriteMsg(when time.Time, msg string, level int) error
    Destroy()
    Flush()
}

全局變量adapters是一個(gè)map,關(guān)聯(lián)了每個(gè)引擎的名稱以及其對(duì)應(yīng)的創(chuàng)建接口。每個(gè)引擎的源文件中都會(huì)有一個(gè)init() 函數(shù),引入"github.com/astaxie/beego/logs"包時(shí)系統(tǒng)會(huì)自動(dòng)調(diào)用該函數(shù),該函數(shù)會(huì)將引擎的創(chuàng)建接口(NewConsole、newFileWriter)注冊(cè)到adapters中。

var adapters = make(map[string]newLoggerFunc)
type newLoggerFunc func() Logger

func Register(name string, log newLoggerFunc) {
    adapters[name] = log
}

/* ------------------------ console.go -------------------------- */
func init() {
    Register(AdapterConsole, NewConsole)
}

// NewConsole create ConsoleWriter returning as LoggerInterface.
func NewConsole() Logger {
    ... ...
    return cw
}

/* ------------------------ file.go -------------------------- */
func init() {
    Register(AdapterFile, newFileWriter)
}

// newFileWriter create a FileLogWriter returning as LoggerInterface.
func newFileWriter() Logger {
    w := &fileLogWriter{
        ... ...
    }
    return w
}

創(chuàng)建日志模塊BeeLogger

logs模塊創(chuàng)建了一個(gè)全局變量beeLogger,默認(rèn)使用控制臺(tái)輸出日志:

var beeLogger = NewLogger()

func NewLogger(channelLens ...int64) *BeeLogger {
    bl := new(BeeLogger)
    ... ...
    bl.setLogger(AdapterConsole)
    return bl
}

type BeeLogger struct {
    lock                sync.Mutex
    level               int
    init                bool
    enableFuncCallDepth bool
    loggerFuncCallDepth int
    asynchronous        bool
    msgChanLen          int64
    msgChan             chan *logMsg
    signalChan          chan string
    wg                  sync.WaitGroup
    outputs             []*nameLogger
}

控制臺(tái)引擎

創(chuàng)建控制臺(tái)引擎,輸出設(shè)置為os.Stdout,即控制臺(tái):

/* ------------------------ console.go -------------------------- */
// consoleWriter implements LoggerInterface and writes messages to terminal.
type consoleWriter struct {
    lg       *logWriter
    Level    int  `json:"level"`
    Colorful bool `json:"color"` //this filed is useful only when system's terminal supports color
}

// NewConsole create ConsoleWriter returning as LoggerInterface.
func NewConsole() Logger {
    cw := &consoleWriter{
        lg:       newLogWriter(os.Stdout), // 輸出設(shè)置為os.Stdout,即控制臺(tái)
        Level:    LevelDebug,
        Colorful: runtime.GOOS != "windows",
    }
    return cw
}

/* ------------------------ logger.go -------------------------- */
type logWriter struct {
    sync.Mutex
    writer io.Writer
}

func newLogWriter(wr io.Writer) *logWriter {
    return &logWriter{writer: wr}
}

默認(rèn)使用控制臺(tái)引擎,日志級(jí)別包括:

/* ------------------------ log.go -------------------------- */
const ( 
    LevelEmergency = iota
    LevelAlert
    LevelCritical
    LevelError
    LevelWarning
    LevelNotice
    LevelInformational
    LevelDebug
)

默認(rèn)使用LevelDebug級(jí)別:

/* ------------------------ log.go -------------------------- */
func NewLogger(channelLens ...int64) *BeeLogger {
    bl := new(BeeLogger)
    bl.level = LevelDebug
    ... ...
    return bl
}

可以通過SetLevel函數(shù)設(shè)置輸出級(jí)別,那么超出所設(shè)置級(jí)別的日志將不輸出:

/* ------------------------ log.go -------------------------- */
func SetLevel(l int) {
    beeLogger.SetLevel(l)
}

func (bl *BeeLogger) SetLevel(l int) {
    bl.level = l
}

我們可以使用以下方法記錄不同級(jí)別的日志:

logs.Debug("... ...")
logs.Warn("... ...")
logs.Notice("... ...")
... ...

以Debug為例,若是設(shè)置的日志級(jí)別低于LevelDebug,則不輸出日志到控制臺(tái),直接返回;否則構(gòu)造日志內(nèi)容,并輸出日志到控制臺(tái):

/* ------------------------ log.go -------------------------- */
func Debug(f interface{}, v ...interface{}) {
    beeLogger.Debug(formatLog(f, v...))
    // formatLog(f, v...)使日志支持格式化輸出logs.Debug("xxx %v", xxx)
}

func (bl *BeeLogger) Debug(format string, v ...interface{}) {
    if LevelDebug > bl.level { // 若是設(shè)置的日志級(jí)別低于LevelDebug,則不輸出日志到控制臺(tái),直接返回
        return
    }
    bl.writeMsg(LevelDebug, format, v...)
}

構(gòu)造日志內(nèi)容

通過以下設(shè)置,可以在每條日志之前加上哪個(gè)文件中哪一行調(diào)用的logs.Debug()函數(shù):

/* ------------------------ 程序員的源文件 -------------------------- */
log.EnableFuncCallDepth(true)
log.SetLogFuncCallDepth(3)

/* ------------------------ log.go -------------------------- */
func EnableFuncCallDepth(b bool) {
    beeLogger.enableFuncCallDepth = b
}
func SetLogFuncCallDepth(d int) {
    beeLogger.loggerFuncCallDepth = d
}

根據(jù)設(shè)置構(gòu)造日志內(nèi)容:

/* ------------------------ log.go -------------------------- */
func (bl *BeeLogger) writeMsg(logLevel int, msg string, v ...interface{}) error {
    ... ...
    when := time.Now() // 當(dāng)前時(shí)間
    if bl.enableFuncCallDepth {
        _, file, line, ok := runtime.Caller(bl.loggerFuncCallDepth) // 獲取文件名(包括路徑)和行號(hào)
        ... ...
        _, filename := path.Split(file)  // 只取文件名
        msg = "[" + filename + ":" + strconv.Itoa(line) + "] " + msg // 在日志前加上“[文件名:行號(hào)] ”字符串
    }

    ... ...
    msg = levelPrefix[logLevel] + msg // 在日志前加上“[D] ”字符串
    ... ...
        bl.writeToLoggers(when, msg, logLevel)
    ... ...
    return nil
}

var levelPrefix = [LevelDebug + 1]string{"[M] ", "[A] ", "[C] ", "[E] ", "[W] ", "[N] ", "[I] ", "[D] "}

func (bl *BeeLogger) writeToLoggers(when time.Time, msg string, level int) {
        ... ...
        l.WriteMsg(when, msg, level)
        ... ...
}

日志顏色設(shè)置

console.go文件中有一個(gè)全局變量colors,這是一個(gè)數(shù)組,每個(gè)元素是一個(gè)brush函數(shù),用于為日志內(nèi)容渲染顏色:

/* ------------------------ console.go -------------------------- */
// brush is a color join function
type brush func(string) string

// newBrush return a fix color Brush
func newBrush(color string) brush {
    pre := "\033["  
    reset := "\033[0m"
    return func(text string) string {
        return pre + color + "m" + text + reset
    }
}

var colors = []brush{
    newBrush("1;37"), // Emergency          white
    newBrush("1;36"), // Alert              cyan
    newBrush("1;35"), // Critical           magenta
    newBrush("1;31"), // Error              red
    newBrush("1;33"), // Warning            yellow
    newBrush("1;32"), // Notice             green
    newBrush("1;34"), // Informational      blue
    newBrush("1;44"), // Debug              Background blue
}

/*
格式:\033[顯示方式;前景色;背景色m
 
說明:
前景色            背景色           顏色
---------------------------------------
30                40              黑色
31                41              紅色
32                42              綠色
33                43              黃色
34                44              藍(lán)色
35                45              紫紅色
36                46              青藍(lán)色
37                47              白色
顯示方式           意義
-------------------------
0                終端默認(rèn)設(shè)置
1                高亮顯示
4                使用下劃線
5                閃爍
7                反白顯示
8                不可見
 
例子:
\033[1;31;40m    
\033[0m    
*/

為日志渲染顏色:

/* ------------------------ console.go -------------------------- */
func (c *consoleWriter) WriteMsg(when time.Time, msg string, level int) error {
    ... ...
        msg = colors[level](msg) 
        // 假設(shè)level為L(zhǎng)evelDebug,則colors[level]得到newBrush("1;44"),
        // 也就是函數(shù)func(text string) string { return "\033[1;44m" + text + "\033[0m" }
        // 也就是日志內(nèi)容渲染為高亮背景顏色為藍(lán)色
    ... ...
    c.lg.println(when, msg)
    return nil
}

日志輸出

以下函數(shù)先將當(dāng)前時(shí)間格式化成"yyyy/mm/dd hh:mm:ss"格式,然后再構(gòu)造成"yyyy/mm/dd hh:mm:ss msg\n"的格式,至此日志內(nèi)容構(gòu)造完成,調(diào)用Write輸出。

/* ------------------------ logger.go -------------------------- */
func (lg *logWriter) println(when time.Time, msg string) {
    lg.Lock()  
    // 競(jìng)爭(zhēng)鎖
    h, _ := formatTimeHeader(when) 
    // 將時(shí)間格式化成"yyyy/mm/dd hh:mm:ss "格式
    lg.writer.Write(append(append(h, msg...), '\n')) 
    // lg.writer為os.Stdout,則為os.Stdout.Write("yyyy/mm/dd hh:mm:ss msg\n")
    lg.Unlock()
}
Debug、Informational、Notice、Warning、Error

文件引擎

創(chuàng)建文件引擎,輸出到文件中,默認(rèn)日志級(jí)別為L(zhǎng)evelTrace,每天更換一個(gè)日志文件,日志文件保存最近7天的日志:

/* ------------------------ file.go -------------------------- */
type fileLogWriter struct {
    sync.RWMutex // write log order by order and  atomic incr maxLinesCurLines and maxSizeCurSize
    // The opened file
    Filename   string `json:"filename"`
    fileWriter *os.File

    // Rotate at line
    MaxLines         int `json:"maxlines"`
    maxLinesCurLines int

    // Rotate at size
    MaxSize        int `json:"maxsize"`
    maxSizeCurSize int

    // Rotate daily
    Daily         bool  `json:"daily"`
    MaxDays       int64 `json:"maxdays"`
    dailyOpenDate int
    dailyOpenTime time.Time

    Rotate bool `json:"rotate"`

    Level int `json:"level"`

    Perm string `json:"perm"`

    RotatePerm string `json:"rotateperm"`

    fileNameOnly, suffix string // like "project.log", project is fileNameOnly and .log is suffix
}

// 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
}

設(shè)置文件引擎

beego支持將日志寫入到文件中,可以根據(jù)需要設(shè)置日志級(jí)別,設(shè)置文件路徑、文件名,可以設(shè)置多少行、多少字節(jié)將日志重新寫到另外一個(gè)日志文件,還可以設(shè)置每天分割一次日志文件并設(shè)置日志保存的天數(shù):

logs.SetLogger(logs.AdapterFile,`{"filename":"log/project.log","level":7,"maxlines":0,"maxsize":0,"daily":true,"maxdays":10}`)
// "filename":"log/project.log" :將日志保存到當(dāng)前目錄下的log目錄下的project.log文件中
// "level":7 :將日志級(jí)別設(shè)為7,也就是LevelDebug
// "maxlines":0 :設(shè)置日志文件分割條件,若文件超過maxlines,則將日志保存到下個(gè)文件中,為0表示不設(shè)置
// "maxsize":0 :設(shè)置日志文件分割條件,若文件超過maxsize,則將日志保存到下個(gè)文件中,為0表示不設(shè)置
// "daily":true:設(shè)置日志日否每天分割一次
// "maxdays":10:設(shè)置保存最近幾天的日志文件,超過天數(shù)的日志文件被刪除,為0表示不設(shè)置


/* ------------------------ log.go -------------------------- */
func SetLogger(adapter string, config ...string) error {
    return beeLogger.SetLogger(adapter, config...)
}

func (bl *BeeLogger) SetLogger(adapterName string, configs ...string) error {
    bl.lock.Lock()  
    // 設(shè)置之前需要加鎖
    defer bl.lock.Unlock()
    ... ...
    return bl.setLogger(adapterName, configs...)
}

func (bl *BeeLogger) setLogger(adapterName string, configs ...string) error {
    config := append(configs, "{}")[0]
    ... ...
    log, ok := adapters[adapterName]
    // adapters[adapterName]返回的是一個(gè)引擎的創(chuàng)建接口,這里為func newFileWriter() Logger
    ... ...
    lg := log()
    // 調(diào)用func newFileWriter() Logger,返回一個(gè)fileLogWriter文件引擎對(duì)象
    err := lg.Init(config)
    // 每個(gè)引擎對(duì)象都實(shí)現(xiàn)了Init函數(shù)
    ... ...
    return nil
}

/* ------------------------ file.go -------------------------- */
func (w *fileLogWriter) Init(jsonConfig string) error {
    err := json.Unmarshal([]byte(jsonConfig), w)
    // jsonConfig是json格式的字符串,Unmarshal方法將其中的字段轉(zhuǎn)化為對(duì)象w的字段
    ... ...
    w.suffix = filepath.Ext(w.Filename)
    // 取出后綴:.log
    w.fileNameOnly = strings.TrimSuffix(w.Filename, w.suffix)
    // 去除后綴:log/project
    if w.suffix == "" {
        w.suffix = ".log"
    }
    // 如果設(shè)置的文件后綴為空,則后綴設(shè)為".log"
    err = w.startLogger()
    return err
}

// start file logger. create log file and set to locker-inside file writer.
func (w *fileLogWriter) startLogger() error {
    file, err := w.createLogFile()
    // 打開文件log/project.log并返回其文件描述符
    ... ...
    w.fileWriter = file
    return w.initFd()
}

func (w *fileLogWriter) createLogFile() (*os.File, error) {
    // Open the log file
    perm, err := strconv.ParseInt(w.Perm, 8, 64)
    // 將設(shè)置的文件權(quán)限轉(zhuǎn)化為8進(jìn)制,64位的整數(shù)
    ... ...
    fd, err := os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(perm))
    // 以只寫權(quán)限并追加寫的方式打開文件(log/project.log),如果文件不存在則創(chuàng)建文件
    ... ...
        os.Chmod(w.Filename, os.FileMode(perm))
        // 確保文件權(quán)限為設(shè)置的值
    ... ...
    return fd, err
}

func (w *fileLogWriter) initFd() error {
    fd := w.fileWriter
    fInfo, err := fd.Stat()
    ... ...
    w.maxSizeCurSize = int(fInfo.Size())
    // 獲取文件當(dāng)前大小
    w.dailyOpenTime = time.Now()
    // 獲取當(dāng)前時(shí)間
    w.dailyOpenDate = w.dailyOpenTime.Day()
    // 獲取文件打開時(shí)間
    w.maxLinesCurLines = 0
    if w.Daily {
        go w.dailyRotate(w.dailyOpenTime)
        // 創(chuàng)建一個(gè)線程處理日志文件按天分割工作
    }
    if fInfo.Size() > 0 {
        count, err := w.lines()
        ... ...
        w.maxLinesCurLines = count
        // 獲取文件當(dāng)前行數(shù)
    }
    return nil
}

日志文件分割

beego用一個(gè)專門的goroutine來處理日志文件分割:

/* ------------------------ file.go -------------------------- */
func (w *fileLogWriter) dailyRotate(openTime time.Time) {
    y, m, d := openTime.Add(24 * time.Hour).Date()
    nextDay := time.Date(y, m, d, 0, 0, 0, 0, openTime.Location())
    tm := time.NewTimer(time.Duration(nextDay.UnixNano() - openTime.UnixNano() + 100))
    // 設(shè)置定時(shí)器,定時(shí)在24小時(shí)(也就是一天)之后,會(huì)給chan變量tm發(fā)送信號(hào)
    <-tm.C
    // 定時(shí)器到期,接收到信號(hào)
    w.Lock()
    if w.needRotate(0, time.Now().Day()) {
        if err := w.doRotate(time.Now()); err != nil {
            fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
        }
    }
    w.Unlock()
}


func (w *fileLogWriter) needRotate(size int, day int) bool {
    // 如果設(shè)置了最大行數(shù)或最大字節(jié)數(shù),是否超過了設(shè)置值
    // 如果按天分割,時(shí)間是否過了文件打開當(dāng)天
    // 滿足一個(gè)以上條件則返回true
    return (w.MaxLines > 0 && w.maxLinesCurLines >= w.MaxLines) ||
        (w.MaxSize > 0 && w.maxSizeCurSize >= w.MaxSize) ||
        (w.Daily && day != w.dailyOpenDate)
}

func (w *fileLogWriter) doRotate(logTime time.Time) error {
    // file exists
    // Find the next available number
    num := 1
    fName := ""
    rotatePerm, err := strconv.ParseInt(w.RotatePerm, 8, 64)
    ... ...
    if w.MaxLines > 0 || w.MaxSize > 0 {
        // 若設(shè)置了最大行數(shù)或者最大字節(jié)數(shù),則將日志文件名稱設(shè)置成格式
        // "project.yyyy-mm-dd.nnn.log",nnn從001-999,最多一天有999個(gè)日志文件
        for ; err == nil && num <= 999; num++ {
            fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format("2006-01-02"), num, w.suffix)
            _, err = os.Lstat(fName)
            // 判斷fName是否存在,若存在則返回nil
        }
    } else {
        // 將日志文件名稱設(shè)置成格式"project.yyyy-mm-dd.log"
        fName = fmt.Sprintf("%s.%s%s", w.fileNameOnly, w.dailyOpenTime.Format("2006-01-02"), w.suffix)
        _, err = os.Lstat(fName)
        for ; err == nil && num <= 999; num++ {
            fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", w.dailyOpenTime.Format("2006-01-02"), num, w.suffix)
            _, err = os.Lstat(fName)
        }
    }
    ... ...
    w.fileWriter.Close()
    // 關(guān)閉當(dāng)前日志文件,當(dāng)前日志文件一直為project.log
    err = os.Rename(w.Filename, fName)
    // 將當(dāng)前日志文件重命名為前面設(shè)置的格式fName
    ... ...
    err = os.Chmod(fName, os.FileMode(rotatePerm))
    startLoggerErr := w.startLogger()
    // 再打開日志文件project.log,記錄日志
    go w.deleteOldLog()
    // 開啟新的線程處理文件刪除工作
    ... ...
    return nil
}

日志文件保存期限

beego允許用戶設(shè)置日志文件保存天數(shù),超高設(shè)置時(shí)間的日志文件將被刪除,這個(gè)工作是由一個(gè)獨(dú)立的goroutine處理的:

/* ------------------------ file.go -------------------------- */
func (w *fileLogWriter) deleteOldLog() {
    dir := filepath.Dir(w.Filename)
    filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) {
        // 遍歷目錄dir下的所有文件
        ... ...
        if !info.IsDir() && info.ModTime().Add(24*time.Hour*time.Duration(w.MaxDays)).Before(time.Now()) {
            if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) &&
                strings.HasSuffix(filepath.Base(path), w.suffix) {
                // 若一個(gè)文件有project前綴以及.log后綴,而且修改日期超過日志文件保存天數(shù),則刪除
                os.Remove(path)
            }
        }
        return
    })
}

構(gòu)造日志內(nèi)容并輸出到日志文件

文件引擎的日志內(nèi)容構(gòu)造與控制臺(tái)日志內(nèi)容構(gòu)造差不多,以Debug為例,都是調(diào)用以下函數(shù):

/* ------------------------ log.go -------------------------- */
func Debug(f interface{}, v ...interface{}) {
    beeLogger.Debug(formatLog(f, v...))
    // formatLog(f, v...)使日志支持格式化輸出logs.Debug("xxx %v", xxx)
}

func (bl *BeeLogger) Debug(format string, v ...interface{}) {
    if LevelDebug > bl.level { // 若是設(shè)置的日志級(jí)別低于LevelDebug,則不輸出日志到控制臺(tái),直接返回
        return
    }
    bl.writeMsg(LevelDebug, format, v...)
}

func (bl *BeeLogger) writeMsg(logLevel int, msg string, v ...interface{}) error {
    ... ...
    when := time.Now() // 當(dāng)前時(shí)間
    if bl.enableFuncCallDepth {
        _, file, line, ok := runtime.Caller(bl.loggerFuncCallDepth) // 獲取文件名(包括路徑)和行號(hào)
        ... ...
        _, filename := path.Split(file)  // 只取文件名
        msg = "[" + filename + ":" + strconv.Itoa(line) + "] " + msg // 在日志前加上“[文件名:行號(hào)] ”字符串
    }

    ... ...
    msg = levelPrefix[logLevel] + msg // 在日志前加上“[D] ”字符串
    ... ...
        bl.writeToLoggers(when, msg, logLevel)
    ... ...
    return nil
}

var levelPrefix = [LevelDebug + 1]string{"[M] ", "[A] ", "[C] ", "[E] ", "[W] ", "[N] ", "[I] ", "[D] "}

func (bl *BeeLogger) writeToLoggers(when time.Time, msg string, level int) {
        ... ...
        l.WriteMsg(when, msg, level)
        ... ...
}

所有引擎都實(shí)現(xiàn)了Logger接口,即實(shí)現(xiàn)了WriteMsg方法:

/* ------------------------ file.go -------------------------- */
func (w *fileLogWriter) WriteMsg(when time.Time, msg string, level int) error {
    if level > w.Level { // 若日志級(jí)別超出設(shè)置的級(jí)別,則不輸出
        return nil
    }
    h, d := formatTimeHeader(when) 
    // 將時(shí)間格式化成"yyyy/mm/dd hh:mm:ss "格式
    msg = string(h) + msg + "\n"
    // 將日志內(nèi)容格式化為"yyyy/mm/dd hh:mm:ss msg\n"格式
    if w.Rotate {
        // 判斷是否需要分割日志文件并且是否達(dá)到分割要求,若是則分割
        w.RLock()
        if w.needRotate(len(msg), d) {
            w.RUnlock()
            w.Lock()
            if w.needRotate(len(msg), d) {
                if err := w.doRotate(when); err != nil {
                    fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
                }
            }
            w.Unlock()
        } else {
            w.RUnlock()
        }
    }
    w.Lock()
    // 將日志寫入文件之前需要競(jìng)爭(zhēng)鎖,如果有多個(gè)線程對(duì)日志文件進(jìn)行寫入操作,則會(huì)有競(jìng)爭(zhēng)關(guān)系。這個(gè)問題在多線程應(yīng)用中可能會(huì)成為性能瓶頸。
    _, err := w.fileWriter.Write([]byte(msg))
    // 將日志寫入文件
    if err == nil {
        w.maxLinesCurLines++
        w.maxSizeCurSize += len(msg)
    }
    w.Unlock()
    return err
}

異步日志

beego日志默認(rèn)使用同步日志寫入的方式。前邊控制臺(tái)和文件引擎在日志寫入前都需要對(duì)資源進(jìn)行加鎖,而且每次輸出日志都需要調(diào)用系統(tǒng)調(diào)用,非常耗時(shí),這導(dǎo)致在高并發(fā)的情況下會(huì)出現(xiàn)性能瓶頸。

異步日志是由一個(gè)專門的goroutine來將日志輸出,調(diào)用日志輸出的goroutine只需將日志內(nèi)容通過信道發(fā)送給日志輸出goroutine,日志輸出goroutine會(huì)從信道中取出日志并輸出。

信道大小默認(rèn)為1000:

var beeLogger = NewLogger()

func NewLogger(channelLens ...int64) *BeeLogger {
    bl := new(BeeLogger)
    ... ...
    bl.msgChanLen = append(channelLens, 0)[0]
    if bl.msgChanLen <= 0 {
        bl.msgChanLen = defaultAsyncMsgLen
    }
    bl.signalChan = make(chan string, 1)
    ... ...
    return bl
}

const defaultAsyncMsgLen = 1e3

設(shè)置異步日志輸出

通過以下函數(shù)設(shè)置信道大小(2000),并觸發(fā)異步日志輸出:

/* ------------------------ 程序員的源文件 -------------------------- */
logs.Async(2000)

/* ------------------------ log.go -------------------------- */
func Async(msgLen ...int64) *BeeLogger {
    return beeLogger.Async(msgLen...)
}

func (bl *BeeLogger) Async(msgLen ...int64) *BeeLogger {
    bl.lock.Lock()
    ... ...
    bl.asynchronous = true
    // 觸發(fā)異步日志輸出
    if len(msgLen) > 0 && msgLen[0] > 0 {
        bl.msgChanLen = msgLen[0]
    }
    bl.msgChan = make(chan *logMsg, bl.msgChanLen)
    // 設(shè)置信道大小
    logMsgPool = &sync.Pool{
        New: func() interface{} { 
            // 若從消息池中取日志時(shí)消息池為空,則調(diào)用該函數(shù),返回一個(gè)logMsg的空接口
            return &logMsg{}
        },
    }
    bl.wg.Add(1)
    // sync.WaitGroup只有3個(gè)方法,Add(),Done(),Wait()。 
    // 其中Done()是Add(-1)的別名。簡(jiǎn)單的來說,
    // 使用Add()添加計(jì)數(shù),Done()減掉一個(gè)計(jì)數(shù),計(jì)數(shù)不為0, Wait()阻塞計(jì)數(shù)為0。 
    
    go bl.startLogger()
    // 啟動(dòng)新的線程處理日志輸出
    return bl
}


// BeeLogger is default logger in beego application.
// it can contain several providers and log message into all providers.
type BeeLogger struct {
    ... ...
    asynchronous        bool
    msgChanLen          int64
    msgChan             chan *logMsg
    signalChan          chan string
    wg                  sync.WaitGroup
}

type logMsg struct {
    level int
    msg   string
    when  time.Time
}

var logMsgPool *sync.Pool

構(gòu)造日志內(nèi)容

以Debug為例,都是調(diào)用以下函數(shù):

/* ------------------------ log.go -------------------------- */
func Debug(f interface{}, v ...interface{}) {
    beeLogger.Debug(formatLog(f, v...))
    // formatLog(f, v...)使日志支持格式化輸出logs.Debug("xxx %v", xxx)
}

func (bl *BeeLogger) Debug(format string, v ...interface{}) {
    if LevelDebug > bl.level { // 若是設(shè)置的日志級(jí)別低于LevelDebug,則不輸出日志到控制臺(tái),直接返回
        return
    }
    bl.writeMsg(LevelDebug, format, v...)
}

func (bl *BeeLogger) writeMsg(logLevel int, msg string, v ...interface{}) error {
    ... ...
    when := time.Now() // 當(dāng)前時(shí)間
    if bl.enableFuncCallDepth {
        _, file, line, ok := runtime.Caller(bl.loggerFuncCallDepth) // 獲取文件名(包括路徑)和行號(hào)
        ... ...
        _, filename := path.Split(file)  // 只取文件名
        msg = "[" + filename + ":" + strconv.Itoa(line) + "] " + msg // 在日志前加上“[文件名:行號(hào)] ”字符串
    }

    ... ...
    msg = levelPrefix[logLevel] + msg // 在日志前加上“[D] ”字符串
    ... ...
    if bl.asynchronous { // 異步日志輸出
        lm := logMsgPool.Get().(*logMsg)
        // 從logMsgPool中獲取一個(gè)logMsg對(duì)象
        lm.level = logLevel
        lm.msg = msg
        lm.when = when
        // 構(gòu)造日志消息
        
        bl.msgChan <- lm
        // 將日志消息通過信道傳輸給日志輸出goroutine
    } else {
        ... ...
    }
    return nil
}

var levelPrefix = [LevelDebug + 1]string{"[M] ", "[A] ", "[C] ", "[E] ", "[W] ", "[N] ", "[I] ", "[D] "}

日志輸出

異步日志輸出由一個(gè)專門的線程處理:

func (bl *BeeLogger) startLogger() {
    gameOver := false
    for {
        select {
        case bm := <-bl.msgChan:
            // 從信道中取出一個(gè)日志并輸出到設(shè)置的引擎中
            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
        }
    }
}

func (bl *BeeLogger) writeToLoggers(when time.Time, msg string, level int) {
    ... ...
        err := l.WriteMsg(when, msg, level)
                // 這里將msg輸出到設(shè)置的引擎,這與前邊同步日志輸出中控制臺(tái)輸出和文件輸出一樣
    ... ...
}

至此,關(guān)于beego logs包中的控制臺(tái)和文件輸出的代碼學(xué)習(xí)就結(jié)束了~其他引擎的學(xué)習(xí)大致也是如此,如果有興趣可以再看看。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,868評(píng)論 18 139
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,547評(píng)論 8 265
  • 在應(yīng)用程序中添加日志記錄總的來說基于三個(gè)目的:監(jiān)視代碼中變量的變化情況,周期性的記錄到文件中供其他應(yīng)用進(jìn)行統(tǒng)計(jì)分析...
    時(shí)待吾閱讀 5,022評(píng)論 0 6
  • 在應(yīng)用程序中添加日志記錄總的來說基于三個(gè)目的:監(jiān)視代碼中變量的變化情況,周期性的記錄到文件中供其他應(yīng)用進(jìn)行統(tǒng)計(jì)分析...
    時(shí)待吾閱讀 5,080評(píng)論 1 13
  • 看了這本書,我突然覺得對(duì)不起我老公了,因?yàn)槲冶任依瞎蟀藲q,是典型的老牛吃嫩草。結(jié)婚前還可以依靠姿色抓住老公的心,...
    很哈閱讀 242評(píng)論 1 0