go logrus
一 概述
1.1 go日志现状
- Go标准库的日志框架非常简单,仅仅提供了Print,Panic和Fatal三个函数。对于更精细的日志级别、日志文件分割,以及日志分发等方面,并没有提供支持 。所以催生了很多第三方的日志库,比较流行的日志框架库包括logrus、zap、zerolog、seelog等。
1.2 logrus特性
- 完全兼容Go标准库日志模块。logrus拥有六种日志级别:debug、info、warn、error、fatal和panic,这是Go标准库日志模块的API的超集。如果你的项目使用标准库日志模块,完全可以用最低的代价迁移到logrus上。
- 可扩展的Hook机制。允许使用者通过hook方式,将日志分发到任意地方,如本地文件系统、标准输出、logstash、elasticsearch或者mq等,或者通过hook定义日志内容和格式等。
- 可选的日志输出格式。logrus内置了两种日志格式,JSONFormatter和TextFormatter。如果这两个格式不满足需求,可以自己动手实现接口Formatter,来定义自己的日志格式。
- Field机制。logrus鼓励通过Field机制进行精细化、结构化的日志记录,而不是通过冗长的消息来记录日志。
- logrus是一个可插拔的、结构化的日志框架。
1.3 logrus不提供的功能
- 输出到本地文件系统没有提供日志分割功能
- 没有提供输出到ELK等日志处理中心的功能
备注:这些功能都可以通过自定义hook来实现
二 安装
go get github.com/sirupsen/logrus
三 logrus简单使用
3.1 第一个示例
main.go
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
}).Info("A walrus appears")
}
结果:
D:\go\project\test_log>go run main.go
time="2020-11-06T09:47:39+08:00" level=info msg="A walrus appears" animal=walrus
3.2 第二个示例
logrus可以通过简单的配置,来定义输出、格式或者日志级别等。示例如下:
main.go
package main
import (
log "github.com/sirupsen/logrus"
"os"
)
func initLog() {
// 设置日志格式为json格式
log.SetFormatter(&log.JSONFormatter{})
// 设置将日志输出到标准输出(默认的输出为stderr,标准错误)
// 日志消息输出可以是任意的io.writer类型
log.SetOutput(os.Stdout)
// 设置日志级别为warn以上
log.SetLevel(log.WarnLevel)
// 记录文件名和行号
log.SetReportCaller(true)
}
func main() {
initLog()
log.WithFields(log.Fields{
"animal": "walrus",
"size": 10,
"country": "china",
}).Info("A group of walrus emerges from the ocean")
log.WithFields(log.Fields{
"omg": true,
"number": 122,
"country": "china",
}).Warn("The group's number increased tremendously!")
}
结果:
备注: 因为设置了log级别, 所以Info内容没有输出
D:\go\project\test_log>go run main.go
{"country":"china","file":"D:/go/project/test_log/main.go:28","func":"main.main","level":"warning","msg":"The group's number increased tremendously!","number":122,"omg":true,"time":"20
20-11-06T13:35:35+08:00"}
3.3 Logger
logger是一种相对高级的用法。对于一个大型项目,往往需要一个全局的logrus实例,即logger对象,来记录项目所有的日志。示例如下:
main.go
package main
import (
log "github.com/sirupsen/logrus"
"os"
)
// logrus提供了New()函数来创建一个logrus的实例。
// 项目中,可以创建任意数量的logrus实例。
var innerLog = log.New()
func main() {
// 为当前logrus实例设置消息的输出,同样地,
// 可以设置logrus实例的输出到任意io.writer
innerLog.Out = os.Stdout
// 为当前logrus实例设置消息输出格式为json格式。
// 同样地,也可以单独为某个logrus实例设置日志级别和hook,这里不详细叙述。
innerLog.Formatter = &log.JSONFormatter{}
innerLog.WithFields(log.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
}
结果:
D:\go\project\test_log>go run main.go
{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the ocean","size":10,"time":"2020-11-06T10:03:19+08:00"}
3.4 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实例记录日志即可。
requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
requestLogger.Info("something happened on that request") # will log request_id and user_ip
requestLogger.Warn("something not great happened")
四 Hook
logrus最令人心动的功能,就是其可扩展的HOOK机制了。通过在初始化时为logrus添加hook,logrus可以实现各种扩展功能。
// logrus在记录Levels()返回的日志级别的消息时,会触发HOOK。
// 然后,按照Fire方法定义的内容,修改logrus.Entry。
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
一个简单自定义的hook如下所示。DefaultFieldHook类型的对象会在所有级别的日志消息中加入默认字段appName=“myAppName”。
main.go
package main
import (
log "github.com/sirupsen/logrus"
"os"
)
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
}
var innerLog = log.New()
func main() {
innerLog.Out = os.Stdout
innerLog.Formatter = &log.JSONFormatter{}
// 增加Hook
var testHook = DefaultFieldHook{}
innerLog.AddHook(&testHook)
innerLog.WithFields(log.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
}
hook的使用也很简单,在初始化前调用log.AddHook(hook)添加相应的hook即可。
结果:
D:\go\project\test_log>go run main.go
{"animal":"walrus","appName":"MyAppName","level":"info","msg":"A group of walrus emerges from the ocean","size":10,"time":"2020-11-06T11:07:08+08:00"}
五 自定义日志格式
main.go
package main
import (
"bytes"
"fmt"
log "github.com/sirupsen/logrus"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
//日志自定义格式
type LogFormatter struct{}
//格式详情
func (s *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
timestamp := time.Now().Local().Format("0102-150405.000")
var file string
var len int
if entry.Caller != nil {
file = filepath.Base(entry.Caller.File)
len = entry.Caller.Line
}
//fmt.Println(entry.Data)
msg := fmt.Sprintf("%s [%s:%d][GOID:%d][%s] %s\n", timestamp, file, len, getGID(), strings.ToUpper(entry.Level.String()), entry.Message)
return []byte(msg), nil
}
// 获取协程号
func getGID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
func main() {
formatter := LogFormatter{}
log.SetOutput(os.Stdout)
log.SetFormatter(&formatter)
log.Info("A group of walrus emerges from the ocean")
}
结果
D:\go\project\test_log>go run main.go
1106-132649.376 [:0][GOID:1][INFO] A group of walrus emerges from the ocean
六 logrus 应用
6.1 Sucess Demo 1
通过Hook 配置日志
可以配置 一小时一个日志文件, 也可以配置一天一个日志文件
可以配置 不同模块,可以输出不同的日志文件
项目目录
local_file_hook.go
package logger
import (
"github.com/lestrrat-go/file-rotatelogs"
"github.com/rifflock/lfshook"
log "github.com/sirupsen/logrus"
"time"
)
func NewLfsHook(logPath, appName string, maxRemainCnt uint) log.Hook {
// hour
logName := logPath + "/" + "%Y%m%d%H" + "/" + appName + "-%Y%m%d%H"+".log"
rotationTime := time.Hour
// day
// logName := logPath + "/" + "%Y%m%d" + "/" + appName + "-%Y%m%d"+".log"
// rotationTime := 24 * time.Hour
writer, err := rotatelogs.New(
logName,
// WithLinkName为最新的日志建立软连接,以方便随着找到当前日志文件
rotatelogs.WithLinkName(logName),
// WithRotationTime设置日志分割的时间,这里设置为一小时分割一次
rotatelogs.WithRotationTime(rotationTime),
// WithMaxAge和WithRotationCount二者只能设置一个,
// WithMaxAge设置文件清理前的最长保存时间,
// WithRotationCount设置文件清理前最多保存的个数。
//rotatelogs.WithMaxAge(time.Hour*24),
rotatelogs.WithRotationCount(maxRemainCnt),
)
if err != nil {
log.Errorf("config local file system for logger error: %v", err)
}
lfsHook := 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})
return lfsHook
}
log.go
package logger
import (
log "github.com/sirupsen/logrus"
)
var GLog *log.Logger
func InitCommonLog(logPath string, appName string, logLevel log.Level) {
GLog = Setup(logPath, appName, logLevel)
}
//初始化日志
func Setup(logPath string, appName string, logLevel log.Level) *log.Logger {
curLog := log.New()
// 配置日志出处
curLog.SetReportCaller(true)
// 配置日志格式
curLog.SetFormatter(&log.TextFormatter{})
// 设置日志级别
curLog.SetLevel(logLevel)
// 配置日志输入文件
// curFileName := logPath + "/" + appName
culHook := NewLfsHook(logPath,appName , 10)
curLog.AddHook(culHook)
return curLog
}
main.go
package main
import (
log "github.com/sirupsen/logrus"
"test_logrus/logger"
"time"
)
var (
innerLog * log.Logger
)
func main() {
// 初始化全局日志
logger.InitCommonLog("./log/", "global_test", log.InfoLevel)
// 初始化局部日志
innerLog = logger.Setup("./log/", "inner_test", log.InfoLevel)
for true {
// 使用全局日志输出
logger.GLog.WithFields(log.Fields{
"user": "admin",
}).Info("test global log")
// 使用局部日志输出
innerLog.WithFields(log.Fields{
"user": "admin",
}).Info("test inner log")
time.Sleep(1 * time.Second)
}
}
结果:
备注: 可以配置日志目录,可以分时间,分目录存储
6.2 Sucess Demo 2
重写 io.write
可以配置 一小时一个日志文件, 也可以配置一天一个日志文件
可以配置 不同模块,可以输出不同的日志文件
项目目录
log.go
package logger
import (
"errors"
"fmt"
log "github.com/sirupsen/logrus"
"os"
"time"
)
var GLog * log.Logger
func InitCommonLog(logPath string, appName string, loglevel log.Level) {
GLog = Setup(logPath, appName, loglevel)
}
type logFileWriter struct {
file *os.File
logPath string
fileDate string //判断日期切换目录
appName string
}
func (p * logFileWriter) Open() (n int, err error){
err = os.MkdirAll(fmt.Sprintf("%s/%s", p.logPath, p.fileDate), os.ModePerm)
if err != nil {
return 0, err
}
filename := fmt.Sprintf("%s/%s/%s-%s.log", p.logPath, p.fileDate, p.appName, p.fileDate)
p.file, err = os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE|os.O_SYNC, 0600)
if err != nil {
return 0, err
}
return 0, nil
}
func (p *logFileWriter) Write(data []byte) (n int, err error) {
if p == nil {
return 0, errors.New("logFileWriter is nil")
}
if p.file == nil {
return 0, errors.New("file not opened")
}
//判断是否需要切换日期
fileDate := time.Now().Format("20060102")
if p.fileDate != fileDate {
p.file.Close()
p.fileDate = fileDate
_ ,err := p.Open()
if err != nil {
return 0, err
}
}
n, e := p.file.Write(data)
return n, e
}
//初始化日志
func Setup(logPath string, appName string, logLevel log.Level) * log.Logger {
curLog := log.New()
// 配置日志出处
curLog.SetReportCaller(true)
// 配置日志格式
curLog.SetFormatter(&log.TextFormatter{})
// 设置日志级别
curLog.SetLevel(logLevel)
// 配置日志输入文件
fileDate := time.Now().Format("20060102")
fileWriter := logFileWriter{nil, logPath, fileDate, appName}
_,err := fileWriter.Open()
if err != nil {
log.Error(err)
return curLog
}
curLog.SetOutput(&fileWriter)
return curLog
}
main.go
package main
import (
log "github.com/sirupsen/logrus"
"test_logrus/logger"
"time"
)
var (
innerLog * log.Logger
)
func main() {
// 初始化全局日志
logger.InitCommonLog("./log/", "global_test", log.InfoLevel)
// 初始化局部日志
innerLog = logger.Setup("./log/", "inner_test", log.InfoLevel)
for true {
// 使用全局日志输出
logger.GLog.WithFields(log.Fields{
"user": "admin",
}).Info("test global log")
// 使用局部日志输出
innerLog.WithFields(log.Fields{
"user": "admin",
}).Info("test inner log")
time.Sleep(1 * time.Second)
}
}
结果:
七 注意事项
7.1 Fatal处理
- 和很多日志框架一样,logrus的Fatal系列函数会执行os.Exit(1)。但是logrus提供可以注册一个或多个fatal handler函数的接口
- logrus.RegisterExitHandler(handler func() {} ),让logrus在执行os.Exit(1)之前进行相应的处理。fatal handler可以在系统异常时调用一些资源释放api等,让应用正确的关闭。
7.2 线程安全
默认情况下,logrus的api都是线程安全的,其内部通过互斥锁来保护并发写。互斥锁工作于调用hooks或者写日志的时候,如果不需要锁,可以调用logger.SetNoLock()来关闭之。
- 可以关闭logrus互斥锁的情形包括:没有设置hook,或者所有的hook都是线程安全的实现。
- 写日志到logger.Out已经是线程安全的了,如logger.Out已经被锁保护,或者写文件时,文件是以O_APPEND方式打开的,并且每次写操作都小于4k。