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。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值