最近在自己開發的go語言web框架 Bingo 中需要一個日誌處理功能 , 看了看標準庫的log
包,
發現功能過於簡單,所以想重新造個輪子,單獨抽出來作為一個模組,輔助框架進行開發
[bingo-log] 是為了完成 bingo 的日誌功能而開發的一個第三方包,不依賴框架,可單獨在其他專案中使用,
Github地址: bingo-log
安裝和使用在 README.md
中已經寫的很清楚了,這裡不再贅述,主要記錄開發流程。
1. 預期效果
我希望這個包包含的功能:
- 支援多種報錯級別
- 日誌自定義配置並自動分割
- 可非同步輸出日誌
2. 實現思路
準備使該日誌包支援(FATAL
,ERROR
,WARNING
,DEBUG
,INFO
) 5種報錯級別,
寫一個日誌結構體作為基礎,在其中設定一個介面型別的資料,將允許自定義的方法放在這個介面中,這樣所有實現該介面的物件都可以作為引數傳入日誌結構體中
如何實現非同步功能?
為了可以限制資源消耗,使用協程連線池將每個輸出放入協程池中,達到非同步的效果,
連線池我就不重複造輪子了,使用一個現成的github專案: grpool
開始開發
- 構建最基礎的底:日誌結構體
首先宣告兩個常量,用來標記同步輸出還是非同步輸出
const (
LogSyncMode = iota
LogPoolMode
)
複製程式碼
構建結構體
type Log struct {
Connector // 內嵌聯結器,用來定製化功能
sync.Mutex
initialized bool // 該日誌物件是否初始化
mode int // 日誌記錄模式 同步記錄 or 協程池記錄
pool *grpool.Pool // 協程池
poolExpiredTime int // 協程池模式下,每個空閒協程的存活時間(秒)
poolWorkerNum int // 協程池模式下,允許的最高協程數
}
複製程式碼
- 構建聯結器介面
我們希望使用聯結器來設定每種輸出,所以這個介面應該實現如下幾種方法
type Connector interface {
Fatal(message ...interface{})
Error(message ...interface{})
Warning(message ...interface{})
Debug(message ...interface{})
Info(message ...interface{}) // 列印
Output(message string) // 將資訊輸出到檔案中
GetMessage(degree int, message ...interface{}) string // 將輸入的資訊新增抬頭(例如新增列印時間等)
GetFile(config map[string]string) *os.File // 當前日誌要輸出到的檔案位置,傳入一個map 代表配置
}
複製程式碼
上面5種方法是5種報錯級別要做的事情,主要做的事情,就是將要輸出的日誌,先呼叫 GetMessage()
將資訊進行包裝,包裝成我們希望的結構,再在控制檯列印輸出,然後再呼叫Output
方法,將日誌列印到日誌檔案中一份
而 Output()
方法中要呼叫 GetFile()
方法得到要輸出的檔案指標,我們可以在GetFile()
方法中設定分割檔案的方式,如果需要動態分割,那麼其中的map
引數就是外部傳進來的引數
- 為
Log
結構體新增方法:
-
先寫如何建立一個日誌物件:
func NewLog(mode int) *Log { l := &Log{} l.SetMode(mode) l.initialize() // 這裡對結構體中的資料做初始化 return l } 複製程式碼
-
然後載入聯結器
// 載入聯結器 func (l *Log) LoadConnector(conn Connector) { l.Connector = conn // 所有實現了聯結器介面的物件都可以作為引數傳入 } 複製程式碼
-
然後寫5種報錯級別:
// 重寫5種日誌級別的列印函式 func (l *Log) Fatal(message string) { // 根據模式 l.exec(l.Connector.Fatal, message) } func (l *Log) Error(message string) { l.exec(l.Connector.Error, message) } func (l *Log) Warning(message string) { l.exec(l.Connector.Warning, message) } func (l *Log) Debug(message string) { l.exec(l.Connector.Debug, message) } func (l *Log) Info(message string) { l.exec(l.Connector.Info, message) } 複製程式碼
-
上方的
exec
方法就是根據輸出模式選擇直接輸出,還是使用協程池輸出:func (l *Log) exec(f func(message ...interface{}), message string) { // 同步模式 if l.mode == LogSyncMode { l.Lock() defer l.Unlock() f(message) } else if l.mode == LogPoolMode { // 協程池非同步模式 l.initialize() // 先初始化 l.Lock() defer l.Unlock() l.AddWaitCount(1) // 向池中新增計數器,可以計算池中有多少協程正在被使用 l.pool.JobQueue <- func() { f(message) defer l.pool.JobDone() } } } 複製程式碼
從上面的程式碼可以看出,Log
結構體只是負責同步還是非同步執行,最重要的地方是聯結器Connector
, 我實現了兩種Connector
(BaseConnector
和KirinConnector
)那麼我們就實現一個基礎聯結器BaseConnector
:
-
建立一個結構體
type BaseConnector struct { sync.Mutex // 這裡是因為有用到map的地方需要加鎖 } 複製程式碼
-
實現聯結器介面:
- 先實現GetFile介面,實際就是在當前路徑下建立
bingo.log
檔案,並返回檔案指標:
// 返回一個檔案控制程式碼,用來寫入資料 func (b BaseConnector) GetFile(config map[string]string) *os.File { // 預設情況下,輸出到當前路徑下的bingo.log檔案中 dir, err := os.Getwd() if err != nil { panic(err) } path := dir + "/bingo.log" // 真實要儲存的檔案位置 // 判斷檔案是否存在 if _, err := os.Stat(path); err != nil { // 檔案不存在,建立 f, err := os.Create(path) //defer f.Close() // 關閉操作要放在呼叫位置 if err != nil { panic(err) } return f } // 開啟該檔案,追加模式 f, err := os.OpenFile(path, os.O_WRONLY, os.ModeAppend) if err != nil { panic(err) } return f } 複製程式碼
- 實現
Output
方法:
func (b BaseConnector) Output(message string) { // 獲取到要輸出的檔案路徑 file := b.GetFile(make(map[string]string)) defer file.Close() n, _ := file.Seek(0, os.SEEK_END) // 向檔案末尾追加資料 // 寫入資料 file.WriteAt([]byte(message), n) } 複製程式碼
- 實現
GetMessage
方法,這裡是將要輸出的日誌包裝成 期望的格式:
// 輸出格式為 [日誌級別][時間][日誌內容] func (b BaseConnector) GetMessage(degree int, message ...interface{}) string { var title string switch degree { case FATAL: title = "[FATAL] " case ERROR: title = "[ERROR] " case WARNING: title = "[WARNING]" case DEBUG: title = "[DEBUG] " case INFO: title = "[INFO]" default: title = "[UNKNOWN]" } // 將傳入的資訊擴充套件一下 // 預設新增當前時間 return title + "[" + time.Now().Format("2006-01-02 15:04:05") + "] " + fmt.Sprint(message...) + "\n" } 複製程式碼
- 實現5種日誌級別:
func (b BaseConnector) Info(message ...interface{}) { // 綠色輸出在控制檯 m := b.GetMessage(INFO, message...) fmt.Print(clcolor.Green(m)) // 輸出在檔案中 b.Output(m) } 複製程式碼
為了在控制檯中達到以不同的顏色輸出不同級別的日誌,我們要在列印函式中加上顏色,具體方式在這裡給終端來點彩色(c語言和Golang版)
我這裡直接使用了一個別人寫好的第三方包xcltapestry/xclpkg
直接使用
clcolor.Green()
即可這樣,一個基本的聯結器就製作好了,我們可以隨時自行擴充套件
- 先實現GetFile介面,實際就是在當前路徑下建立
小結
使用方式類似於:
log := bingo_log.NewLog(bingo_log.LogSyncMode)
conn := new(bingo_log.BaseConnector)
log.LoadConnector(conn)
log.Info("testing")
log.Debug("testing")
log.Warning("testing")
log.Error("testing")
log.Fatal("testing")
複製程式碼
介面是golang種極其強大的特性,我們可以利用介面完成很多動態結構
最後再推薦一下自己的 WEB 框架 Bingo,求 star,求 PR ~~~