Logrus打印trace

看完 Go每周一库之Logrus 由感而发 ,是旭文博客的补充,强烈建议先看旭文的博客

Logrus打印trace

Logrus 前置知识点

旭文提到 Logrus 的两个重要结构体:Logger Entry,但是对于这两者并没有做详细的介绍,但是这些知识点对于本文下文有重要的意义。

Logger

Logger 是一个非常有趣的结构,我们先来看一下它的内部结构,没有写出所有的结构

type Logger struct {
	Out io.Writer // 封装流,所有的日志都会调用 Write 方法写.
	Hooks LevelHooks // 钩子. 在写日志的之前会调用
	Formatter Formatter // 结构化方法,是一个函数.所有的日志都会调用Format方法把Entry结构化成[]byte数组
	
    Level Level // 日志等级
	mu MutexWrap // 锁.非常重要.
	// Reusable empty entry
	entryPool sync.Pool // entry池。用来保证不会多次分配内存
}

可以看到 Logger 结构体没有依赖其余的组件,都是通过接口的方式暴露出功能,在我们实现插件的时候只需要关注自己需要实现的接口,而完全不需要去修改 logrus 的代码。

Level、Out、Formatter、Hooks这些功能旭文的博客已经写的很明白,此处就不多赘述。

Entry

Entry 对应每条日志的实体,他的内部结构如下:

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
	Context context.Context
	err string
}

Entry内部包括了Logger结构体指针,这是一个非常常见的编程手法,我们知道Entry都是通过Logger生成的,那么这些Entry就必须要知道是哪个Logger生成了ta,并且Entry会高度依赖Logger(Logger掌握了输出信息),但是需要注意: Entry没有使用 Logger 的 EnterPool.

Entry最为重要的字段就是Data了,它保存了日志除内容之外的结构化信息,Fields本身就是一个map,没有什么神奇的东西。

Entry也可以生成Entry,主要通过 Data 复制来实现。

Logrus实现trace

trace_id应该是一个微服务的刚需。它由Gateway产生,并在微服务各个节点之中流转,一个trace_id就可以标记出一条完整的链路。

想想有没有这种需求:我需要打印一次请求的完整日志、快速定位服务A请求服务B的日志。需要实现这个功能就需要我们把trace_id给加入到结构化日志中,这可以使用logrus完美实现。

通过刚刚的前置知识点可以知道

  1. 每条日志的信息都是由 Entry 保存的
  2. Entry 内部由 Data、Ctx字段
  3. Entry 知道 Logger 指针
  4. Logger 在每次输出Entry日志的时候会调用 Hooks

接下来我们就可以通过这些来实现我们的功能了。首先书写 Gin 中间件,获取请求头的TraceID信息写入到 gin.Request.Context 中。

// GinTraceIdMiddleware 生成trade_id
func GinTraceIdMiddleware(ginCtx *gin.Context) {
	reqCtx := ginCtx.Request.Context()
	header := ginCtx.GetHeader(HeaderTraceIdKey)
	if header != "" {
		reqCtx = ContextWithTraceId(reqCtx, header)
	} else {
		reqCtx = ContextWithRandomTraceId(reqCtx)
	}
	ginCtx.Request = ginCtx.Request.WithContext(reqCtx)
}

// ContextWithRandomTraceId 为Context生成随机的trace_id
func ContextWithRandomTraceId(ctx context.Context) (newCtx context.Context) {
	traceId := UUID()
	ContextWithTraceId(ctx, traceId)
	return
}

// ContextWithTraceId 为Context设置trace_id. 如果ID已存在则不设置
func ContextWithTraceId(ctx context.Context, traceId string) (newCtx context.Context) {
	oldTraceId, ok := ctx.Value(TraceIdKey).(string)
	if ok && oldTraceId != "" {
		return
	}
	
	newCtx = context.WithValue(ctx, TraceIdKey, traceId)
	return
}

这样我们可以直接书写 router.Use(GinTraceIdMiddleware) 使得每次的请求的ctx都携带 __trace_id ,这样我们只需要让这个ctx在程序各个地方流转即可。

接下来是第二步:让每次打印都携带流转的Ctx。通过上文可以知道每个 Entry 都会携带一个 Ctx 信息。这样Entry也可以设置成这个Ctx。然后在打印日志的时候把Ctx的TraceId信息加入到 Entry 的 Data 中即可。这一步我们可以使用Hook来实现。

// TraceIdHook 使用该结构体在日志中打印TraceID信息
type TraceIdHook struct {}

// Levels 所有等级, 可以书写成可配置的
func (m *TraceIdHook) Levels() []logrus.Level {
	return logrus.AllLevels
}

const (
	TraceIdKey       = "__trace_id"
	HeaderTraceIdKey = "__trace_id"
)

// Fire implements logrus.Hook.Fire
func (m *TraceIdHook) Fire(entry *logrus.Entry) (err error) {
	var tradeId string
	if entry.Context != nil {
		var ok bool
		tradeId, ok = entry.Context.Value(TraceIdKey).(string) // 判断 entry 中的ctx字段是否存在 traceId 信息
		if !ok {
			return
		}
	}

	if tradeId == "" {
		return
	}

	entry.Data["trace_id"] = tradeId // 把 trace_id 信息加入到携带信息中,这样就可以打印出来了

	return
}

使用方法

主函数

func main() {
	engine := gin.Default()
	logrus.AddHook(&log.TraceIdHook{})

	engine.Use(log.GinTraceIdMiddleware)

	engine.POST(`/`, func(c *gin.Context) {
		logrus.WithContext(c.Request.Context()).
			WithField(`func`, `index`).
			Info("hello")

		c.String(200, "OK!!!")
	})

	_ = engine.Run(":8080")
}

log包

package log

import (
	"context"
	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/sirupsen/logrus"
	"strings"
)

func InitLog() {
	logrus.AddHook(&TraceIdHook{})
}

type TraceIdHook struct {
}

func (m *TraceIdHook) Levels() []logrus.Level {
	return logrus.AllLevels
}

const (
	TraceIdKey       = "__trace_id"
	HeaderTraceIdKey = "__trace_id"
)

// Fire implements logrus.Hook.Fire
func (m *TraceIdHook) Fire(entry *logrus.Entry) (err error) {
	var tradeId string
	if entry.Context != nil {
		var ok bool
		tradeId, ok = entry.Context.Value(TraceIdKey).(string)
		if !ok {
			return
		}
	}

	if tradeId == "" {
		return
	}

	entry.Data["trace_id"] = tradeId

	return
}

// GinTraceIdMiddleware 生成trade_id
func GinTraceIdMiddleware(ginCtx *gin.Context) {
	reqCtx := ginCtx.Request.Context()
	header := ginCtx.GetHeader(HeaderTraceIdKey)
	if header != "" {
		reqCtx = ContextWithTraceId(reqCtx, header)
	} else {
		reqCtx = ContextWithRandomTraceId(reqCtx)
	}
	ginCtx.Request = ginCtx.Request.WithContext(reqCtx)
}

// ContextWithRandomTraceId 为Context生成随机的trace_id
func ContextWithRandomTraceId(ctx context.Context) (newCtx context.Context) {
	traceId := UUID()
	newCtx = ContextWithTraceId(ctx, traceId)
	return
}

// ContextWithTraceId 为Context设置trace_id. 如果ID已存在则不设置
func ContextWithTraceId(ctx context.Context, traceId string) (newCtx context.Context) {
	oldTraceId, ok := ctx.Value(TraceIdKey).(string)
	if ok && oldTraceId != "" {
		return
	}

	newCtx = context.WithValue(ctx, TraceIdKey, traceId)
	return
}

func UUID() string {
	value := uuid.New()

	return strings.ToLower(
		strings.Replace(value.String(), "-", "", -1),
	)
}

func BgContextWithCancel() (ctx context.Context, cancel context.CancelFunc) {
	ctx, cancel = context.WithCancel(context.Background())
	ctx = ContextWithRandomTraceId(ctx)
	return
}

func GetTraceId(ctx context.Context) (traceId string) {
	traceId, _ = ctx.Value(TraceIdKey).(string)
	return
}

这样当我们每打一次日志的时候都可以记录到trace_id

写在最后

也不知道想写点什么东西在最后,就这样吧

请用钱砸死我!!!