Go语言日志库logrus

news2024/10/5 16:28:36

Go语言日志库logrus

1、介绍 logrus

logrus是目前Github上star数量最多的日志包,功能强大、性能高效、高度灵活,还提供了自定义插件的功能。很

多优秀的开源项目,例如:docker、prometheus等都使用了logrus。logrus除了具有日志的基本功能外,还具有

如下特性:

  • 支持常用的日志级别,logrus支持如下日志级别:Debug、Info、Warn、Error、Fatal和Panic。

  • 可扩展,logrus的hook机制允许使用者通过hook的方式将日志分发到任意地方,例如:本地文件、标准输

    出、elasticsearch、logstash、kafka等。

  • 支持自定义日志格式,logrus内置了2种格式:JSONFormatter和TextFormatter。除此之外,logrus允许使用

    者通过实现Formatter接口,来自定义日志格式。

  • 结构化日志记录,logrus的Field机制可以允许使用者自定义日志字段,而不是通过冗长的消息来记录日志。

  • 预设日志字段,logrus的Default Fields机制可以给一部分或者全部日志统一添加共同的日志字段,例如给某

    次HTTP请求的所有日志添加X-Request-ID字段。

  • Fatal handlers:logrus允许注册一个或多个handler,当发生fatal级别的日志时调用。当我们的程序需要优

    雅关闭时,该特性会非常有用。

  • 它是一个结构化、插件化的日志记录库。完全兼容 golang 标准库中的日志模块。它还内置了 2 种日志输出格

    式 JSONFormatter 和 TextFormatter,来定义输出的日志格式。

github地址:https://github.com/sirupsen/logrus

2、logrus 使用

2.1 安装

$ go get github.com/sirupsen/logrus

2.2 简单示例

最简单的使用logrus的示例如下:

package main

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

func main() {
	log.WithFields(log.Fields{
		"animal": "walrus",
	}).Info("A walrus appears")
}

运行输出:

# 输出
time="2023-06-02T11:00:26+08:00" level=info msg="A walrus appears" animal=walrus

2.3 设置日志格式

2.3.1 内置日志格式

logrus内置的formatter有 2 种,logrus.TextFormatter 和 logrus.JSONFormatter。

logrus.JSONFormatter{}, 设置为 json 格式,所有设置选项在 logrus.JSONFormatter。

package main

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

func main() {
	// 设置为json格式
	log.SetFormatter(&log.JSONFormatter{})
	log.WithFields(log.Fields{
		"animal": "walrus",
	}).Info("A walrus appears")

	// 设置json里的日期输出格式
	log.SetFormatter(&log.JSONFormatter{
		TimestampFormat: "2006-01-02 15:04:05",
	})
	log.WithFields(log.Fields{
		"animal": "walrus",
	}).Info("A walrus appears")
}
# 输出
{"animal":"walrus","level":"info","msg":"A walrus appears","time":"2023-06-02T11
:44:48+08:00"}
{"animal":"walrus","level":"info","msg":"A walrus appears","time":"2023-06-02 11
:44:48"}

logrus.TextFormatter{},设置为文本格式,所有的设置选项在 logrus.TextFormatter。

package main

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

func main() {
	log.SetFormatter(&log.TextFormatter{})
	log.WithFields(log.Fields{
		"animal": "walrus",
	}).Info("A walrus appears")

	log.SetFormatter(&log.TextFormatter{
		TimestampFormat: "2006-01-02 15:04:05",
	})
	log.WithFields(log.Fields{
		"animal": "walrus",
	}).Info("A walrus appears")
}
# 输出
time="2023-06-02T11:47:46+08:00" level=info msg="A walrus appears" animal=walrus
time="2023-06-02 11:47:46" level=info msg="A walrus appears" animal=walrus

2.3.2 自定义日志格式

可以根据 Formatter 接口自定义日志格式,里面有一个 Format 方法,这个 Format 方法里有一个struct类型数据

*Entry, Entry.Data 是所有字段集合,Fields 类型为 map[string]interface{}。

// The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones:
//
// * `entry.Data["msg"]`. The message passed from Info, Warn, Error ..
// * `entry.Data["time"]`. The timestamp.
// * `entry.Data["level"]. The level the entry was logged at.
//
// Any additional fields added with `WithField` or `WithFields` are also in
// `entry.Data`. Format is expected to return an array of bytes which are then
// logged to `logger.Out`.
type Formatter interface {
	Format(*Entry) ([]byte, error)
}
// An entry is the final or intermediate Logrus logging entry. It contains all
// the fields passed with WithField{,s}. It's finally logged when Trace, Debug,
// Info, Warn, Error, Fatal or Panic is called on it. These objects can be
// reused and passed around as much as you wish to avoid field duplication.
type Entry struct {
	Logger *Logger

	// Contains all the fields set by the user.
	Data Fields

	// Time at which the log entry was created
	Time time.Time

	// Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic
	// This field will be set on entry firing and the value will be equal to the one in Logger struct field.
	Level Level

	// Calling method, with package name
	Caller *runtime.Frame

	// Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic
	Message string

	// When formatter is called in entry.log(), a Buffer may be set to entry
	Buffer *bytes.Buffer

	// Contains the context set by the user. Useful for hook processing etc.
	Context context.Context

	// err may contain a field formatting error
	err string
}
// Fields type, used to pass to `WithFields`.
type Fields map[string]interface{}

通过实现接口logrus.Formatter可以实现自己的格式。

type Formatter interface {
  Format(*Entry) ([]byte, error)
}

例子:

package main

import (
	"fmt"
	jsoniter "github.com/json-iterator/go"
	log "github.com/sirupsen/logrus"
)

type MyJSONFormatter struct {
	JSONPrefix string
	Otherdata  string
}

func (my *MyJSONFormatter) Format(entry *log.Entry) ([]byte, error) {
	entry.Data["msg"] = fmt.Sprintf("%s%s%s", my.JSONPrefix, my.Otherdata, entry.Message)
	json, err := jsoniter.Marshal(&entry.Data)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal fields to JSON , %w", err)
	}
	return append(json, '\n'), nil
}

func main() {
	formatter := &MyJSONFormatter{
		JSONPrefix: "jsonprefix-",
		Otherdata:  "otherdata:",
	}
	log.SetFormatter(formatter)
	log.Info("this is customered formatter")
}
# 输出
{"msg":"jsonprefix-otherdata:this is customered formatter"}

2.3.3 第三方自定义formatter设置日志格式

  • FluentdFormatte:Formats entries that can be parsed by Kubernetes and Google Container Engine.

  • logstas:Logs fields as Logstash Events。

  • caption-json-formatte:logrus’s message json formatter with human-readable caption added。

  • powerful-logrus-formatte:get fileName, log’s line number and the latest function’s name when print

    log; Sava log to files。

  • nested-logrus-formatter:Human-readable log formatter, converts logrus fields to a nested

    structure。

我们这里介绍 nested-logrus-formatter。

安装:

$ go get github.com/antonfisher/nested-logrus-formatter
package main

import (
	nested "github.com/antonfisher/nested-logrus-formatter"
	"github.com/sirupsen/logrus"
)

func main() {
	logrus.SetFormatter(&nested.Formatter{
		HideKeys:    true,
		FieldsOrder: []string{"component", "category"},
	})
	logrus.Info("info msg")
}
# 输出
Jun  2 16:59:55.116 [INFO] info msg

nested格式提供了多个字段用来定制行为:

// Formatter - logrus formatter, implements logrus.Formatter
type Formatter struct {
	// FieldsOrder - default: fields sorted alphabetically
	FieldsOrder []string

	// TimestampFormat - default: time.StampMilli = "Jan _2 15:04:05.000"
	TimestampFormat string

	// HideKeys - show [fieldValue] instead of [fieldKey:fieldValue]
	HideKeys bool

	// NoColors - disable colors
	NoColors bool

	// NoFieldsColors - apply colors only to the level, default is level + fields
	NoFieldsColors bool

	// NoFieldsSpace - no space between fields
	NoFieldsSpace bool

	// ShowFullLevel - show a full level [WARNING] instead of [WARN]
	ShowFullLevel bool

	// NoUppercaseLevel - no upper case for level value
	NoUppercaseLevel bool

	// TrimMessages - trim whitespaces on messages
	TrimMessages bool

	// CallerFirst - print caller info first
	CallerFirst bool

	// CustomCallerFormatter - set custom formatter for caller info
	CustomCallerFormatter func(*runtime.Frame) string
}
  • 默认,logrus 输出日志中字段是 key=value 这样的形式。使用 nested 格式,我们可以通过设置 HideKeys 为

    true 隐藏键,只输出值;

  • 默认,logrus 是按键的字母序输出字段,可以设置 FieldsOrder 定义输出字段顺序;

  • 通过设置 TimestampFormat 设置日期格式。

package main

import (
	nested "github.com/antonfisher/nested-logrus-formatter"
	"github.com/sirupsen/logrus"
	"time"
)

func main() {
	logrus.SetFormatter(&nested.Formatter{
		// HideKeys:        true,
		TimestampFormat: time.RFC3339,
		FieldsOrder:     []string{"name", "age"},
	})
	logrus.WithFields(logrus.Fields{
		"name": "dj",
		"age":  18,
	}).Info("info msg")
}
# 输出
2023-06-02T17:36:03+08:00 [INFO] [name:dj] [age:18] info msg

注意到,我们将时间格式设置成time.RFC3339,即2006-01-02T15:04:05Z07:00这种形式。

2.4 设置日志级别

logrus 的使用非常简单,与标准库 log 类似,logrus 支持更多的日志级别。

logrus日志一共7级别,从高到低:panic,fatal,error,warn,info,debug,trace。

  • Panic:记录日志,然后panic

  • Fatal:致命错误,出现错误时程序无法正常运转。输出日志后,程序退出;

  • Error:错误日志,需要查看原因;

  • Warn:警告信息,提醒程序员注意;

  • Info:关键操作,核心流程的日志;

  • Debug:一般程序中输出的调试信息;

  • Trace:很细粒度的信息,一般用不到;

log.SetLevel(log.WarnLevel):设置输出警告级别。

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")
}
# 输出
# 另外,我们观察到输出中有三个关键信息,time、level和msg
# time:输出日志的时间
# level:日志级别
# msg:日志信息
time="2023-06-02T11:18:54+08:00" level=trace msg="trace msg"
time="2023-06-02T11:18:54+08:00" level=debug msg="debug msg"
time="2023-06-02T11:18:54+08:00" level=info msg="info msg"
time="2023-06-02T11:18:54+08:00" level=warning msg="warn msg"
time="2023-06-02T11:18:54+08:00" level=error msg="error msg"
time="2023-06-02T11:18:54+08:00" level=fatal msg="fatal msg"

logrus有一个日志级别,高于这个级别的日志不会输出。默认的级别为InfoLevel。所以为了能看到Trace

Debug日志,我们在main函数第一行设置日志级别为TraceLevel

由于logrus.Fatal会导致程序退出,下面的logrus.Panic不会执行到。

2.5 设置日志输出方式

log.SetOutput(os.Stdout):输出到 Stdout,默认输出到 Stderr。

输出到文件里:

logfile, _ := os.OpenFile(“./logrus.log”, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)

logrus.SetOutput(logfile)

package main

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

func init() {
	// 设置format json
	log.SetFormatter(&log.JSONFormatter{})
	// 设置输出警告级别
	log.SetLevel(log.InfoLevel)
	// 输出到stdout而不是默认的stderr
	log.SetOutput(os.Stdout)
}

func main() {
	log.WithFields(log.Fields{
		"animal": "dog",
		"size":   10,
	}).Info("a group of dog emerges from the zoon")

	log.WithFields(log.Fields{
		"omg":    true,
		"number": 12,
	}).Warn("the group's number increased")

	// 从WithFields()返回的logrus.Entry
	contextLogger := log.WithFields(log.Fields{
		"common": "this is a common filed",
		"other":  "i also should be logged always",
	})
	// 共同字段输出
	contextLogger.Info("I'll be logged with common and other field")
	contextLogger.Info("Me too")
}

运行输出:

# 输出
{"animal":"dog","level":"info","msg":"a group of dog emerges from the zoon","siz
e":10,"time":"2023-06-02T14:11:27+08:00"}
{"level":"warning","msg":"the group's number increased","number":12,"omg":true,"
time":"2023-06-02T14:11:27+08:00"}
{"common":"this is a common filed","level":"info","msg":"I'll be logged with com
mon and other field","other":"i also should be logged always","time":"2023-06-02
T14:11:27+08:00"}
{"common":"this is a common filed","level":"info","msg":"Me too","other":"i also
 should be logged always","time":"2023-06-02T14:11:27+08:00"}
// 输出到文件
package main

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

func init() {
	// 设置format json
	log.SetFormatter(&log.JSONFormatter{})
	// 设置输出警告级别
	log.SetLevel(log.InfoLevel)
	logfile, _ := os.OpenFile("./logrus.log", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
	log.SetOutput(logfile)
}

func main() {
	log.WithFields(log.Fields{
		"animal": "dog",
		"size":   10,
	}).Info("a group of dog emerges from the zoon")

	log.WithFields(log.Fields{
		"omg":    true,
		"number": 12,
	}).Warn("the group's number increased")

	// 从WithFields()返回的logrus.Entry
	contextLogger := log.WithFields(log.Fields{
		"common": "this is a common filed",
		"other":  "i also should be logged always",
	})
	// 共同字段输出
	contextLogger.Info("I'll be logged with common and other field")
	contextLogger.Info("Me too")
}
# logrus.log文件的内容
{"animal":"dog","level":"info","msg":"a group of dog emerges from the zoon","size":10,"time":"2023-06-02T14:20:22+08:00"}
{"level":"warning","msg":"the group's number increased","number":12,"omg":true,"time":"2023-06-02T14:20:22+08:00"}
{"common":"this is a common filed","level":"info","msg":"I'll be logged with common and other field","other":"i also should be logged always","time":"2023-06-02T14:20:22+08:00"}
{"common":"this is a common filed","level":"info","msg":"Me too","other":"i also should be logged always","time":"2023-06-02T14:20:22+08:00"}

一次可以输出到多种介质中:

package main

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

func main() {
	writer1 := &bytes.Buffer{}
	writer2 := os.Stdout
	writer3, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_CREATE, 0755)
	if err != nil {
		log.Fatalf("create file log.txt failed: %v", err)
	}
	logrus.SetOutput(io.MultiWriter(writer1, writer2, writer3))
	logrus.Info("info msg")
}
# 输出
time="2023-06-02T16:46:18+08:00" level=info msg="info msg"

2.6 logrus 的 Fatal 处理

定义了输出 Fatal 日志后,其后的日志都不能输出了,这是为什么?日志后面有个信息 exit status

func (logger *Logger) Fatal(args ...interface{}) {
	logger.Log(FatalLevel, args...)
	logger.Exit(1)
}

因为 logrus 的 Fatal 输出后,会执行 os.Exit(1)。那如果程序后面还有一些必要的程序要处理怎么办?

logrus 提供了 RegisterExitHandler 方法,在 fatal 异常时处理一些问题。

package main

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

func main() {
	log.SetFormatter(&log.TextFormatter{
		TimestampFormat: "2006-01-02 15:04:05",
	})
	log.RegisterExitHandler(func() {
		fmt.Println("发生了fatal异常,执行一些必要的处理工作")
	})
	log.Warn("warn")
	log.Fatal("fatal")
	// 不会执行
	log.Info("info")
}

运行输出:

# 输出
发生了fatal异常,执行一些必要的处理工作
time="2023-06-02 14:23:19" level=warning msg=warn
time="2023-06-02 14:23:19" level=fatal msg=fatal

和很多日志框架一样,logrus的Fatal系列函数会执行os.Exit(1)。但是logrus提供可以注册一个或多个fatal

handler函数的接口logrus.RegisterExitHandler(handler func() {} ),让logrus在执行os.Exit(1)之前进行相应的处

理。fatal handler可以在系统异常时调用一些资源释放api等,让应用正确的关闭。

2.7 切分日志文件

如果日志文件太大了,想切分成小文件,但是 logrus 没有提供这个功能。

一种是借助linux系统的 logrotate 命令来切分 logrus 生成的日志文件。

另外一种是用 logrus 的 hook 功能,做一个切分日志的插件。找到了 file-rotatelogs,但是这个库状态

已经是 archived 状态,库作者现在不接受任何修改,他也不继续维护了。所以使用还是慎重些。

在 logrus issue 里找到了这个 https://github.com/natefinch/lumberjack 切割文件的库。

package main

import (
	log "github.com/sirupsen/logrus"
	"gopkg.in/natefinch/lumberjack.v2"
)

func main() {
	logger := &lumberjack.Logger{
		Filename:   "./testlogrus.log",
		MaxSize:    500,  // 日志文件大小,单位是 MB
		MaxBackups: 3,    // 最大过期日志保留个数
		MaxAge:     28,   // 保留过期文件最大时间,单位 天
		Compress:   true, // 是否压缩日志,默认是不压缩,这里设置为true,压缩日志
	}
	log.SetOutput(logger) // logrus设置日志的输出方式
	log.SetLevel(log.TraceLevel)
	log.Trace("trace msg")
	log.Debug("debug msg")
	log.Info("info msg")
	log.Warn("warn msg")
	log.Error("error msg")
	log.Fatal("fatal msg")
	log.Panic("panic msg")
}
# 日志文件的输出
time="2023-06-02T14:32:00+08:00" level=trace msg="trace msg"
time="2023-06-02T14:32:00+08:00" level=debug msg="debug msg"
time="2023-06-02T14:32:00+08:00" level=info msg="info msg"
time="2023-06-02T14:32:00+08:00" level=warning msg="warn msg"
time="2023-06-02T14:32:00+08:00" level=error msg="error msg"
time="2023-06-02T14:32:00+08:00" level=fatal msg="fatal msg"

将不同等级的输出保存到不同的文件中:

package main

import (
	"github.com/lestrrat-go/file-rotatelogs"
	"github.com/rifflock/lfshook"
	log "github.com/sirupsen/logrus"
	"time"
)

var infoLogName = "info"
var errorLogName = "error"

func newLfsHook(maxRemainCnt uint) log.Hook {
	infoWriter, infoErr := rotatelogs.New(
		infoLogName+".%Y%m%d%H",
		// WithLinkName为最新的日志建立软连接,以方便随着找到当前日志文件
		// rotatelogs.WithLinkName(infoLogName),
		// WithRotationTime设置日志分割的时间,这里设置为一小时分割一次
		rotatelogs.WithRotationTime(time.Hour),
		// WithMaxAge和WithRotationCount二者只能设置一个
		// WithMaxAge设置文件清理前的最长保存时间
		// WithRotationCount设置文件清理前最多保存的个数
		// rotatelogs.WithMaxAge(time.Hour*24),
		rotatelogs.WithRotationCount(maxRemainCnt),
	)
	if infoErr != nil {
		log.Errorf("config local file system for info logger error: %v", infoErr)
	}
	errorWriter, errorErr := rotatelogs.New(
		errorLogName+".%Y%m%d%H",
		// WithLinkName为最新的日志建立软连接,以方便随着找到当前日志文件
		// rotatelogs.WithLinkName(errorLogName),
		// WithRotationTime设置日志分割的时间,这里设置为一小时分割一次
		rotatelogs.WithRotationTime(time.Hour),
		// WithMaxAge和WithRotationCount二者只能设置一个
		// WithMaxAge设置文件清理前的最长保存时间
		// WithRotationCount设置文件清理前最多保存的个数
		// rotatelogs.WithMaxAge(time.Hour*24),
		rotatelogs.WithRotationCount(maxRemainCnt),
	)
	if errorErr != nil {
		log.Errorf("config local file system for error logger error: %v", errorErr)
	}
	lfsHook := lfshook.NewHook(lfshook.WriterMap{
		log.InfoLevel:  infoWriter,
		log.ErrorLevel:  errorWriter,
	}, &log.TextFormatter{DisableColors: true})
	return lfsHook
}

func main(){
	log.SetLevel(log.InfoLevel)
	log.AddHook(newLfsHook(10))
	log.Info("info!")
	log.Error("error!")
}
# 输出
time="2023-06-02T16:07:54+08:00" level=info msg="info!"
time="2023-06-02T16:07:54+08:00" level=error msg="error!"

生成了 info.2023060216error.2023060216

# info.2023060216
time="2023-06-02T16:07:54+08:00" level=info msg="info!"
# error.2023060216
time="2023-06-02T16:07:54+08:00" level=error msg="error!"

2.8 设置logrus实例

如果一个应用有多个地方使用日志,可以单独实例化一个 logrus,作为全局的日志实例。

logger是一种相对高级的用法, 对于一个大型项目, 往往需要一个全局的logrus实例,即 logger 对象来记录项目所

有的日志。

package main

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

var ll = logrus.New()

func main() {
	// 设置输出日志位置,可以设置日志到file里
	ll.Out = os.Stdout
	// 可以设置输出到文件
	// logfile, _ := os.OpenFile("./logrus.log", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
	// log.Out = logfile
	ll.Formatter = &logrus.JSONFormatter{}
	ll.WithFields(logrus.Fields{
		"fruit": "apple",
		"size":  20,
	}).Info(" a lot of apples on the tree")
}

输出:

{"fruit":"apple","level":"info","msg":" a lot of apples on the tree","size":20,"
time":"2023-06-02T14:37:53+08:00"}

2.9 fields

在使用 logrus 时,鼓励用 log.WithFields(log.Fields{}).Fatal() 这种方式替代:

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

也就是不用 %s,%d 这种方式格式化,而是直接传入变量 event,topic 给 log.Fields ,这样就显得结构化日志输

出,很人性化美观。

logrus不推荐使用冗长的消息来记录运行信息,它推荐使用 Fields 来进行精细化的、结构化的信息记录。

package main

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

func main() {
	log.WithFields(log.Fields{
		"event": "event",
		"topic": "topic",
		"key":   "key",
	}).Info("Failed to send event")
}
# 输出
time="2023-06-02T14:52:59+08:00" level=info msg="Failed to send event" event=event key=key topic=topic

2.10 设置默认字段

比如在链路追踪里,会有一个 rquest_id ,trace_id 等,想这个 log 一直带有这 2 个字段,logrus 怎么设置?

可以用 log.WithFields(log.Fields{“request_id”: request_id, “trace_id”: trace_id})

requestLogger := log.WithFields(log.Fields{"request_id": request_id, "trace_id": trace_id})
requestLogger.Info("something happened on that request")
requestLogger.Warn("something not great happened")

例子:

package main

import (
	"github.com/google/uuid"
	log "github.com/sirupsen/logrus"
)

func main() {
	uid := uuid.New()
	request_id := uid
	trace_id := uid
	requestLogger := log.WithFields(log.Fields{"request_id": request_id, "trace_id": trace_id})
	requestLogger.Info("something happened on that request")
	requestLogger.Warn("something not great happened")
}
# 输出
time="2023-06-02T14:54:56+08:00" level=info msg="something happened on that requ
est" request_id=e16b8b4d-67e9-4600-875b-8fba6e889756 trace_id=e16b8b4d-67e9-4600
-875b-8fba6e889756
time="2023-06-02T14:54:56+08:00" level=warning msg="something not great happened
" request_id=e16b8b4d-67e9-4600-875b-8fba6e889756 trace_id=e16b8b4d-67e9-4600-87
5b-8fba6e889756

2.11 hook钩子-扩展logrus功能

hook 给 logrus 提供了强大的可扩展功能。

logrus最令人心动的功能就是其可扩展的HOOK机制了,通过在初始化时为logrus添加hook,logrus可以实现各种

扩展功能。

用户可以给 logrus 编写钩子插件,根据自己的日志需求编写 hook。

logrus 也有一些内置插件 hooks。

grus官方仅仅内置了syslog的 hook:

https://github.com/sirupsen/logrus/tree/master/hooks/syslog

此外,但Github也有很多第三方的hook可供使用。我们可以使用一些 Hook 将日志发送到 redis/mongodb 等存

储中:

  • mgorus:将日志发送到 mongodb:

    https://github.com/weekface/mgorus

  • logrus-redis-hook:将日志发送到 redis:

    https://github.com/rogierlommers/logrus-redis-hook

  • logrus-amqp:将日志发送到 ActiveMQ:

    https://github.com/vladoatanasov/logrus_amqp

  • logrus_influxdb:发送到 InfluxDB:

    https://github.com/abramovic/logrus_influxdb

  • logrus-logstash-hook:发送到 Logstash:

    https://github.com/bshuster-repo/logrus-logstash-hook

  • elastic:发送到 elastcisearch:

    https://github.com/olivere/elastic

    https://pkg.go.dev/gopkg.in/olivere/elastic.v5

下面我们自定义 hook。

Hook接口:

logrus的hook接口定义如下,其原理是每此写入日志时拦截,修改logrus.Entry。

Levels()方法返回感兴趣的日志级别,输出其他日志时不会触发钩子。Fire是日志输出前调用的钩子方法。

// logrus在记录Levels()返回的日志级别的消息时会触发HOOK,
// 按照Fire方法定义的内容修改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
}

hook的使用也很简单,在初始化前调用log.AddHook(hook)添加相应的hook即可。

完整案例:

package main

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

type DefaultFieldHook struct {
}

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

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

func main() {
	logrus.SetLevel(logrus.TraceLevel)
	logrus.AddHook(&DefaultFieldHook{})
	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")
}
# 输出
time="2023-06-02T15:27:36+08:00" level=trace msg="trace msg" appName=MyAppName
time="2023-06-02T15:27:36+08:00" level=debug msg="debug msg" appName=MyAppName
time="2023-06-02T15:27:36+08:00" level=info msg="info msg" appName=MyAppName
time="2023-06-02T15:27:36+08:00" level=warning msg="warn msg" appName=MyAppName
time="2023-06-02T15:27:36+08:00" level=error msg="error msg" appName=MyAppName
time="2023-06-02T15:27:36+08:00" level=fatal msg="fatal msg" appName=MyAppName

2.11.1 发送至redis

安装:

$ go get github.com/rogierlommers/logrus-redis-hook

例子:

package main

import (
	logredis "github.com/rogierlommers/logrus-redis-hook"
	"github.com/sirupsen/logrus"
	"io/ioutil"
)

func init() {
	hookConfig := logredis.HookConfig{
		Host:     "localhost",
		Key:      "mykey",
		Format:   "v0",
		App:      "aweosome",
		Hostname: "localhost",
		TTL:      3600,
		Port:     6379,
	}
	hook, err := logredis.NewHook(hookConfig)
	if err == nil {
		logrus.AddHook(hook)
	} else {
		logrus.Errorf("logredis error: %q", err)
	}
}

func main() {
	logrus.Info("just some info logging...")
	logrus.WithFields(logrus.Fields{
		"animal": "walrus",
		"foo":    "bar",
		"this":   "that",
	}).Info("additional fields are being logged as well")
	logrus.SetOutput(ioutil.Discard)
	logrus.Info("This will only be sent to Redis")
}

为了程序能正常工作,我们还需要安装redis

直接输入redis-server,启动服务器。

运行程序后,我们使用redis-cli查看。

我们看到mykey是一个list,每过来一条日志,就在list后新增一项。

# 输出结果
time="2023-06-05T21:14:08+08:00" level=info msg="just some info logging..."
time="2023-06-05T21:14:08+08:00" level=info msg="additional fields are being log
ged as well" animal=walrus foo=bar this=that

redis的结果:

在这里插入图片描述

2.11.2 发送至elasticsearch

package main

import (
	"github.com/olivere/elastic/v7"
	"github.com/sirupsen/logrus"
	"gopkg.in/sohlich/elogrus.v7"
)

func main() {
	log := logrus.New()
	client, err := elastic.NewClient(elastic.SetURL("http://localhost:9200"))
	if err != nil {
		log.Panic(err)
	}
	hook, err := elogrus.NewAsyncElasticHook(client, "localhost", logrus.DebugLevel, "mylog")
	if err != nil {
		log.Panic(err)
	}
	log.Hooks.Add(hook)

	log.WithFields(logrus.Fields{
		"name": "joe",
		"age":  42,
	}).Error("Hello world!")
}
# 输出
time="2023-06-05T21:23:41+08:00" level=error msg="Hello world!" age=42 name=joe

查看elasticsearch:

$ curl http://localhost:9200/mylog/_search
{
	"took": 50,
	"timed_out": false,
	"_shards": {
		"total": 1,
		"successful": 1,
		"skipped": 0,
		"failed": 0
	},
	"hits": {
		"total": {
			"value": 1,
			"relation": "eq"
		},
		"max_score": 1.0,
		"hits": [{
			"_index": "mylog",
			"_type": "log",
			"_id": "rNO6i4gB1Ed8BTkaSoga",
			"_score": 1.0,
			"_source": {
				"Host": "localhost",
				"@timestamp": "2023-06-05T13:23:41.7171093Z",
				"Message": "Hello world!",
				"Data": {
					"age": 42,
					"name": "joe"
				},
				"Level": "ERROR"
			}
		}]
	}
}

2.12 记录文件名和行号

调用logrus.SetReportCaller(true)设置在输出日志中添加文件名和方法信息。

package main

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

func main() {
	logrus.SetReportCaller(true)
	logrus.Info("info msg")
}
# 输出
time="2023-06-02T15:30:02+08:00" level=info msg="info msg" func=main.main file="
C:/Users/admin/Desktop/tt/go-logrus/015.go:9"
package main

import (
	"fmt"
	log "github.com/sirupsen/logrus"
	"runtime"
	"strings"
)

// line number hook for log the call context,
type lineHook struct {
	Field  string
	// skip为遍历调用栈开始的索引位置
	Skip   int
	levels []log.Level
}

// Levels implement levels
func (hook lineHook) Levels() []log.Level {
	return log.AllLevels
}

// Fire implement fire
func (hook lineHook) Fire(entry *log.Entry) error {
	entry.Data[hook.Field] = findCaller(hook.Skip)
	return nil
}

func findCaller(skip int) string {
	file := ""
	line := 0
	var pc uintptr
	// 遍历调用栈的最大索引为第11层.
	for i := 0; i < 11; i++ {
		file, line, pc = getCaller(skip + i)
		// 过滤掉所有logrus包,即可得到生成代码信息
		if !strings.HasPrefix(file, "logrus") {
			break
		}
	}

	fullFnName := runtime.FuncForPC(pc)

	fnName := ""
	if fullFnName != nil {
		fnNameStr := fullFnName.Name()
		// 取得函数名
		parts := strings.Split(fnNameStr, ".")
		fnName = parts[len(parts)-1]
	}

	return fmt.Sprintf("%s:%d:%s()", file, line, fnName)
}

func getCaller(skip int) (string, int, uintptr) {
	pc, file, line, ok := runtime.Caller(skip)
	if !ok {
		return "", 0, pc
	}
	n := 0

	// 获取包名
	for i := len(file) - 1; i > 0; i-- {
		if file[i] == '/' {
			n++
			if n >= 2 {
				file = file[i+1:]
				break
			}
		}
	}
	return file, line, pc
}

func main() {
	log.SetLevel(log.TraceLevel)
	log.AddHook(&lineHook{})
	log.Trace("trace msg")
	log.Debug("debug msg")
	log.Info("info msg")
	log.Warn("warn msg")
	log.Error("error msg")
	log.Fatal("fatal msg")
	log.Panic("panic msg")
}
# 输出
time="2023-06-02T15:43:42+08:00" level=trace msg="trace msg" ="go-logrus/016.go:
56:getCaller()"
time="2023-06-02T15:43:42+08:00" level=debug msg="debug msg" ="go-logrus/016.go:
56:getCaller()"
time="2023-06-02T15:43:42+08:00" level=info msg="info msg" ="go-logrus/016.go:56
:getCaller()"
time="2023-06-02T15:43:42+08:00" level=warning msg="warn msg" ="go-logrus/016.go
:56:getCaller()"
time="2023-06-02T15:43:42+08:00" level=error msg="error msg" ="go-logrus/016.go:
56:getCaller()"
time="2023-06-02T15:43:42+08:00" level=fatal msg="fatal msg" ="go-logrus/016.go:
56:getCaller()"

2.13 线程安全

默认情况下,logrus的api都是线程安全的,其内部通过互斥锁来保护并发写。互斥锁工作于调用hooks或者写日

志的时候,如果不需要锁,可以调用logger.SetNoLock()来关闭之。可以关闭logrus互斥锁的情形包括:

  • 没有设置hook,或者所有的hook都是线程安全的实现。

  • 写日志到logger.Out已经是线程安全的了,如logger.Out已经被锁保护,或者写文件时,文件是以O_APPEND

    方式打开的,并且每次写操作都小于4k。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/613048.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VMware nat模式配置

使用nat模式&#xff0c;需要配置ip才能做到虚拟机与主机、外网正常通信 步骤 1 选择虚拟机设置&#xff0c;将网络连接改为nat模式 2 查看主机vmware network adpter vmnet8 打开控制面板。选择网络连接&#xff0c;右击vmnet8&#xff0c;打开属性 选择ip4&#xff0c;双击…

kubesphere插件,应用商店,应用仓库

应用商店 参考 步骤 以platform-admin角色的账号(admin)登录kubesphere点击右上角 “平台管理”点击“集群管理”点击 “自定义资源 CRD”搜索 clusterconfiguration点击 ClusterConfiguration点击 ks-installer 右侧的三个点&#xff0c;点击“编辑文件”在YAML 文件中&…

Linux进程间通信【匿名管道】

✨个人主页&#xff1a; 北 海 &#x1f389;所属专栏&#xff1a; Linux学习之旅 &#x1f383;操作环境&#xff1a; CentOS 7.6 阿里云远程服务器 文章目录 &#x1f307;前言&#x1f3d9;️正文1、进程间通信相关概念1.1、目的1.2、发展1.3、分类 2、什么是管道&#xff1…

Redi事务,数据持久化

4.其他数据功能 4.1pubsub发布订阅 Redis 发布订阅(pub/sub)是一种消息通信模式&#xff1a;发送者(pub)发送消息&#xff0c;订阅者(sub)接收消息。 Redis 客户端可以订阅任意数量的频道。 下面示例展示了频道 channel1 &#xff0c; 以及订阅这个频道的三个客户端 —— c…

旧改周报--深圳7大项目获进展:中海、星河等主导

2023年第22周&#xff0c;深圳市共发布拆除重建类权利人核实公示1项&#xff0c;规划公告1项&#xff0c;规划修改&#xff08;草案&#xff09;公示2项&#xff0c;规划&#xff08;修改&#xff09;公告1项&#xff0c;实施主体公示1项&#xff0c;建设用地批复1项&#xff0…

win7安装visual studio 2015出现安装包丢失或损坏

winr 输入 certmgr.msc 查看有没有选中的两个证书&#xff0c;如果没有需要从其他电脑导入&#xff0c;然后直接点击安装界面重试&#xff0c;即可继续安装

【SpringMVC】RESTful案例

1、Rest风格 对于Rest风格&#xff0c;我们需要学习的内容包括: REST简介REST入门案例REST快速开发案例:基于RESTful页面数据交互 1. REST简介 REST&#xff08;Representational State Transfer&#xff09;&#xff0c;表现形式状态转换,它是一种软件架构风格 当我们想表示…

优秀的测试开发应该具备的六大能力有哪些?

目录 前言 什么是测试开发工程师&#xff1f; 测试开发的六大能力 前言 从我工作中接触到的测试开发&#xff0c;以及面试测试开发候选人时问的问题&#xff0c;我将自己对测试开发这个岗位的理解&#xff0c;总结了如下六点能力。 我个人认为&#xff0c;具备如下六点能力…

数据结构之二叉树,实现二叉树的创建与遍历,以及二叉树的一些练习题

目录 目录 一、二叉树的创建与遍历 1.创建二叉树 构建出来的树如图&#xff1a; 2.二叉树的销毁 3.二叉树的前序遍历[Leetcode144.力扣] 4.二叉树的中序遍历 5.二叉树的后序遍历 二、二叉树的实现 1.获取树中节点的个数 2.获取叶子节点的个数 3.获取第K层节点的个数…

Redis实现分布式锁的原理:常见问题解析及解决方案、源码解析Redisson的使用

0、引言&#xff1a;分布式锁的引出 锁常常用于多线程并发的场景下保证数据的一致性&#xff0c;例如防止超卖、一人一单等场景需求 。通过加锁可以解决在单机情况下安全问题&#xff0c;但是在集群模式下就不行了。集群模式&#xff0c;即部署了多个服务器、并配置了负载均衡后…

记录使用Echarts-gl实现3D地图

一、前言 最近项目需要做个大屏展示的&#xff0c;开始做了第一版用户觉得地图太过于单调了&#xff0c;给我发了一个视频&#xff0c;让我参考着做。我看着视频上的地图旋转了方向、地图有标记、看着像是3D的&#xff08;视频上的地图使用多个图层叠加起来、CSS样式做了旋转&…

Nginx网络服务——location规则与rewrite重写

Nginx网络服务——location规则与rewrite重写 一、Nginx中location与rewrite1.location与rewrite常用的正则表达式2. location与rewrite的联系和区别 二、location的匹配规则1.location 的匹配分类2.location 常用的匹配规则3.location 优先级4.location匹配规则优先通用的总结…

【知识图谱搭建到应用】--知识存储--04

文章目录 Mysqljenafuseki数据存储数据建模数据映射注意事项 py2neoneo4jPy2neo与Neo4j的版本问题Py2neo导入三元组数据批量导入csv文件 rdflib库 前面几篇在讲述骗理论的内容&#xff0c;本片主要描述如何将清洗过的结构化数据存储在转换成三元组并存储起来&#xff0c;并于后…

ChatGPT与软件架构(4) - 架构师提示工程指南

架构师可以通过各种类型的对话提示&#xff0c;提升驱动ChatGPT对话输出的质量&#xff0c;更好的利用AI能力辅助架构设计。原文: Software Architects’ Guide to Enhancing ChatGPT Interactions With Prompt Types Robert Stump Unsplash 前言 随着ChatGPT等人工智能语言模型…

12.数据结构之AVL树

前言 提到平衡二叉查找树&#xff0c;不得不提二叉查找树。二叉查找树&#xff0c;说简单点&#xff0c;其实就是将我们的数据节点&#xff0c;有序的维护为一个树形结构。这样我们查的时候&#xff0c;那么我们查找某个节点在不在集合中的时间复杂度实际上就是树的高度。如果…

华为OD机试真题 Java 实现【玩牌高手】【2023 B卷 100分】,附详细解题思路

一、题目描述 给定一个长度为n的整型数组&#xff0c;表示一个选手在n轮内可选择的牌面分数。选手基于规则选牌&#xff0c; 请计算所有轮结束后其可以获得的最高总分数。 选择规则如下&#xff1a; 在每轮里选手可以选择获取该轮牌面&#xff0c;则其总分数加上该轮牌面分…

python笔记 第二章 变量

系列文章目录 第一章 初识python 文章目录 2.1变量2.1.1变量的作用2.1.2定义变量标识符命名习惯使用变量 2.2 认识bugDebug工具Debug工具使用步骤: 2.3 数据类型 2.1变量 目标 变量的作用定义变量认识数据类型 2.1.1变量的作用 变量就是一个存储数据的的时候当前数据所在的…

Java基础——堆和栈、static关键字、静态变量和成员变量的区别

Java程序运行顺序&#xff1a;Java应用程序—虚拟机—操作系统—硬件 Java中栈内存用来存储局部变量和方法调用&#xff0c;堆内存用来存储Java中的对象&#xff0c;成员变量、局部变量、类变量指向的对象都存储在堆内存中。 static关键字&#xff1a; 随着类的加载而加载优先…

ISATAP隧道配置与验证

ISATAP隧道配置与验证 【实验目的】 熟悉IPv6ISATAP隧道的概念。 掌握IPv6和IPv4共存的实现方法。 掌握IPv6 ISATAP地址编址规则。 掌握IPv6 ISATAP隧道的配置。 验证配置。 【实验拓扑】 设备参数如下表所示。 设备 接口 IP地址 子网掩码 默认网关 R1 S0/0 192.…

【内存管理大猫腻:从“越界”到“内存泄漏”应有尽有】

本章重点 什么是动态内存 为什么要有动态内存 什么是野指针 对应到C空间布局&#xff0c; malloc 在哪里申请空间 常见的内存错误和对策 C中动态内存“管理”体现在哪 什么是动态内存 动态内存是指在程序运行时&#xff0c;根据需要动态分配的内存空间。 #include <stdio.h&…