封装logrus.Logger函数时如何保留调用处行号前缀?
这个问题我之前也碰到过!原因很简单——你现在启用的log.SetReportCaller(true),只会追踪到直接调用logrus原生方法的那一行,也就是你封装的Info()函数里的log.Info(args...)这行代码,所以日志自然会显示logging.go的行号和封装函数名。
要让日志显示真正调用logging.Info()的业务代码位置,我们需要跳过封装函数这一层调用栈,获取上层的调用者信息。这里有两种可靠的实现方式:
方法一:手动抓取调用者信息(推荐,不依赖logrus内部实现)
这种方法我们不用logrus自带的调用者追踪,而是在封装函数里自己获取业务代码的调用位置,再把这些信息作为自定义字段加到日志里。
修改你的代码如下:
package logging import ( "fmt" "github.com/sirupsen/logrus" "path" "runtime" "strings" ) var log *logrus.Logger func init() { log = logrus.New() // 关闭logrus自带的caller追踪,我们自己处理 log.Formatter = &logrus.TextFormatter{ // 自定义日志格式,把调用者信息放在前面 Format: func(entry *logrus.Entry) ([]byte, error) { // 取出我们提前加好的调用者字段 caller := entry.Data["caller"].(string) funcName := entry.Data["func"].(string) // 仿照logrus默认格式拼接日志行 logLine := fmt.Sprintf( "%s[%s]%s %s %s\n", strings.ToUpper(entry.Level.String()), entry.Time.Format("0000"), caller, funcName, entry.Message, ) return []byte(logLine), nil }, } } // 工具函数:跳过封装层,获取真正的调用者帧信息 func getRealCallerFrame() *runtime.Frame { // Skip参数说明: // 0: runtime.Callers 本身 // 1: getRealCallerFrame 这个函数 // 2: logging.Info/Warn 等封装函数 // 3: 真正调用logging.Info的业务代码 pcBuffer := make([]uintptr, 10) frameCount := runtime.Callers(3, pcBuffer) if frameCount == 0 { return nil } frames := runtime.CallersFrames(pcBuffer[:frameCount]) frame, _ := frames.Next() return frame } func Info(args ...interface{}) { callerFrame := getRealCallerFrame() var callerStr, funcStr string if callerFrame != nil { // 只保留文件名(去掉全路径)和行号 callerStr = fmt.Sprintf("%s:%d", path.Base(callerFrame.File), callerFrame.Line) // 显示完整函数名 funcStr = fmt.Sprintf("%s()", callerFrame.Function) } else { // 兜底处理,防止获取失败 callerStr = "unknown:0" funcStr = "unknown()" } // 添加调用者字段并输出日志 entry := log.WithFields(logrus.Fields{ "caller": callerStr, "func": funcStr, }) entry.Info(args...) }
方法二:修改logrus的CallerPrettyfier跳过封装层
如果你更倾向于用logrus自带的ReportCaller功能,可以在CallerPrettyfier里判断当前帧是不是封装函数,如果是,就往上找一层调用栈:
package logging import ( "fmt" "github.com/sirupsen/logrus" "path" "runtime" "strings" ) var log *logrus.Logger func init() { log = logrus.New() log.SetReportCaller(true) log.Formatter = &logrus.TextFormatter{ CallerPrettyfier: func(f *runtime.Frame) (string, string) { // 替换成你实际的包路径,比如"myfolder/logging." if strings.HasPrefix(f.Function, "your-module-path/logging.") { // 跳过当前封装函数,获取上层调用者 pcBuffer := make([]uintptr, 2) // Skip参数需要根据调用栈调整,这里是4: // 0: runtime.Callers, 1: 当前的CallerPrettyfier, 2: logrus内部的caller逻辑, 3: 封装函数, 4: 业务代码调用者 frameCount := runtime.Callers(4, pcBuffer) if frameCount > 0 { frames := runtime.CallersFrames(pcBuffer[:frameCount]) if realFrame, ok := frames.Next(); ok { filename := path.Base(realFrame.File) return fmt.Sprintf("%s()", realFrame.Function), fmt.Sprintf("%s:%d", filename, realFrame.Line) } } // 获取失败就用当前帧兜底 } // 非封装函数的情况,正常处理 filename := path.Base(f.File) return fmt.Sprintf("%s()", f.Function), fmt.Sprintf("%s:%d", filename, f.Line) }, } } func Info(args ...interface{}) { log.Info(args...) }
效果验证
当你在业务代码里调用logging.Info("test log")时,日志就会输出你想要的格式:INFO[0000]myfile.go:39 myfolder/myfile.myfunction() test log
内容的提问来源于stack exchange,提问作者webpacket
相关产品推荐
相关产品推荐

