如何在Go中为每个请求生成带追踪Token的日志
为每个请求生成唯一日志ID的Go实现方案
你当前的问题在于全局日志实例的前缀是服务启动时生成的固定ID,导致所有请求共用同一个ID。要实现每个请求对应唯一日志ID,核心是利用Go的context传递请求上下文信息,把请求ID绑定到每个请求的生命周期中。
步骤1:重构基础日志初始化
首先修改日志初始化逻辑,去掉固定的ID前缀,因为ID需要每个请求动态生成:
import ( "log" "time" "fmt" "context" "net/http" "github.com/matoous/go-nanoid/v2" "gopkg.in/natefinch/lumberjack.v2" ) var baseLogger *log.Logger func Init() { file := &lumberjack.Logger{ Filename: "info.log", MaxSize: 20, // 单位:MB MaxBackups: 5, Compress: true, } // 仅初始化基础日志实例,不添加请求ID前缀 baseLogger = log.New(file, "", log.Ldate|log.Ltime) }
步骤2:定义上下文键与请求ID注入逻辑
创建用于在context中存储请求ID的专属键,避免与其他上下文值冲突:
// 自定义上下文键类型,避免命名冲突 type ctxKeyRequestID struct{} // 生成请求ID并注入到上下文 func injectRequestID(ctx context.Context) context.Context { id, err := gonanoid.New() if err != nil { // 生成失败时使用时间戳作为 fallback ID id = fmt.Sprintf("fallback-%d", time.Now().UnixNano()) } return context.WithValue(ctx, ctxKeyRequestID{}, id) }
步骤3:封装带请求ID的日志函数
封装日志打印函数,从上下文自动取出请求ID并拼接到日志内容前:
func Info(ctx context.Context, format string, v ...interface{}) { reqID, ok := ctx.Value(ctxKeyRequestID{}).(string) if !ok { reqID = "unknown-request" } // 将请求ID作为日志前缀输出 baseLogger.Printf("[%s] %s", reqID, fmt.Sprintf(format, v...)) } // 同理可以封装Error、Debug等其他日志级别函数 func Error(ctx context.Context, format string, v ...interface{}) { reqID, ok := ctx.Value(ctxKeyRequestID{}).(string) if !ok { reqID = "unknown-request" } baseLogger.Printf("[%s] ERROR: %s", reqID, fmt.Sprintf(format, v...)) }
步骤4:通过中间件绑定请求ID到每个请求
如果是HTTP服务,使用中间件为每个进入的请求生成并注入ID:
func RequestIDMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 为当前请求上下文注入唯一ID ctx := injectRequestID(r.Context()) // 将携带ID的上下文传递给后续处理流程 next.ServeHTTP(w, r.WithContext(ctx)) }) }
步骤5:在业务逻辑中使用日志函数
在请求处理函数中,直接传入当前请求的上下文调用日志函数即可:
func main() { Init() mux := http.NewServeMux() mux.HandleFunc("/api/test", func(w http.ResponseWriter, r *http.Request) { // 传入请求上下文,自动携带唯一ID Info(r.Context(), "接收到请求,路径:%s", r.URL.Path) // 模拟业务逻辑 if err := someBusinessLogic(); err != nil { Error(r.Context(), "业务逻辑执行失败:%v", err) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("服务器内部错误")) return } Info(r.Context(), "请求处理完成") w.Write([]byte("请求成功")) }) // 用中间件包装路由,确保所有请求都能获取到唯一ID http.ListenAndServe(":8080", RequestIDMiddleware(mux)) } func someBusinessLogic() error { // 模拟错误 return fmt.Errorf("数据库查询超时") }
替代方案:每个请求创建独立日志实例
如果你更倾向于使用带前缀的日志实例,也可以在中间件中为每个请求创建专属的日志实例,再存入上下文:
type ctxKeyLogger struct{} func RequestIDMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id, err := gonanoid.New() if err != nil { id = fmt.Sprintf("fallback-%d", time.Now().UnixNano()) } // 基于基础日志创建带请求ID前缀的实例 reqLogger := log.New(baseLogger.Writer(), fmt.Sprintf("[%s] ", id), baseLogger.Flags()) // 将日志实例存入上下文 ctx := context.WithValue(r.Context(), ctxKeyLogger{}, reqLogger) next.ServeHTTP(w, r.WithContext(ctx)) }) } // 封装日志函数,从上下文取出专属实例 func Info(ctx context.Context, format string, v ...interface{}) { logger, ok := ctx.Value(ctxKeyLogger{}).(*log.Logger) if !ok { logger = baseLogger } logger.Printf(format, v...) }
这种方式的优势是无需每次手动拼接请求ID,日志实例的前缀已包含ID,直接调用打印即可。
内容的提问来源于stack exchange,提问作者htan
相关产品推荐
相关产品推荐

