Go語言 日志組件編寫

在使用Go語言的時候,發(fā)現標準庫的日志log功能很初級拉鹃。

log.Println(i)
log.Printf(format, i...)
log.Fatalln(i)
log.Panic()
...

基本也就圍繞上面幾個方法來做日志處理鲫忍。于是我上Github查找第三方庫。在看了幾個log庫之后坝辫,覺得不是滿意射亏。我打算自己嘗試實現log日志組件。

最后實現log的使用如下:

// 標準日志
l := logs.NewLogger()
l.SetLogLevel(logs.WARN)  //設置日志輸出等級
l.Debug("Debug")
l.Info("Info")
l.Notice("Notice")
l.Warn(100)
l.Error("Error")
l.Critical("Critical")
l.Fatal("Fatal")
// 保存到本地log文件的日志
l := logs.NewRotateFileLogger("/Users/xxx/logs")   // 設置log日志文件存放路徑
l.SetLogLevel(logs.INFO)                             //設置日志輸出等級
l.SetNewFileGapTime(time.Hour)           // 設置每個小時創(chuàng)建新的log文件
l.Notice("Notice")
l.Warn(100)
l.Error("Error")
l.Critical("Critical")
l.Fatal("Fatal")

性能上通過Benchmark分析輸出一個Hello

平均每次執(zhí)行時間:4177ns/p
內存使用情況:168 B/op 和 7 allocs/op

完全可以應對每秒上萬的輸出及舍。性能達到要求了窟绷。

設計思考

我對組件一直有這樣的思路,

  1. 組件要易于上手攘残,減少學習成本。盡量減少不必要的概念和抽象遗契。

  2. 初始的組件能滿足最常用的使用場景病曾,開箱即可用。滿足80%的情況知态。同時又易于拓展负敏,特別需求可以簡單通過組件的拓展來封裝贡茅。

  3. 組件API精簡顶考,設計上更符合人們對組件直觀的認知妖泄。

開發(fā)前

對日志組件的需求,大部分情況無非一幾點

  1. 帶有Info蹈胡、Warn罚渐、Error等等的不同等級的輸出。
  2. 可以設置輸出等級荷并。
  3. 輸出內容格式可以自定義。
  4. 支持log文件的寫入翩伪。
  5. 最好也支持過濾Filter輸出的功能谈息。
設計API

初定的API

l.SetLogLevel(level)  //設置日志輸出等級
l.Debug(...)
l.Info(...)
l.Notice(...)
l.Warn(...)
...
l.SetFormat(...)  //設置輸出格式

前面的幾個API比較好理解,但是在設計輸出格式SetFormat的時候囊颅,我查看了幾個第三方庫,特別容易在這里引入概念和抽象層盲憎。設置還有封裝出格式化結構的struct來定義格式的胳挎。這樣的設計明顯不符合我之前說的設計思考的幾點,于是我查看了官方的log慕爬。官方的log很簡樸但是很好理解,一個format字符串+對應format字符串格式的內容磅甩。例如格式%s-%s 對應2017/11/26 和Hello姥卢。這種方式開發(fā)者都熟悉,比較適合僧叉。所以我現在了官方的方式來自定義格式棺榔。

可是用一個SetFormat來傳入參數定義格式其實很難封裝得好,因為格式和前面的Info(data)有點不好封裝症歇,格式和data可以對應,但是擴展很弱宛蚓。
我想輸出是2017/11/26 Hello德频,定義好格式后缩幸,data不能簡單的是Hello,Info(data) 這里的data就要自己再加入時間2017/11/26钞护。

于是我選擇了用傳入函數來定義格式輸出爆办。

logFormatFunc func(logType LogType, i interface{}) (string, []interface{}, bool)

logType標記輸出的等級,i表示輸出的日志余佃,
函數返回(string, []interface{},bool)
string是格式 例如%s-%s。 []interface{}表示返回格式的內容爆土。
bool主要是滿足做Filter過濾器

好了 先看看Logger默認的格式定義函數

func (l *Logger) DefaultLogFormatFunc(logType LogType, i interface{})  (string, []interface{}, bool){
    format := "\033["+ logTypesColors[logType] + "m%s [%s] %s \033[0m\n"
    layout :="2006-01-02 15:04:05.999"
    formatTime := time.Now().Format(layout)
    if len(formatTime) != len(layout) {
        // 可能出現結尾是0被省略 如 2006-01-02 15:04:05.9 2006-01-02 15:04:05.99步势,補上
        formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
    }

    values := make([]interface{}, 3)
    values[0] = logTypeStrings[logType]
    values[1] = formatTime
    values[2] = fmt.Sprint(i)

    return format, values, true
}

其實就是格式定義是%s [%s] %s
返回[]interface{} 對應格式的 3個%s。

編寫Logger

提供Info(...) Warn(...) Error(...) 和 SetLogLevel(level) 再提供了SetLoggerFormat設置自定義格式輸出等等功能盅抚。
接口少但易于拓展倔矾。

通過SetLoggerFormat 我拓展實現了log文件的RotateFileLogger

RotateFileLogger的格式函數如下:


func (l *RotateFileLogger) DefaultLogFormatFunc(logType LogType, i interface{})  (string, []interface{}, bool){
    format := "%s [%s] %s\n"
    now := time.Now()
    gapTime := now.Sub(l.lastFileTime)
    if gapTime > l.newFileGapTime && l.newFileGapTime > 0{
        l.file.Close()

        rate := int(int64(gapTime) / int64(l.newFileGapTime))
        l.lastFileTime = l.lastFileTime.Add(l.newFileGapTime * time.Duration(rate))
        file, err := l.createLogFile(l.fileNameFormatFunc(l.lastFileTime))
        if err != nil{
            file.Close()
            panic(err)
        }

        l.file = file
        l.out = file
    }

    layout :="2006-01-02 15:04:05.999"
    formatTime := now.Format(layout)
    defer func() {
        e := recover()
        if e!=nil{
            panic(debug.Stack())
        }
    }()
    if len(formatTime) != len(layout) {
        // 對于如果是 出現2006-01-02 15:04:05.99  適配處理 成2006-01-02 15:04:05.990
        formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
    }

    values := make([]interface{}, 3)
    values[0] = GetLogTypeString(logType)
    values[1] = formatTime
    values[2] = fmt.Sprint(i)

    return format, values, true
}

源碼 (2個文件)

logger.go

package logs

import (
    "sync"
    "io"
    "fmt"
    "time"
    "os"
    "strings"
)

type LogType int

const (
    DEBUG =     LogType(0)
    INFO =      LogType(1)
    NOTICE =    LogType(2)
    WARN =      LogType(3)
    ERROR =     LogType(4)
    CRITICAL =  LogType(5)
    FATAL =     LogType(6)
)

var logTypeStrings = func() []string{
    // log 類型對應的 名稱字符串破讨,用于輸出,所以統(tǒng)一了長度提陶,故 DEBUG 為 "DEBUG..." 和 "CRITICAL"等長
    types :=[]string{"DEBUG", "INFO", "NOTICE", "WARN", "ERROR", "CRITICAL", "FATAL"}
    maxTypeLen := 0
    for _, t := range types{
        if len(t) > maxTypeLen {
            maxTypeLen = len(t)
        }
    }
    for index, t := range types{
        typeLen := len(t)
        if typeLen < maxTypeLen{
            types[index] += strings.Repeat(".", maxTypeLen - typeLen)
        }
    }
    return types
}()

var logTypesColors = []string{"0;35", "1;36", "1;37", "0;33", "1;31", "1;31", "1;31"}

/*
    創(chuàng)建 Logger
 */
func NewLogger()  *Logger{
    logger := &Logger{}
    logger.Init()
    return logger
}

func GetLogTypeString(t LogType)  string{
    return logTypeStrings[t]
}

/*
    Logger 日志

    l := NewLogger()
    l.Info("hello")
    l.Warn(1)

    輸入格式可以通過 SetLoggerFormat 設置隙笆。默認輸出格式定義在 見Logger的DefaultLogFormatFunc

    可以通過 SetLogLevel 設置輸出等級。
 */
type Logger struct {
    mu              sync.Mutex
    out             io.Writer

    logFormatFunc   func(logType LogType, i interface{}) (string, []interface{}, bool)
    logLevel        LogType
}

func (l *Logger) Init() {
    l.mu.Lock()
    defer l.mu.Unlock()
    l.logFormatFunc = l.DefaultLogFormatFunc
    l.out = os.Stdout
    l.logLevel = DEBUG
}

func (l* Logger) SetLogLevel(logType LogType) {
    l.mu.Lock()
    defer l.mu.Unlock()
    l.logLevel = logType
}

func (l* Logger) GetLogLevel() LogType{
    l.mu.Lock()
    defer l.mu.Unlock()
    return l.logLevel
}

// 設置格式化log輸出函數
// 函數返回 format 和 對應格式 []interface{}
func (l *Logger) SetLoggerFormat(formatFunc func(logType LogType, i interface{}) (string, []interface{}, bool))  {
    l.mu.Lock()
    defer l.mu.Unlock()
    l.logFormatFunc = formatFunc
}

// 輸出信息
func (l *Logger) Debug(i interface{})  {
    l.log(DEBUG, i)
}

func (l *Logger) Info(i interface{}){
    l.log(INFO, i)
}

func (l *Logger) Notice(i interface{}){
    l.log(NOTICE, i)
}

func (l *Logger) Warn(i interface{})  {
    l.log(WARN, i)
}

func (l *Logger) Error(i interface{})  {
    l.log(ERROR, i)
}

func (l *Logger) Critical(i interface{}) {
    l.log(CRITICAL, i)
}

func (l *Logger) Fatal(i interface{}) {
    l.log(FATAL, i)
}



func (l *Logger) DefaultLogFormatFunc(logType LogType, i interface{})  (string, []interface{}, bool){
    format := "\033["+ logTypesColors[logType] + "m%s [%s] %s \033[0m\n"
    layout :="2006-01-02 15:04:05.999"
    formatTime := time.Now().Format(layout)
    if len(formatTime) != len(layout) {
        // 可能出現結尾是0被省略 如 2006-01-02 15:04:05.9 2006-01-02 15:04:05.99,補上
        formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
    }

    values := make([]interface{}, 3)
    values[0] = logTypeStrings[logType]
    values[1] = formatTime
    values[2] = fmt.Sprint(i)

    return format, values, true
}

func (l *Logger) log(logType LogType, i interface{})  {
    l.mu.Lock()
    defer l.mu.Unlock()

    if l.logLevel > logType{
        return
    }

    format, data, isLog := l.logFormatFunc(logType, i)
    if !isLog{ return}

    _, err := fmt.Fprintf(l.out, format, data...)
    if err !=nil{
        panic(err)
    }
}

file_logger.go

package logs

import (
    "os"
    "time"
    "fmt"
    "runtime/debug"
)

/*
 *
 */
func NewRotateFileLogger(dir string)  *RotateFileLogger{
    logger := &RotateFileLogger{}
    logger.Init(dir)
    return logger
}



// 一段時間自動創(chuàng)建新的log文件
// 如果在間隔時間無日志剪决,此間隔不會創(chuàng)建log文件
type RotateFileLogger struct {
    Logger

    file                    *os.File                // 正在操作文件
    dirPath                 string                  // logs 文件所在文件夾
    fileNameFormatFunc      func(t time.Time) string    // 獲取文件名稱格式

    newFileGapTime          time.Duration       // 創(chuàng)建新log的間隔時間
    lastFileTime            time.Time           // 上次創(chuàng)建文件檀训,文件對應時間(依據間隔時間,不是真實創(chuàng)建時間)
}

func (l * RotateFileLogger) Init(dir string) {
    l.Logger.Init()
    l.mu.Lock()
    defer l.mu.Unlock()

    l.fileNameFormatFunc = l.DefaultFileNameFormat
    l.logFormatFunc = l.DefaultLogFormatFunc
    l.newFileGapTime = 0
    l.lastFileTime = time.Now()
    l.dirPath = dir

    file, err := l.createLogFile(l.fileNameFormatFunc(l.lastFileTime))
    if err != nil{
        panic(err)
        return
    }

    l.file = file
    l.out = file
}

func (l * RotateFileLogger) SetNewFileGapTime(gapTime time.Duration)  {
    l.mu.Lock()
    defer l.mu.Unlock()
    l.newFileGapTime = gapTime
}
// 默認 創(chuàng)建格式化的文件名. fileTime 不一定是創(chuàng)建文件的時間
func (l * RotateFileLogger) DefaultFileNameFormat(fileTime time.Time) string{
    // tips: linux 不支持 2006-01-02 15:04:05.999 ":"名稱
    // tips: os x  2006/01-02 15-04-05-999 帶有"/"報no such file or directory
    layout :="2006-01-02 15-04-05-999"
    formatTime := fileTime.Format(layout)
    if len(formatTime) != len(layout) {
        // 對于如果是 出現2006-01-02 15:04:05.99  適配處理 成2006-01-02 15:04:05.990
        formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
    }
    return formatTime + ".log"
}

func (l * RotateFileLogger) createLogFile(filename string)  (*os.File, error){

    if len(l.dirPath) != 0{
        filename = l.dirPath + "/" + filename
        err:=os.MkdirAll(l.dirPath, 0777)
        if err != nil{
            panic(err)
        }
    }

    file, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
    if err != nil{
        return nil, err
    }
    return file, nil
}

func (l *RotateFileLogger) DefaultLogFormatFunc(logType LogType, i interface{})  (string, []interface{}, bool){
    format := "%s [%s] %s\n"
    now := time.Now()
    gapTime := now.Sub(l.lastFileTime)
    if gapTime > l.newFileGapTime && l.newFileGapTime > 0{
        l.file.Close()

        rate := int(int64(gapTime) / int64(l.newFileGapTime))
        l.lastFileTime = l.lastFileTime.Add(l.newFileGapTime * time.Duration(rate))
        file, err := l.createLogFile(l.fileNameFormatFunc(l.lastFileTime))
        if err != nil{
            file.Close()
            panic(err)
        }

        l.file = file
        l.out = file
    }

    layout :="2006-01-02 15:04:05.999"
    formatTime := now.Format(layout)
    defer func() {
        e := recover()
        if e!=nil{
            panic(debug.Stack())
        }
    }()
    if len(formatTime) != len(layout) {
        // 對于如果是 出現2006-01-02 15:04:05.99  適配處理 成2006-01-02 15:04:05.990
        formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
    }

    values := make([]interface{}, 3)
    values[0] = GetLogTypeString(logType)
    values[1] = formatTime
    values[2] = fmt.Sprint(i)

    return format, values, true
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末荧琼,一起剝皮案震驚了整個濱河市差牛,隨后出現的幾起案子,更是在濱河造成了極大的恐慌偏化,老刑警劉巖镐侯,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異搭伤,居然都是意外死亡袜瞬,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門拍鲤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汞扎,“玉大人,你說我怎么就攤上這事澈魄。” “怎么了痹扇?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵鲫构,是天一觀的道長。 經常有香客問我结笨,道長,這世上最難降的妖魔是什么伐憾? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任算途,我火速辦了婚禮蚀腿,結果婚禮上扫外,老公的妹妹穿的比我還像新娘廓脆。我一直安慰自己,他們只是感情好停忿,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布席赂。 她就那樣靜靜地躺著吮铭,像睡著了一般颅停。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纸肉,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天喊熟,我揣著相機與錄音,去河邊找鬼芥牌。 笑死,一個胖子當著我的面吹牛拐叉,可吹牛的內容都是我干的扇商。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蔬芥,長吁一口氣:“原來是場噩夢啊……” “哼控汉!你這毒婦竟也來了?” 一聲冷哼從身側響起姑子,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谢翎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體森逮,經...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡褒侧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了烟央。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歪脏。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖怠硼,靈堂內的尸體忽然破棺而出移怯,到底是詐尸還是另有隱情,我是刑警寧澤舟误,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站眯牧,受9級特大地震影響赖草,放射性物質發(fā)生泄漏。R本人自食惡果不足惜秧骑,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一乎折、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骂澄,春花似錦、人聲如沸磨镶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽矩桂。三九已至,卻和暖如春雹锣,著一層夾襖步出監(jiān)牢的瞬間癞蚕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工桦山, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人会放。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓钉凌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親矢沿。 傳聞我的和親對象是個殘疾皇子酸纲,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現摄狱,斷路器无午,智...
    卡卡羅2017閱讀 134,656評論 18 139
  • 在應用程序中添加日志記錄總的來說基于三個目的:監(jiān)視代碼中變量的變化情況,周期性的記錄到文件中供其他應用進行統(tǒng)計分析...
    時待吾閱讀 5,046評論 1 13
  • 在應用程序中添加日志記錄總的來說基于三個目的:監(jiān)視代碼中變量的變化情況酣衷,周期性的記錄到文件中供其他應用進行統(tǒng)計分析...
    時待吾閱讀 4,985評論 0 6
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,124評論 25 707