天天看點

go logrus包最好的日志管理詳解

文章目錄

  • ​​1. logrus介紹​​
  • ​​2. logrus特性​​
  • ​​3. 基本用法​​
  • ​​3.1 輸出至終端平台​​
  • ​​3.2 輸出至檔案​​
  • ​​3.3 設定日志輸出格式與日志輸出級别​​
  • ​​3.4 自定義Logger​​
  • ​​3.5 Fields用法​​
  • ​​3.6 hook​​
  • ​​3.6.1 Logrus-Hook-Email​​
  • ​​3.6.2 将日志發送到其他位置​​
  • ​​3.6.3 Logrus-Hook 日志分隔​​
  • ​​3.7 Fatal處理​​
  • ​​3.8 線程安全​​
  • ​​3.9 輸出代碼檔案名​​

1. logrus介紹

golang标準庫的日志架構非常簡單,僅僅提供了print,panic和fatal三個函數對于更精細的日志級别、日志檔案分割以及日志分發等方面并沒有提供支援. 是以催生了很多第三方的日志庫,但是在golang的世界裡,沒有一個日志庫像slf4j那樣在Java中具有絕對統治地位.golang中,流行的日志架構包括logrus、zap、zerolog、seelog等.

logrus是目前Github上star數量最多的日志庫,目前(2018.12,下同)star數量為8119,fork數為1031. logrus功能強大,性能高效,而且具有高度靈活性,提供了自定義插件的功能.很多開源項目,如docker,prometheus,dejavuzhou/ginbro等,都是用了logrus來記錄其日志.

zap是Uber推出的一個快速、結構化的分級日志庫.具有強大的ad-hoc分析功能,并且具有靈活的儀表盤.zap目前在GitHub上的star數量約為4.3k. seelog提供了靈活的異步排程、格式化和過濾功能.目前在GitHub上也有約1.1k。

2. logrus特性

  • 完全相容golang标準庫日志子產品:logrus擁有六種日志級别:​

    ​debug​

    ​​、​

    ​info​

    ​​、​

    ​warn​

    ​​、​

    ​error​

    ​​、​

    ​fatal​

    ​​和​

    ​panic​

    ​,這是golang标準庫日志子產品的API的超集.如果您的項目使用标準庫日志子產品,完全可以以最低的代價遷移到logrus上.

第三方庫需要先安裝:

$ ;go get github.com/sirupsen/logrus      

後使用:

package main

import (
  "github.com/sirupsen/logrus"
)

func main() {
  logrus.SetLevel(logrus.TraceLevel)

  logrus.Trace("trace msg")
  logrus.Debug("debug msg")
  logrus.Info("info msg")
  logrus.Warn("warn msg")
  logrus.Error("error msg")
  logrus.Fatal("fatal msg")
  logrus.Panic("panic msg")
}      

logrus的使用非常簡單,與标準庫log類似。logrus支援更多的日志級别:

Panic:記錄日志,然後panic。

Fatal:緻命錯誤,出現錯誤時程式無法正常運轉。輸出日志後,程式退出;

Error:錯誤日志,需要檢視原因;

Warn:警告資訊,提醒程式員注意;

Info:關鍵操作,核心流程的日志;

Debug:一般程式中輸出的調試資訊;

Trace:很細粒度的資訊,一般用不到;

日志級别從上向下依次增加,Trace最大,Panic最小。logrus有一個日志級别,高于這個級别的日志不會輸出。預設的級别為InfoLevel。是以為了能看到Trace和Debug日志,我們在main函數第一行設定日志級别為TraceLevel。

運作程式,輸出:

$ ;go run main.go
time="2020-02-07T21:22:42+08:00" ;level=trace ;msg="trace msg"
time="2020-02-07T21:22:42+08:00" ;level=debug ;msg="debug msg"
time="2020-02-07T21:22:42+08:00" ;level=info ;msg="info msg"
time="2020-02-07T21:22:42+08:00" ;level=info ;msg="warn msg"
time="2020-02-07T21:22:42+08:00" ;level=error ;msg="error msg"
time="2020-02-07T21:22:42+08:00" ;level=fatal ;msg="fatal msg"
exit status 1      

由于logrus.Fatal會導緻程式退出,下面的logrus.Panic不會執行到。

另外,我們觀察到輸出中有三個關鍵資訊,time、level和msg:

time:輸出日志的時間;

level:日志級别;

msg:日志資訊。

  • 可擴充的Hook機制:允許使用者通過hook的方式将日志分發到任意地方,如本地檔案系統、标準輸出、logstash、elasticsearch或者mq等,或者通過hook定義日志内容和格式等.

    可選的日志輸出格式:logrus内置了兩種日志格式,JSONFormatter和TextFormatter,如果這兩個格式不滿足需求,可以自己動手實作接口Formatter,來定義自己的日志格式.

  • Field機制:logrus鼓勵通過Field機制進行精細化的、結構化的日志記錄,而不是通過冗長的消息來記錄日志.
  • logrus是一個可插拔的、結構化的日志架構.
  • Entry: logrus.WithFields會自動傳回一個 *Entry,Entry裡面的有些變量會被自動加上
time:entry被建立時的時間戳
msg:在調用.Info()等方法時被添加
level      

3. 基本用法

3.1 輸出至終端平台

package main

import "github.com/sirupsen/logrus"

func main() {
    logrus.WithFields(logrus.Fields{
        "animal": "walrus",
        "number": 1,
        "size":   10,
    }).Info("A walrus appears")
}      
$ ; go run logrus1.go
INFO[0000] A walrus appears                              ;animal=walrus ;number=1 ;size=10      

3.2 輸出至檔案

package main

import (
  "github.com/sirupsen/logrus"
  "os"
)
func main() {
  log := logrus.New()
  file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
  if err == nil {
     log.Out = file
  } else {
     log.Info("Failed to log to file, using default stderr")
  }
  log.Info("log-- log--")

}      

輸出:

$ ;cat logrus.log 
time="2020-05-26T10:29:20+08:00" ;level=info ;msg="log-- log--"      

3.3 設定日志輸出格式與日志輸出級别

文法:

logrus.SetFormatter(&logrus.JSONFormatter{})
logrus.Infoln("JSONFormatter")

logrus.SetFormatter(&logrus.TextFormatter{})
logrus.Infoln("TextFormatter not time")      

示例1

package main

import (
    "os"
    log "github.com/sirupsen/logrus"
)

func init() {
    // 設定日志格式為json格式
    log.SetFormatter(&log.JSONFormatter{})

    // 設定将日志輸出到标準輸出(預設的輸出為stderr,标準錯誤)
    // 日志消息輸出可以是任意的io.writer類型
    log.SetOutput(os.Stdout)

    // 設定日志級别為warn以上
    log.SetLevel(log.WarnLevel)
}

func main() {
    log.WithFields(log.Fields{
        "animal": "walrus",
        "size":   10,
    }).Info("A group of walrus emerges from the ocean")

    log.WithFields(log.Fields{
        "omg":    true,
        "number": 122,
    }).Warn("The group's number increased tremendously!")

    log.WithFields(log.Fields{
        "omg":    true,
        "number": 100,
    }).Fatal("The ice breaks!")
}      

輸出:

$ ;go run logrus3.go
{"level":"warning","msg":"The group's number increased tremendously!","number":122,"omg":true,"time":"2020-05-26T10:53:39+08:00"}
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,"time":"2020-05-26T10:53:39+08:00"}
exit status 1      

3.4 自定義Logger

如果想在一個應用裡面向多個地方log,可以建立Logger執行個體. logger是一種相對進階的用法, 對于一個大型項目, 往往需要一個全局的logrus執行個體,即logger對象來記錄項目所有的日志.

文法:

log := logrus.New()
log.Formatter= new(logrus.JSONFormatter)
log.Infoln("my logger")      

示例1:輸出到終端

package main

import (
    "github.com/sirupsen/logrus"
    "os"
)

// logrus提供了New()函數來建立一個logrus的執行個體.
// 項目中,可以建立任意數量的logrus執行個體.
var log = logrus.New()

func main() {
    // 為目前logrus執行個體設定消息的輸出,同樣地,
    // 可以設定logrus執行個體的輸出到任意io.writer
    log.Out = os.Stdout

    // 為目前logrus執行個體設定消息輸出格式為json格式.
    // 同樣地,也可以單獨為某個logrus執行個體設定日志級别和hook,這裡不詳細叙述.
    log.Formatter = &logrus.JSONFormatter{}

    log.WithFields(logrus.Fields{
        "animal": "walrus",
        "size":   10,
    }).Info("A group of walrus emerges from the ocean")
}      

輸出:

$ ;go run logrus4.go
{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the ocean","size":10,"time":"2020-05-26T10:59:26+08:00"}      

示例2:輸出到檔案

package main

import (
    "github.com/sirupsen/logrus"
    "os"
)

var log = logrus.New()

func main() {
    file ,err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
    if err == nil{
        log.Out = file
    }else{
        log.Info("Failed to log to file")
    }

    log.WithFields(logrus.Fields{
        "filename": "123.txt",
    }).Info("打開檔案失敗")
}      

輸出:

$ ;cat logrus.log 
time="2020-05-26T11:08:07+08:00" ;level=info ;msg="打開檔案失敗" ;filename=123.txt      

3.5 Fields用法

logrus不推薦使用冗長的消息來記錄運作資訊,它推薦使用Fields來進行精細化的、結構化的資訊記錄. 例如下面的記錄日志的方式:

log.Fatalf("Failed to send event %s to topic %s with key %d", event, topic, key)      

在logrus中不太提倡,logrus鼓勵使用以下方式替代之:

log.WithFields(log.Fields{
  "event": event,
  "topic": topic,
  "key": key,
}).Fatal("Failed to send event")      

前面的WithFields API可以規範使用者按照其提倡的方式記錄日志.但是WithFields依然是可選的,因為某些場景下,使用者确實隻需要記錄儀一條簡單的消息.

通常,在一個應用中、或者應用的一部分中,都有一些固定的Field.比如在處理使用者http請求時,上下文中,所有的日志都會有request_id和user_ip.為了避免每次記錄日志都要使用log.WithFields(log.Fields{“request_id”: request_id, “user_ip”: user_ip}),我們可以建立一個logrus.Entry執行個體,為這個執行個體設定預設Fields,在上下文中使用這個logrus.Entry執行個體記錄日志即可.

package main

import (
    "github.com/sirupsen/logrus"
)

var log = logrus.New()

func main() {
    entry := logrus.WithFields(logrus.Fields{
        "name": "test",
    })
    entry.Info("message1")
    entry.Info("message2")
}      

輸出:

$ ;go run logrus6.go
INFO[0000] message1                                      ;name=test
INFO[0000] message2                                      ;name=test      

3.6 hook

hook的原理是,在logrus寫入日志時攔截,修改logrus.Entry

type Hook interface {
    Levels() []Level
    Fire(*Entry) error
}      

使用示例:

自定義一個hook DefaultFieldHook,在所有級别的日志消息中加入預設字段

appName="myAppName"

type DefaultFieldHook struct {
}

func (hook *DefaultFieldHook) Fire(entry *log.Entry) error {
    entry.Data["appName"] = "MyAppName"
    return nil
}

func (hook *DefaultFieldHook) Levels() []log.Level {
    return log.AllLevels
}      

在初始化時,調用logrus.AddHook(hook)添加響應的hook即可

logrus官方僅僅内置了syslog的hook. 此外,但Github也有很多第三方的hook可供使用,文末将提供一些第三方HOOK的連接配接.

3.6.1 Logrus-Hook-Email

email這裡隻需用NewMailAuthHook方法得到hook,再添加即可

package main

import (
    "time"

    //"github.com/logrus_mail"
        "github.com/zbindenren/logrus_mail"
    "github.com/sirupsen/logrus"
)

func main() {
    logger := logrus.New()
    hook, err := logrus_mail.NewMailAuthHook(
        "logrus_email",
        "smtp.163.com",
        25,
        "[email protected]",
        "[email protected]",
        "[email protected]",
        "xxxxxx",
    )
    if err == nil {
        logger.Hooks.Add(hook)
    }
    //生成*Entry
    var filename = "123.txt"
    contextLogger := logger.WithFields(logrus.Fields{
        "file":    filename,
        "content": "GG",
    })
    //設定時間戳和message
    contextLogger.Time = time.Now()
    contextLogger.Message = "這是一個hook發來的郵件"
    //隻能發送Error,Fatal,Panic級别的log
    contextLogger.Level = logrus.ErrorLevel

    //使用Fire發送,包含時間戳,message
    hook.Fire(contextLogger)
}      

3.6.2 将日志發送到其他位置

将日志發送到日志中心也是logrus所提倡的,雖然沒有提供官方支援,但是目前Github上有很多第三方hook可供使用:

​​logrus_amqp​​​:Logrus hook for Activemq。

​​​logrus-logstash-hook​​​:Logstash hook for logrus。

​​​mgorus​​​:Mongodb Hooks for Logrus。

​​​logrus_influxdb​​​:InfluxDB Hook for Logrus。

​​​logrus-redis-hook​​:Hook for Logrus which enables logging to RELK stack (Redis, Elasticsearch, Logstash and Kibana)

3.6.3 Logrus-Hook 日志分隔

logrus本身不帶日志本地檔案分割功能,但是我們可以通過file-rotatelogs進行日志本地檔案分割. 每次當我們寫入日志的時候,logrus都會調用file-rotatelogs來判斷日志是否要進行切分.

示例1:

package main

import (
    "time"

    rotatelogs "github.com/lestrrat-go/file-rotatelogs"
    log "github.com/sirupsen/logrus"
)

func init() {
    path := "/Users/opensource/test/go.log"
    /* 日志輪轉相關函數
    `WithLinkName` 為最新的日志建立軟連接配接
    `WithRotationTime` 設定日志分割的時間,隔多久分割一次
    WithMaxAge 和 WithRotationCount二者隻能設定一個
      `WithMaxAge` 設定檔案清理前的最長儲存時間
      `WithRotationCount` 設定檔案清理前最多儲存的個數
    */
    // 下面配置日志每隔 1 分鐘輪轉一個新檔案,保留最近 3 分鐘的日志檔案,多餘的自動清理掉。
    writer, _ := rotatelogs.New(
        path+".%Y%m%d%H%M",
        rotatelogs.WithLinkName(path),
        rotatelogs.WithMaxAge(time.Duration(180)*time.Second),
        rotatelogs.WithRotationTime(time.Duration(60)*time.Second),
    )
    log.SetOutput(writer)
    //log.SetFormatter(&log.JSONFormatter{})
}

func main() {
    for {
        log.Info("hello, world!")
        time.Sleep(time.Duration(2) * time.Second)
    }
}      

示例2:

package main
 
import (
    "github.com/lestrrat-go/file-rotatelogs"
    "github.com/pkg/errors"
    "github.com/rifflock/lfshook"
    log "github.com/sirupsen/logrus"
    "path"
    "time"
)
 
func ConfigLocalFilesystemLogger(logPath string, logFileName string, maxAge time.Duration, rotationTime time.Duration) {
    baseLogPaht := path.Join(logPath, logFileName)
    writer, err := rotatelogs.New(
        baseLogPaht+".%Y%m%d%H%M",
        //rotatelogs.WithLinkName(baseLogPaht), // 生成軟鍊,指向最新日志檔案
        rotatelogs.WithMaxAge(maxAge), // 檔案最大儲存時間
        rotatelogs.WithRotationTime(rotationTime), // 日志切割時間間隔
    )
    if err != nil {
        log.Errorf("config local file system logger error. %+v", errors.WithStack(err))
    }
    lfHook := lfshook.NewHook(lfshook.WriterMap{
        log.DebugLevel: writer, // 為不同級别設定不同的輸出目的
        log.InfoLevel:  writer,
        log.WarnLevel:  writer,
        log.ErrorLevel: writer,
        log.FatalLevel: writer,
        log.PanicLevel: writer,
    },&log.TextFormatter{DisableColors: true})
    log.AddHook(lfHook)
}
 
 
//切割日志和清理過期日志
func ConfigLocalFilesystemLogger1(filePath string) {
    writer, err := rotatelogs.New(
        filePath+".%Y%m%d%H%M",
        rotatelogs.WithLinkName(filePath),         // 生成軟鍊,指向最新日志檔案
        rotatelogs.WithMaxAge(time.Second*60*3),     // 檔案最大儲存時間
        rotatelogs.WithRotationTime(time.Second*60), // 日志切割時間間隔
    )
    if err != nil {
        log.Fatal("Init log failed, err:", err)
    }
    log.SetOutput(writer)
    log.SetLevel(log.InfoLevel)
}
 
func main()  {
    ConfigLocalFilesystemLogger1("log")
    for {
        log.Info(111)
        time.Sleep(500*time.Millisecond)
    }
}      

3.7 Fatal處理

和很多日志架構一樣,logrus的Fatal系列函數會執行os.Exit(1)。但是logrus提供可以注冊一個或多個fatal handler函數的接口​

​logrus.RegisterExitHandler(handler func() {} )​

​,讓logrus在執行os.Exit(1)之前進行相應的處理。fatal handler可以在系統異常時調用一些資源釋放api等,讓應用正确的關閉。

3.8 線程安全

預設情況下,logrus的api都是線程安全的,其内部通過互斥鎖來保護并發寫。互斥鎖工作于調用hooks或者寫日志的時候,如果不需要鎖,可以調用logger.SetNoLock()來關閉之。可以關閉logrus互斥鎖的情形包括:

  • 沒有設定hook,或者所有的hook都是線程安全的實作。
  • 寫日志到logger.Out已經是線程安全的了,如logger.Out已經被鎖保護,或者寫檔案時,檔案是以O_APPEND方式打開的,并且每次寫操作都小于4k。

3.9 輸出代碼檔案名

調用logrus.SetReportCaller(true)設定在輸出日志中添加檔案名和方法資訊:

package main

import (
  "github.com/sirupsen/logrus"
)

func main() {
  logrus.SetReportCaller(true)

  logrus.Info("info msg")
}      

輸出多了兩個字段file為調用logrus相關方法的檔案名,method為方法名:

$ ;go run main.go
time="2020-02-07T21:46:03+08:00" ;level=info ;msg="info msg" ;func=main.main ;file="D:/code/golang/src/github.com/darjun/go-daily-lib/logrus/caller/main.go:10"      

添加字段