基於go開發日誌處理包

silsuer在掘金發表於2018-10-22

最近在自己開發的go語言web框架 Bingo 中需要一個日誌處理功能 , 看了看標準庫的log包, 發現功能過於簡單,所以想重新造個輪子,單獨抽出來作為一個模組,輔助框架進行開發

[bingo-log] 是為了完成 bingo 的日誌功能而開發的一個第三方包,不依賴框架,可單獨在其他專案中使用,

Github地址: bingo-log

安裝和使用在 README.md 中已經寫的很清楚了,這裡不再贅述,主要記錄開發流程。

1. 預期效果

我希望這個包包含的功能:

  1. 支援多種報錯級別
  2. 日誌自定義配置並自動分割
  3. 可非同步輸出日誌

2. 實現思路

準備使該日誌包支援(FATAL,ERROR,WARNING,DEBUG,INFO) 5種報錯級別,

寫一個日誌結構體作為基礎,在其中設定一個介面型別的資料,將允許自定義的方法放在這個介面中,這樣所有實現該介面的物件都可以作為引數傳入日誌結構體中

如何實現非同步功能?

為了可以限制資源消耗,使用協程連線池將每個輸出放入協程池中,達到非同步的效果,

連線池我就不重複造輪子了,使用一個現成的github專案: grpool

開始開發

  1. 構建最基礎的底:日誌結構體

首先宣告兩個常量,用來標記同步輸出還是非同步輸出

const (
	LogSyncMode = iota
	LogPoolMode
)
複製程式碼

構建結構體

type Log struct {
	Connector                    // 內嵌聯結器,用來定製化功能
	sync.Mutex
	initialized     bool         // 該日誌物件是否初始化
	mode            int          // 日誌記錄模式  同步記錄 or 協程池記錄
	pool            *grpool.Pool // 協程池
	poolExpiredTime int          // 協程池模式下,每個空閒協程的存活時間(秒)
	poolWorkerNum   int          // 協程池模式下,允許的最高協程數
}
複製程式碼
  1. 構建聯結器介面

我們希望使用聯結器來設定每種輸出,所以這個介面應該實現如下幾種方法

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引數就是外部傳進來的引數

  1. 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, 我實現了兩種ConnectorBaseConnectorKirinConnector)那麼我們就實現一個基礎聯結器BaseConnector:

  • 建立一個結構體

      type BaseConnector struct {
      	sync.Mutex  // 這裡是因為有用到map的地方需要加鎖
      }
    複製程式碼
  • 實現聯結器介面:

    1. 先實現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
      }
    複製程式碼
    1. 實現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)
      }
    
    複製程式碼
    1. 實現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"
          }
    複製程式碼
    1. 實現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() 即可

    這樣,一個基本的聯結器就製作好了,我們可以隨時自行擴充套件

小結

使用方式類似於:

   
  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 ~~~

相關文章