一個好的log模塊能夠幫助咱們排錯,分析,統計git
通常來講log中須要有時間、棧信息(好比說文件名行號等),這些東西通常某些底層log模塊已經幫咱們作好了。但在業務中還有不少咱們須要記錄的信息,好比說:在web開發中,若是咱們接收到一條request,咱們可能須要執行不少操做,最基本的:github
若是僅僅只有這兩條的話咱們其實是能夠將消息放到一行來展現,但更復雜的狀況是也可能還須要記錄某些其餘的信息,好比說咱們在此次請求中將某個消息放入了消息隊列,咱們可能須要將這個消息是否放置成功,內容是什麼,等等記錄下來。若是分行記錄的話當出現問題須要排查的話可能會十分麻煩,由於線上的環境通常是併發的,咱們沒法保證同一個請求中的日誌每行都挨在一塊兒,因此咱們通常須要一個requestId來區分哪些日誌是同一個請求所產生的。因此咱們可能須要這樣的請求處理函數:web
func HandleRequest(requestId string, requestData []byte) (response []byte) { log.Info(requestId, requestData) ... log.Info(requestId, "do something: A") ... log.Info(requestId, "do something: B") ... log.Info(requestId, response) ... }
但這樣是否是很麻煩!每次打印日誌都須要額外的手動記錄requestId
,咱們須要有個通用的東西統一記錄requestId
,而後只須要將msg做爲參數放置進去就好了。數組
那麼咱們可能會想到一個解決辦法:每一個Request
都做爲一個結構體,這個結構體包含了一個prefix
字段,用來存儲像requestId
這樣的須要預置的前綴,那麼這個結構體可能看起來是這樣的:併發
type Request struct { Header []byte Body []byte Method []byte Url []byte ... prefix string } func (r *Request) Info(msg []byte) { log.Info(r.prefix, msg) } func (r *Request) SetPrefix(prefix string) { r.prefix = prefix }
那麼咱們前面的請求處理函數可能就像這樣:app
func HandleRequest(r *Request) (response []byte) { r.Info(requestData) ... r.Info("do something: A") ... r.Info("do something: B") ... r.Info(response) ... }
到這裏彷佛大功就告成了,但新的問題來了,由於項目中用到了http2.0,一個鏈接能夠處理多個請求,你的老大但願每一個鏈接都要記錄日誌,且能正確區分不一樣的鏈接。這時候你可能想都沒想就給鏈接結構體Conn
加上了prefix
字段,而後給Conn
加上了Info
等記錄方法,但聰明的你突然發現本身彷佛是在作一些重複的工做,爲什麼不把日誌抽離出來?因而就像這樣:函數
// r.go type PrefixLog struct { prefix string } func NewPrefixLog(prefix string) (pl *PrefixLog){ return $PrefixLog{prefix} } func (pl *PrefixLog) Info(msg []byte){ Log.Info(pl.prefix, msg) // 假設這裏行號是30 } type Request struct { Header []byte Body []byte Method []byte Url []byte ... *PrefixLog } type Conn struct { requestCount uint32 *PrefixLog } ...
此次基本大功告成!但彷佛新的問題又來了,假如爲了更方便的排錯,咱們在日誌須要保存log的文件名行號信息的話,上面這種形式就有問題了,由於經過這種方式調用的話全部的日誌的文件名和行號都是相同的: file_name: r.go line:30
,這該咋辦呢?ui
frp中的log模塊相對簡單,其封裝了beego
的log模塊,主要邏輯寫在utils/log
文件中,來分析一下該文件。日誌
import ( "github.com/fatedier/beego/logs" ) // Log is the under log object var Log *logs.BeeLogger
這個Log
變量是frp中log模塊的核心,幾乎全部(或者說就是全部)的日誌都是由這個Log
變量來負責操做的。code
func init() { Log = logs.NewLogger(200) Log.EnableFuncCallDepth(true) Log.SetLogFuncCallDepth(Log.GetLogFuncCallDepth() + 1) }
這個init
函數則初始化了Log
對象,注意Log.SetLogFuncCallDepth(Log.GetLogFuncCallDepth() + 1)
這句,大致上就是:咱們的程序能夠說是由一個一個的函數組成,這些函數之間相互調用,每調用一個函數就進行了一次入棧操做,當某個函數執行完就執行了出棧操做,而loggerFuncCallDepth
則表示要訪問的棧的位置。
那這個東西有啥用呢?咱們知道咱們打印日誌的時候有的時候但願可以在日誌中輸出執行log的文件以及行號信息,拿go標準庫log
舉個例子:
// main 文件 func a() { ... b("hell0") // 假如該行行號爲10 ... } func wtf(msg string) { ... msg = "[WTF!!]: " + msg log.Printf(msg) // 假如該行行號爲21 ... } func main() { a() }
// 標準庫log中的Printf方法,注意其內部調用了Output方法,且第一個參數爲2 func Printf(format string, v ...interface{}) { std.Output(2, fmt.Sprintf(format, v...)) } // 這是真正執行了打印的方法 func (l *Logger) Output(calldepth int, s string) error { ... }
這裏函數的調用順序是main -> a -> wtf -> log.Printf -> Output
,能夠說這是一個深度爲5的棧,calldepth爲0表示棧頂,也就是Output
對應的棧空間,1則表示log.Printf
對應的棧空間,2表示wtf
對應的棧空間,3表示wtf
......以此類推。由於log
模塊設置的callpath是2,也就是假如咱們設置了Llongfile
或者Lshortfile
標識符的時候輸出的文件名是main
,行號爲21,假如咱們設置callpath爲3的話,輸出的文件名依然是main
但行號則變爲了10。
這裏打印函數就拿Info
來講明吧
func Info(format string, v ...interface{}) { Log.Info(format, v...) }
能夠看到Info
函數實際上就是調用了Log.Info
方法,Log.Info
作了不少關於併發控制,格式輸出,buffer寫入的操做,但其最主要就是作了「將咱們要打印的文字輸出出來」這個操做。
type PrefixLogger struct { prefix string allPrefix []string } func (pl *PrefixLogger) AddLogPrefix(prefix string) { if len(prefix) == 0 { return } pl.prefix += "[" + prefix + "] " pl.allPrefix = append(pl.allPrefix, prefix) } // 一樣,這裏也僅僅列出PrefixLogger的Info方法 func (pl *PrefixLogger) Info(format string, v ...interface{}) { Log.Info(pl.prefix+format, v...) }
PrefixLogger
實際上就是一個具備前綴功能的很簡單的結構體。