歡迎關注「Keegan小鋼」公衆號獲取更多文章緩存
撮合引擎開發:開篇數據結構
撮合引擎開發:對接黑箱post
咱們都知道日誌在一個程序中有着重要的做用,撮合引擎也一樣須要一個完善的日誌輸出功能,以方便調試和查詢數據。spa
對一個撮合引擎來講,須要輸出的日誌主要有如下幾類:設計
另外,撮合引擎產生的日誌會很是多,因此還應該作日誌分割,按日期分割是最經常使用的日誌分割方式,因此咱們也一樣將不一樣日期的日誌分割到不一樣日誌文件保存。
首先,咱們都知道日誌是有分級別的,多的好比 log4j 定義了 8 種級別的日誌。不過,最經常使用的就 4 種級別,優先級從低到高分別爲:DEBUG、INFO、WARN、ERROR。通常,不一樣環境會設置不一樣的日誌級別,如 DEBUG 級別通常只在開發和測試環境才設置,生產環境則會設置爲 INFO 或更高級別。當設置爲高級別時,低級別的日誌消息是不會打印出來的。那爲了打印不一樣級別的日誌消息,能夠提供不一樣級別的打印函數,好比提供 log.Debug()、log.Info() 等函數。
其次,日誌須要輸出到文件保存,所以,就須要指定文件保存的目錄、文件名和文件對象。通常,保存的文件目錄和運行程序應該放在一塊兒,因此,指定的文件目錄最好是相對路徑。
另外,文件還要根據日期作分割,即不一樣日期的日誌消息要保存到不一樣的日誌文件,那麼,天然要記錄下當前日誌的日期。以及須要定時監控,當檢測到最新日期跟當前日誌的日期相比已經跨日了,說明須要進行日誌分割了,那就將當前的日誌文件進行備份,並建立新文件用來保存新日期的日誌消息。
最後,日誌消息寫入文件的話,那就少不了耗時的 I/O 操做,若是用同步方式寫日誌,無疑會減低撮合性能,所以,最好選用異步方式寫日誌,能夠用帶緩衝的通道實現。
我從新自定義了一個 log 包,並建立了 log.go 文件,全部代碼都寫在該文件中。
第一步,先定義幾種日誌等級,直接定義成枚舉類型,以下:
type LEVEL byte
const (
DEBUG LEVEL = iota
INFO
WARN
ERROR
)
複製代碼
第二步,定義日誌的結構體,其包含的字段比較多,以下:
type FileLogger struct {
fileDir string // 日誌文件保存的目錄
fileName string // 日誌文件名(無需包含日期和擴展名)
prefix string // 日誌消息的前綴
logLevel LEVEL // 日誌等級
logFile *os.File // 日誌文件
date *time.Time // 日誌當前日期
lg *log.Logger // 系統日誌對象
mu *sync.RWMutex // 讀寫鎖,在進行日誌分割和日誌寫入時須要鎖住
logChan chan string // 日誌消息通道,以實現異步寫日誌
stopTickerChan chan bool // 中止定時器的通道
}
複製代碼
第三步,爲了能將日誌應用到程序中任何地方,就須要定義一個全局的日誌對象,並要對該日誌對象進行初始化。初始化操做有一點複雜,咱們先來看代碼:
const DATE_FORMAT = "2006-01-02"
var fileLog *FileLogger
func Init(fileDir, fileName, prefix, level string) error {
CloseLogger()
f := &FileLogger{
fileDir: fileDir,
fileName: fileName,
prefix: prefix,
mu: new(sync.RWMutex),
logChan: make(chan string, 5000),
stopTikerChan: make(chan bool, 1),
}
switch strings.ToUpper(level) {
case "DEBUG":
f.logLevel = DEBUG
case "WARN":
f.logLevel = WARN
case "ERROR":
f.logLevel = ERROR
default:
f.logLevel = INFO
}
t, _ := time.Parse(DATE_FORMAT, time.Now().Format(DATE_FORMAT))
f.date = &t
f.isExistOrCreateFileDir()
fullFileName := filepath.Join(f.fileDir, f.fileName+".log")
file, err := os.OpenFile(fullFileName, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
return err
}
f.logFile = file
f.lg = log.New(f.logFile, prefix, log.LstdFlags|log.Lmicroseconds)
go f.logWriter()
go f.fileMonitor()
fileLogger = f
return nil
}
複製代碼
這個初始化的邏輯有點多,我來進行拆分講解。首先,第一步,調用了 CloseLogger() 函數,該函數主要是關閉文件、關閉通道等操做。爲了中止一個不斷循環的 goroutine,關閉通道是一個經常使用的方案,這在以前的文章也有說過。那麼,因爲初始化函數能夠會被調用屢次,以實現配置的變動,那若是不先結束舊的 goroutine ,那一樣功能的 goroutine 將不止一個在同時運行,這無疑將會出問題。所以,須要先關閉 Logger,關閉 Logger 的代碼以下:
func CloseLogger() {
if fileLogger != nil {
fileLogger.stopTikerChan <- true
close(fileLogger.stopTikerChan)
close(fileLogger.logChan)
fileLogger.lg = nil
fileLogger.logFile.Close()
}
}
複製代碼
關閉 Logger 以後,就是對一些字段的初始化賦值了,其中,f.date 設置爲了當前日期,後面判斷是否須要分割就以這個日期爲條件。f.isExistOrCreateFileDir() 則會判斷日誌目錄是否存在,若是不存在則會建立該目錄。接着,將目錄、設置的文件名和添加的 .log 文件擴展名拼接在一塊兒,拼接出文件的完整名字並打開文件。以後就是用該文件來初始化系統日誌對象 f.lg 了,將日誌消息寫入文件時其實就是調用該對象的 Output() 函數。後面啓動了兩個 goroutine:一個用來監聽 logChan,實現將日誌消息寫入文件;一個用來定時監聽文件是否須要分割,須要分割時則實現分割。
接着,咱們就來看看這兩個 goroutine 的實現:
func (f *FileLogger) logWriter() {
defer func() { recover() }()
for {
str, ok := <-f.logChan
if !ok {
return
}
f.mu.RLock()
f.lg.Output(2, str)
f.mu.RUnlock()
}
}
func (f *FileLogger) fileMonitor() {
defer func() { recover() }()
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if f.isMustSplit() {
if err := f.split(); err != nil {
Error("Log split error: %v\n", err)
}
}
case <-f.stopTikerChan:
return
}
}
}
複製代碼
能夠看到 logWriter() 循環從 logChan 通道讀取日誌消息,當通道被關閉則退出,不然就調用 f.lg.Output() 將日誌輸出。fileMonitor() 裏則建立了一個每隔 30 秒發送一次的 ticker,當從 ticker.C 接收到數據以後,就判斷是否須要分割,若是須要則調用分割函數 f.split()。而從 f.stopTikerChan 收到數據時,說明該定時器也要結束了。
接着,再來看看 isMustSplit() 和 split() 函數了。isMustSplit() 很是簡單,就兩行代碼,以下:
func (f *FileLogger) isMustSplit() bool {
t, _ := time.Parse(DATE_FORMAT, time.Now().Format(DATE_FORMAT))
return t.After(f.date)
}
複製代碼
split() 則複雜些,首先對日誌要先加寫鎖,避免分割時依然有日誌寫入,接着對當前的日誌文件進行重命名備份,而後生成新文件用來記錄新的日誌消息,並將當前的全局日誌對象指向新文件、新日期和新的系統日誌對象。實現代碼以下:
func (f *FileLogger) split() error {
f.mu.Lock()
defer f.mu.Unlock()
logFile := filepath.Join(f.fileDir, f.fileName)
logFileBak := logFile + "-" + f.date.Format(DATE_FORMAT) + ".log"
if f.logFile != nil {
f.logFile.Close()
}
err := os.Rename(logFile, logFileBak)
if err != nil {
return err
}
t, _ := time.Parse(DATE_FORMAT, time.Now().Format(DATE_FORMAT))
f.date = &t
f.logFile, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
return err
}
f.lg = log.New(f.logFile, f.prefix, log.LstdFlags|log.Lmicroseconds)
return nil
}
複製代碼
最後,就剩下定義一些接收日誌消息的函數了,實現都很簡單,以 Info() 爲例:
func Info(format string, v ...interface{}) {
_, file, line, _ := runtime.Caller(1)
if fileLogger.logLevel <= INFO {
fileLogger.logChan <- fmt.Sprintf("[%v:%v]", filepath.Base(file), line) + fmt.Sprintf("[INFO]"+format, v...)
}
}
複製代碼
Debug()、Warn()、Error() 等函數都相似的,照貓畫虎便可。
至此,咱們這個可以實現按日期分割日誌文件的日誌包就完成了,剩下的,就在對應須要添加日誌輸出的地方調用響應的日誌等級函數便可。
本小結的核心實際上是增長了一個通用的日誌包,該日誌包不只能夠用在咱們的撮合引擎,也能用於其餘項目。若是再將其擴展,還能夠改成按其餘條件分割,好比按小時分割,或按文件大小分割。有興趣的小夥伴能夠本身去嘗試一下。
今日的思考題:要實現接口的請求和響應數據進行統一的日誌輸出,有哪些方案?
掃描如下二維碼便可關注公衆號(公衆號名稱:Keegan小鋼)