frp原始碼剖析-frp中的log模組
前言&引入
一個好的log模組可以幫助我們排錯,分析,統計
一般來說log中需要有時間、棧資訊(比如說檔名行號等),這些東西一般某些底層log模組已經幫我們做好了。但在業務中還有很多我們需要記錄的資訊,比如說:在web開發中,如果我們接收到一條request,我們可能需要執行很多操作,最基本的:
- 請求資料是要記錄的
- response也是要記錄的
如果僅僅只有這兩條的話我們實際上是可以將訊息放到一行來展示,但更復雜的情況是也可能還需要記錄某些其他的資訊,比如說我們在這次請求中將某個訊息放入了訊息佇列,我們可能需要將這個訊息是否放置成功,內容是什麼,等等記錄下來。如果分行記錄的話當出現問題需要排查的話可能會十分麻煩,因為線上的環境一般是併發的,我們無法保證同一個請求中的日誌每行都挨在一起,所以我們一般需要一個requestId來區分哪些日誌是同一個請求所產生的。所以我們可能需要這樣的請求處理函式:
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 }
那麼我們前面的請求處理函式可能就像這樣:
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
,這該咋辦呢?
正文
frp中的log模組相對簡單,其封裝了beego
的log模組,主要邏輯寫在utils/log
檔案中,來分析一下該檔案。
全域性變數之Log
import ( "github.com/fatedier/beego/logs" ) // Log is the under log object var Log *logs.BeeLogger
這個Log
變數是frp中log模組的核心,幾乎所有(或者說就是所有)的日誌都是由這個Log
變數來負責操作的。
初始化之init函式
func init() { Log = logs.NewLogger(200) Log.EnableFuncCallDepth(true) Log.SetLogFuncCallDepth(Log.GetLogFuncCallDepth() + 1) }
這個init
函式則初始化了Log
物件,注意Log.SetLogFuncCallDepth(Log.GetLogFuncCallDepth() + 1)
這句,大體上就是:我們的程式可以說是由一個一個的函式組成,這些函式之間相互呼叫,每呼叫一個函式就進行了一次入棧操作,當某個函式執行完就執行了出棧操作,而loggerFuncCallDepth
則表示要訪問的棧的位置。
關於calldepth
那這個東西有啥用呢?我們知道我們列印日誌的時候有的時候希望能夠在日誌中輸出執行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寫入的操作,但其最主要就是做了“將我們要列印的文字輸出出來”這個操作。
log檔案中唯一的一個結構體: PrefixLogger
type PrefixLogger struct { prefixstring 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
實際上就是一個具有字首功能的很簡單的結構體。