You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 04:44:56