日誌幾乎是每一個實際的軟件項目從開發到最後實際運行過程當中都必不可少的東西。它對於查看代碼運行流程,記錄發生的事情等方面都是很重要的。linux
一個好的日誌系統應當能準確地記錄須要記錄的信息,同時兼具良好的性能,接下來本文將從0寫一個Golang日誌處理包。json
一般Go應用程序多爲併發模型應用,在併發處理實際應用的過程當中也沒法避免併發地調用日誌方法。緩存
一般來講,Go中除非聲明方法是協程安全的,不然最好都視爲協程不安全,Go中默認的日誌方法爲log,該方法協程安全,可是功能有限。安全
第三方Go日誌包如ZAP、logrus等均爲較爲不錯的日誌庫,也提供了較多的配置選項,可是對於高度定製需求的日誌則有些不適用,從0開始本身寫一個就較爲適用。併發
要作到按json方式輸出到文件,而且支持豐富的類型,第一個能想到的則是Golang中強大的interfaceapp
設計兩種類型分別用於接收map型和string型日誌,再根據日誌分級設定分別暴露出對外接口:性能
func Debug(msg map[string]interface{}) { writeLog("DEBUG", msg) } func DebugMsg(msg interface{}) { writeLog("DEBUG", map[string]interface{}{"msg": msg}) } func Info(msg map[string]interface{}) { writeLog("INFO", msg) } func InfoMsg(msg interface{}) { writeLog("INFO", map[string]interface{}{"msg": msg}) } func Warn(msg map[string]interface{}) { writeLog("WARN", msg) } func WarnMsg(msg interface{}) { writeLog("WARN", map[string]interface{}{"msg": msg}) } func Error(msg map[string]interface{}) { writeLog("ERROR", msg) } func ErrorMsg(msg interface{}) { writeLog("ERROR", map[string]interface{}{"msg": msg}) }
最終都是使用writeLog進行日誌的處理,writeLog方法定義以下:測試
func writeLog(level string, msg map[string]interface{})
Golang對於文件的寫入方式多種多樣,一般來說最後都是使用操做系統的磁盤IO方法把數據寫入文件操作系統
在選型上這塊使用bufio方式來構建,使用默認的4096長度,若是收集的日誌長度超過了緩衝區長度則自動將內容寫入到文件,同時增長一組定時器,每秒將緩衝區內容寫入到文件中,這樣在總體性能上較爲不錯設計
針對多協程搶佔的問題,Golang提供有兩個比較標準的應對方式:使用channel或者加鎖
在這裏採用讀寫鎖的方式來進行處理bufio,bufio的WriteString方法需串行處理,要否則會致使錯誤,而Flush方法能夠多協程同時操做
基於這些特性,咱們在使用WriteString方法的時候使用寫鎖,使用Flush方法時採用讀鎖:
fileWriter.Mu.Lock() fileWriter.Writer.WriteString(a) fileWriter.Mu.Unlock()
fileWriter.Mu.RLock() err = fileWriter.Writer.Flush() if err != nil { log.Println("flush log file err", err) } fileWriter.Mu.RUnlock()
首先明確一個問題,在每日結束第二天開始時將打開一個新的日誌文件,那麼還在緩衝區未完成刷新的數據怎麼處理呢?
bufio提供了Reset方法,可是該方法註釋說明將丟棄未刷新的數據而直接從新指向新的io writer,所以咱們不能直接使用該方法,不然這個時間節點附近的數據將會丟掉
實際測試證實若是先關閉原IO,再從新建立新的文件描述符,最後調用Reset方法指向新的描述符事後這段時間將會丟掉達約20ms的數據
基於此,咱們使用了二級指針:
1.判斷當前日期是否就是今天,若是是則等待下個判斷週期,若是不是則開始準備指針重定向操做
2.判斷哪個文件描述符爲空,若是爲空則爲其建立新的描述符,並指定配對的filebuffer,若是不爲空則說明它就是當前正在操做的文件
3.將filewriter指向新的filebuffer
4.對老的filebuffer進行Flush操做,以後將filebuffer和file都置爲空
通過這樣的操做事後,跨天的日誌文件處理就不會有數據丟失的狀況了
if today != time.Now().Day() { today = time.Now().Day() if file[0] == nil { file[0], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatal("open log file err: ", err) } fileBuffer[0] = bufio.NewWriterSize(file[0], 4096) fileWriter.Writer = fileBuffer[0] if fileBuffer[1].Buffered() > 0 { fileBuffer[1].Flush() } fileBuffer[1] = nil file[1].Close() file[1] = nil } else if file[1] == nil { file[1], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatal("open log file err: ", err) } fileBuffer[1] = bufio.NewWriterSize(file[1], 4096) fileWriter.Writer = fileBuffer[1] if fileBuffer[0].Buffered() > 0 { fileBuffer[0].Flush() } fileBuffer[0] = nil file[0].Close() file[0] = nil } }
一個基本的概念就是程序在操做系統退出的時候一般都會獲得系統信號,好比linux的kill操做就是給應用程序發送系統信號
信號分不少種,好比常見的ctrl+c對應的信號則是Interrupt信號,這塊去搜索「posix信號」也有詳細的解釋說明
基於此,常規的異常處理咱們能夠捕獲系統信號而後作一些結束前的處理操做,這樣的信號能夠在多個包同時使用,均會收到信號,不用擔憂信號強佔的問題
好比這個包在接收到退出信號時則刷新全部緩存數據並關閉全部文件描述符:
func exitHandle() { <-exitChan if file != nil { if fileWriter.Writer.Buffered() > 0 { fileWriter.Writer.Flush() } //及時關閉file句柄 if file[0] != nil { file[0].Close() } if file[1] != nil { file[1].Close() } } os.Exit(1) //使用os.Exit強行關掉 }
package logger import ( "bufio" "bytes" "encoding/json" "fmt" "log" "os" "os/signal" "xxx/config" "sync" "syscall" "time" ) var file []*os.File var err error var fileBuffer []*bufio.Writer var exitChan chan os.Signal type fileWriterS struct { Writer *bufio.Writer Mu sync.RWMutex } var fileWriter fileWriterS var today int func LoggerInit() { filePath := config.Get("app", "log_path") //config處能夠直接換成本身的config甚至直接寫死 _, err := os.Stat(filePath) if err != nil { if os.IsNotExist(err) { os.MkdirAll(filePath, os.ModePerm) } else { log.Fatal("log path err:", err) } } file = make([]*os.File, 2) file[0] = nil file[1] = nil fileBuffer = make([]*bufio.Writer, 2) fileBuffer[0] = nil fileBuffer[1] = nil today = time.Now().Day() file[0], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatal("open log file err: ", err) } fileBuffer[0] = bufio.NewWriterSize(file[0], 4096) fileWriter.Writer = fileBuffer[0] exitChan = make(chan os.Signal) signal.Notify(exitChan, os.Interrupt, os.Kill, syscall.SIGTERM) go exitHandle() go func() { time.Sleep(1 * time.Second) for { if fileWriter.Writer.Buffered() > 0 { fileWriter.Mu.RLock() err = fileWriter.Writer.Flush() if err != nil { log.Println("flush log file err", err) } fileWriter.Mu.RUnlock() } time.Sleep(1 * time.Second) if today != time.Now().Day() { today = time.Now().Day() if file[0] == nil { file[0], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatal("open log file err: ", err) } fileBuffer[0] = bufio.NewWriterSize(file[0], 4096) fileWriter.Writer = fileBuffer[0] if fileBuffer[1].Buffered() > 0 { fileBuffer[1].Flush() } fileBuffer[1] = nil file[1].Close() file[1] = nil } else if file[1] == nil { file[1], err = os.OpenFile(filePath+"/"+config.Get("app", "name")+"_"+time.Now().Format("2006-01-02")+".log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatal("open log file err: ", err) } fileBuffer[1] = bufio.NewWriterSize(file[1], 4096) fileWriter.Writer = fileBuffer[1] if fileBuffer[0].Buffered() > 0 { fileBuffer[0].Flush() } fileBuffer[0] = nil file[0].Close() file[0] = nil } } } }() } func exitHandle() { <-exitChan if file != nil { if fileWriter.Writer.Buffered() > 0 { fileWriter.Writer.Flush() } if file[0] != nil { file[0].Close() } if file[1] != nil { file[1].Close() } } os.Exit(1) } func Debug(msg map[string]interface{}) { writeLog("DEBUG", msg) } func DebugMsg(msg interface{}) { writeLog("DEBUG", map[string]interface{}{"msg": msg}) } func Info(msg map[string]interface{}) { writeLog("INFO", msg) } func InfoMsg(msg interface{}) { writeLog("INFO", map[string]interface{}{"msg": msg}) } func Warn(msg map[string]interface{}) { writeLog("WARN", msg) } func WarnMsg(msg interface{}) { writeLog("WARN", map[string]interface{}{"msg": msg}) } func Error(msg map[string]interface{}) { writeLog("ERROR", msg) } func ErrorMsg(msg interface{}) { writeLog("ERROR", map[string]interface{}{"msg": msg}) } func writeLog(level string, msg map[string]interface{}) { will_write_map := make(map[string]interface{}) t := time.Now() will_write_map["@timestamp"] = fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d.%03d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6) will_write_map["level"] = level for p, v := range msg { will_write_map[p] = v } //buffer滿了會自動flush bf := bytes.NewBuffer([]byte{}) jsonEncoder := json.NewEncoder(bf) jsonEncoder.SetEscapeHTML(false) jsonEncoder.Encode(will_write_map) a := bf.String() // fmt.Println(a) fileWriter.Mu.Lock() fileWriter.Writer.WriteString(a) fileWriter.Mu.Unlock() // fileWriter.WriteString("\n") }