Beego Logs 原始碼分析 上篇
最近參加春招,確實挺受打擊,平常做專案遇到的問題,學到的知識點沒有及時總結,導致在面試的時候無法清晰的描述出來,因此本專欄後續日常更新,總結程式設計之路的點滴。下面進入正題。
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。