logrus如何为业务系统增加日志追踪串联traceId

并发系统中每秒钟往往有很多请求,在系统记录日志时候一般我们会给每条请求分配一个请求id,当系统有多个微服务时候往往希望一个关联id能把整个系统串联起来,这样当出现问题时候可以方便的排查问题;

现在以目前所开发的go项目为例,简要介绍如何利用logrus中间件将业务日志加上traceId,让每条请求的所有处理日志都带上这个id。

目前的项目主要以web请求处理为主,每个请求过来的时候都会携带一个请求id,如果没有的话,我们可以直接用uuid生成,然后作为后续的traceid,具体生成的位置,可以在HandleFunc地方使用中间件嵌入,或者具体的处理函数中添加;

先介绍使用中间件解析的形式,让下面的每一个HandleFunc 都先经过中间件,中间件中解析请求,在header中嵌入traceId,这样client和server端都具有了相同的id;

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
http.HandleFunc("/xxxx", middle.Middleware(handler.Handle))

func Middleware(next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        traceID := r.FormValue("reqid")
        if traceID == "" {
            uid := uuid.NewV4()
            r.Form.Set("reqid", uid.String())
            traceID = uid.String()
        }
        logger.AddHook(hook.NewTraceIdHook(traceID))

        w.Header().Set("X-Trace-Id", traceID)

        next.ServeHTTP(w, r)
    })

}

中间件这里对logrus初始化时候,增加了一个hook ,这样后续这条请求的日志都会记录上traceId

logger.AddHook(hook.NewTraceIdHook(traceID))

具体的hook.go 如下

package hook

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

type TraceIdHook struct {
    TraceId string
}

func NewTraceIdHook(traceId string) logrus.Hook {
    hook := TraceIdHook{
        TraceId: traceId,
    }
    return &hook
}

func (hook *TraceIdHook) Fire(entry *logrus.Entry) error {
    entry.Data["traceId"] = hook.TraceId
    return nil
}

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

新增logger模块,对logrus包中方法进行扩展

package logger

import (
    "context"
    "io"

    "github.com/sirupsen/logrus"
)

// Define key
const (
    TraceIDKey = "trace_id"
    UserIDKey  = "user_id"
    TagKey     = "tag"
    VersionKey = "version"
    StackKey   = "stack"
)

var (
    version string
)

// Logger Logrus
type Logger = logrus.Logger

// Entry Logrus entry
type Entry = logrus.Entry

// Hook 定义日志钩子别名
type Hook = logrus.Hook

// StandardLogger 获取标准日志
func StandardLogger() *Logger {
    return logrus.StandardLogger()
}

// SetLevel 设定日志级别
func SetLevel(level int) {
    logrus.SetLevel(logrus.Level(level))
}

// SetFormatter 设定日志输出格式
func SetFormatter(format string) {
    switch format {
    case "json":
        logrus.SetFormatter(new(logrus.JSONFormatter))
    default:
        logrus.SetFormatter(new(logrus.TextFormatter))
    }
}

// SetOutput 设定日志输出
func SetOutput(out io.Writer) {
    logrus.SetOutput(out)
}

// SetVersion 设定版本
func SetVersion(v string) {
    version = v
}

// AddHook 增加日志钩子
func AddHook(hook Hook) {
    logrus.AddHook(hook)
}

type (
    traceIDKey struct{}
    userIDKey  struct{}
    tagKey     struct{}
    stackKey   struct{}
)

// NewTraceIDContext 创建跟踪ID上下文
func NewTraceIDContext(ctx context.Context, traceID string) context.Context {
    return context.WithValue(ctx, traceIDKey{}, traceID)
}

// FromTraceIDContext 从上下文中获取跟踪ID
func FromTraceIDContext(ctx context.Context) string {
    v := ctx.Value(traceIDKey{})
    //todo remove
    if v != nil {
        if s, ok := v.(string); ok {
            return s
        }
    }
    return ""
}

// NewUserIDContext 创建用户ID上下文
func NewUserIDContext(ctx context.Context, userID string) context.Context {
    return context.WithValue(ctx, userIDKey{}, userID)
}

// FromUserIDContext 从上下文中获取用户ID
func FromUserIDContext(ctx context.Context) string {
    v := ctx.Value(userIDKey{})
    if v != nil {
        if s, ok := v.(string); ok {
            return s
        }
    }
    return ""
}

// NewTagContext 创建Tag上下文
func NewTagContext(ctx context.Context, tag string) context.Context {
    return context.WithValue(ctx, tagKey{}, tag)
}

// FromTagContext 从上下文中获取Tag
func FromTagContext(ctx context.Context) string {
    v := ctx.Value(tagKey{})
    if v != nil {
        if s, ok := v.(string); ok {
            return s
        }
    }
    return ""
}

// NewStackContext 创建Stack上下文
func NewStackContext(ctx context.Context, stack error) context.Context {
    return context.WithValue(ctx, stackKey{}, stack)
}

// FromStackContext 从上下文中获取Stack
func FromStackContext(ctx context.Context) error {
    v := ctx.Value(stackKey{})
    if v != nil {
        if s, ok := v.(error); ok {
            return s
        }
    }
    return nil
}

// WithContext Use context create entry
func WithContext(ctx context.Context) *Entry {
    if ctx == nil {
        ctx = context.Background()
    }

    fields := map[string]interface{}{}

    if version != "" {
        fields[VersionKey] = version
    }
    if v := FromTraceIDContext(ctx); v != "" {
        fields[TraceIDKey] = v
    }

    //if v := FromUserIDContext(ctx); v != "" {
    //  fields[UserIDKey] = v
    //}
    //
    //if v := FromTagContext(ctx); v != "" {
    //  fields[TagKey] = v
    //}
    //
    //if v := FromStackContext(ctx); v != nil {
    //  fields[StackKey] = fmt.Sprintf("%+v", v)
    //}

    return logrus.WithContext(ctx).WithFields(fields)
}

// Define logrus alias
var (
    //Tracef = logrus.Tracef
    Debugf = logrus.Debugf
    Infof  = logrus.Infof
    Warnf  = logrus.Warnf
    Errorf = logrus.Errorf
    Fatalf = logrus.Fatalf
    Panicf = logrus.Panicf
    Printf = logrus.Printf
    Info   = logrus.Info
    Debug  = logrus.Debug
    Error  = logrus.Error
)

这种形式较为复杂,还有一种简单的形式,就是只在具体的handlefunc 处理的时候,让logrus 增加一个hook即可;

先添加上面的hook.go,然后在具体的请求处理过程中,判断是debug请求(根据需要)的话,设置logrus的hook即可,这种形式最为简单;

if reqparms.Debug {
    logger.AddHook(hook.NewTraceIdHook(clientParams.ReqId))
}

其他业务模块可根据服务传递的traceId按照上面的方法,即可实现在不同模块使用相同traceId记录服务日志,如果能通过kafka、logd、filebeat等收集到es后即可一个id实现所有的关联日志查询; 参考: [1]gin-admin [2]从别人的代码中学习golang系列 [3]opentracing-go

本文链接:参与评论 »

--EOF--

提醒:本文最后更新于 1033 天前,文中所描述的信息可能已发生改变,请谨慎使用。

Comments