在gRPC中间件中使用Uber/Zap日志获取Go的真实错误来源
解决gRPC中间件中记录错误真实文件行号的问题
核心问题
你当前用runtime.Caller(2)获取的是中间件/当前函数调用栈的位置,而非错误实际产生的底层函数位置。要获取错误真实来源,需要让错误本身携带栈追踪信息,再在中间件中解析这些信息。
推荐解决方案:用zap+错误栈追踪库
1. 用github.com/pkg/errors包装错误(底层函数修改)
在错误产生的底层函数中,用errors.WithStack或errors.Wrap包装错误,让错误携带完整栈信息:
import "github.com/pkg/errors" // 底层支付实现示例 func (p *SomePayment) Exec(ctx context.Context, req *payment.PayRequest) (*payment.PayResponse, error) { // 模拟错误产生 if req.Amount <= 0 { // 包装错误,添加栈追踪 return nil, errors.WithStack(fmt.Errorf("invalid amount: %d", req.Amount)) } // ... 正常逻辑 }
2. 配置zap自动解析错误栈
调整你的日志初始化函数,确保zap能提取错误中的栈信息:
func RegisterLogger(c config.Config) *zap.SugaredLogger { var logger *zap.Logger var err error if c.IsDebug { // 开发环境默认包含栈追踪 logger, err = zap.NewDevelopment() } else { // 生产环境手动配置栈追踪字段 cfg := zap.NewProductionConfig() // 指定栈信息的字段名,默认是"stacktrace" cfg.EncoderConfig.StacktraceKey = "stacktrace" logger, err = cfg.Build(zap.AddStacktrace(zap.ErrorLevel)) // 仅Error级别记录栈 } if err != nil { panic(err) } defer logger.Sync() return logger.Sugar() }
3. 在gRPC中间件中记录错误
把错误记录逻辑移到gRPC中间件,直接用zap.Error(err)传递错误,zap会自动解析栈信息:
// gRPC一元中间件,统一记录错误 func ErrorLoggingMiddleware(logger *zap.SugaredLogger) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { resp, err := handler(ctx, req) if err != nil { logger.Error("gRPC请求处理失败", zap.String("method", info.FullMethod), zap.Error(err)) // 自动携带错误栈及来源文件行号 } return resp, err } } // 注册gRPC服务时添加中间件 func NewGRPCServer(logger *zap.SugaredLogger) *grpc.Server { return grpc.NewServer( grpc.UnaryInterceptor(ErrorLoggingMiddleware(logger)), ) }
备选方案:自定义带栈的错误类型
如果不想引入第三方库,可以自定义错误类型携带栈信息:
import ( "errors" "runtime" "strings" ) type StackedError struct { Err error Stack []uintptr } func (e *StackedError) Error() string { return e.Err.Error() } func (e *StackedError) Unwrap() error { return e.Err } // NewStackedError 创建带栈的错误,skip跳过当前函数和调用者 func NewStackedError(err error, skip int) *StackedError { stack := make([]uintptr, 32) n := runtime.Callers(skip+2, stack) // +2跳过NewStackedError和runtime.Callers本身 return &StackedError{Err: err, Stack: stack[:n]} } // 在中间件中解析栈信息 func parseStack(stack []uintptr) []string { var lines []string for _, pc := range stack { fn := runtime.FuncForPC(pc) if fn == nil { continue } file, line := fn.FileLine(pc) // 提取函数名(去掉包路径) funcName := fn.Name() if idx := strings.LastIndex(funcName, "."); idx != -1 { funcName = funcName[idx+1:] } lines = append(lines, fmt.Sprintf("%s:%d (%s)", file, line, funcName)) } return lines }
使用时,底层函数抛错:
return nil, NewStackedError(fmt.Errorf("invalid amount"), 1)
中间件中解析:
if err != nil { var stackedErr *StackedError if errors.As(err, &stackedErr) { stackLines := parseStack(stackedErr.Stack) logger.Error("请求失败", zap.String("method", info.FullMethod), zap.Strings("error_stack", stackLines), zap.Error(err)) } }
不推荐的方案:调整runtime.Caller的skip参数
直接修改runtime.Caller(skip)中的skip值(比如改成3、4),虽然可能暂时拿到正确位置,但调用链一旦变化(如新增中间件、函数嵌套),skip值就会失效,维护成本极高,不建议使用。
内容的提问来源于stack exchange,提问作者Charles
相关产品推荐
相关产品推荐

