看完 Go每周一库之Logrus 由感而发 ,是旭文博客的补充,强烈建议先看旭文的博客
Logrus打印trace Logrus
前置知识点旭文提到 Logrus
的两个重要结构体:Logger Entry
,但是对于这两者并没有做详细的介绍,但是这些知识点对于本文下文有重要的意义。
Logger
Logger
是一个非常有趣的结构,我们先来看一下它的内部结构,没有写出所有的结构
type Logger struct { Out io.Writer Hooks LevelHooks Formatter Formatter Level Level mu MutexWrap entryPool sync.Pool }
可以看到 Logger
结构体没有依赖其余的组件,都是通过接口的方式暴露出功能,在我们实现插件的时候只需要关注自己需要实现的接口,而完全不需要去修改 logrus
的代码。
Level、Out、Formatter、Hooks这些功能旭文的博客已经写的很明白,此处就不多赘述。
Entry
Entry
对应每条日志的实体,他的内部结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type Entry struct { Logger *Logger Data Fields Time time.Time Level Level Caller *runtime.Frame Message string 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实现tracetrace_id应该是一个微服务的刚需。它由Gateway产生,并在微服务各个节点之中流转,一个trace_id就可以标记出一条完整的链路。
想想有没有这种需求:我需要打印一次请求的完整日志、快速定位服务A请求服务B的日志。需要实现这个功能就需要我们把trace_id给加入到结构化日志中,这可以使用logrus完美实现。
通过刚刚的前置知识点可以知道
每条日志的信息都是由 Entry 保存的 Entry 内部由 Data、Ctx字段 Entry 知道 Logger 指针 Logger 在每次输出Entry日志的时候会调用 Hooks 接下来我们就可以通过这些来实现我们的功能了。首先书写 Gin
中间件,获取请求头的TraceID信息写入到 gin.Request.Context
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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) }func ContextWithRandomTraceId (ctx context.Context) (newCtx context.Context) { traceId := UUID() ContextWithTraceId(ctx, traceId) return }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来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 type TraceIdHook struct {}func (m *TraceIdHook) Levels () []logrus .Level { return logrus.AllLevels }const ( TraceIdKey = "__trace_id" HeaderTraceIdKey = "__trace_id" )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 }
使用方法主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 package logimport ( "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" )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 }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) }func ContextWithRandomTraceId (ctx context.Context) (newCtx context.Context) { traceId := UUID() newCtx = ContextWithTraceId(ctx, traceId) return }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
写在最后也不知道想写点什么东西在最后,就这样吧