Golang Zerolog:Caller能否输出被包装HTTP处理函数的文件及行号?
问题描述
我用通用Handle函数包装HTTP Handler来复用错误处理逻辑,但Zerolog的Caller输出总是显示包装函数(handler.go里的Handle方法)的文件和行号。我希望能打印返回错误的被包装函数(比如endpoints.go里userHandler返回错误的那行)的对应行号,不确定调用栈里有没有相关信息,请问能不能通过当前实现做到?还是必须改成直接在HTTP Handler里记录日志?
相关代码
handler.go
func (h *Handler) Handle(fn appHandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if e := fn(h.app, w, r); e != nil { if e.Context != nil { logger := e.Context.Caller().Logger() // 此处打印的是当前文件及行号 logger.Error().Stack().Err(e.Error).Msg(e.LogMessage) } else { log.Error().Err(e.Error).Msg(e.LogMessage) } if e.HttpCode != 0 { http.Error(w, e.HttpMessage, e.HttpCode) } else { http.Error(w, e.HttpMessage, http.StatusInternalServerError) } } } }
endpoints.go
func init() { router.Post("/user", handler.Handle(userHandler)) } func userHandler(app *App, w http.ResponseWriter, r *http.Request) *AppError { userUid := r.Context().Value(authentication.UserUid).(string) logger := log.With().Str("userUid", userUid) // 业务逻辑处理 user, err := database.FindUser(userUid) if err != nil { return &AppError{Error: err, Context: &logger} // 希望此处的文件及行号被打印 } // 其他操作 return nil // 成功返回 }
解决方案
不需要直接在HTTP Handler内记录日志,以下两种可靠方式可实现需求:
方法一:在返回AppError时预先注入Caller信息
问题根源是你在包装函数内调用e.Context.Caller(),导致Caller指向包装函数的位置。只需在被包装Handler返回错误时,提前给logger添加Caller信息即可:
- 修改
userHandler的错误返回逻辑:
if err != nil { // 在此处调用Caller(),让Caller指向当前错误产生的行 return &AppError{Error: err, Context: logger.Caller()} }
- 修改
Handle方法内的日志初始化代码,移除多余的Caller()调用:
if e.Context != nil { logger := e.Context.Logger() logger.Error().Stack().Err(e.Error).Msg(e.LogMessage) }
此时Zerolog的Caller字段会显示userHandler中返回错误的行号,因为Caller()是在错误产生的位置执行的。
方法二:给AppError新增Caller字段,手动记录调用位置
若不想提前修改logger结构,可给AppError新增字段存储错误位置:
- 更新
AppError结构体:
type AppError struct { Error error Context *zerolog.Context Caller string // 新增:存储错误产生的文件及行号 HttpCode int HttpMessage string LogMessage string }
- 在返回错误时,用
runtime.Caller()获取当前位置:
import ( "fmt" "runtime" ) // ... if err != nil { _, file, line, ok := runtime.Caller(0) caller := "" if ok { caller = fmt.Sprintf("%s:%d", file, line) } return &AppError{Error: err, Context: &logger, Caller: caller} }
- 在包装函数的日志中添加该Caller字段:
if e.Context != nil { logger := e.Context.Logger() logger.Error().Stack().Err(e.Error).Str("caller", e.Caller).Msg(e.LogMessage) }
这种方式灵活性更高,可自定义Caller格式,且不依赖Zerolog的内置Caller机制。
不推荐的方案:用CallerSkip跳过栈帧
你也可以尝试在包装函数中使用CallerWithSkip(1)跳过当前栈帧:
logger := e.Context.CallerWithSkip(1).Logger()
但该方案依赖固定的栈帧层数,后续代码结构变更(如新增中间层)会导致Caller指向错误位置,可靠性远不如前两种方法。
内容的提问来源于stack exchange,提问作者Galen Howlett
相关产品推荐
相关产品推荐

