文章目錄
- 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
,這是golang标準庫日志子產品的API的超集.如果您的項目使用标準庫日志子產品,完全可以以最低的代價遷移到logrus上.panic
第三方庫需要先安裝:
$ ;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"
添加字段