zoukankan      html  css  js  c++  java
  • GO-GRPC实践(二) 增加拦截器,实现自定义context(带request_id)、recover以及请求日志打印

    demo代码地址

    https://github.com/Me1onRind/go-demo

    拦截器原理

    和gin或django的middleware一样, 在请求真正到达请求方法之前, 框架会依次调用注册的middleware函数, 可以基于此方便的对每个请求进行身份验证、日志记录、限流等功能

    拦截器函数原型

    func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
    

    入参

    • ctx 请求上下文
    • req 请求报文
    • info 请求的接口信息
    • handler 下一个拦截器(或真正的请求方法)

    返回值

    • resp 返回报文
    • err 错误

    新增目录

    ├── internal
        ├── core
            ├── common
            │   ├── context.go # 自定义上下文
            ├── middleware
                ├── context.go # 生成自定义上下文
                ├── logger.go  # 日志记录
                └── recover.go # recover
    
    
    

    代码实现

    自定义上下文

    ​ go语言中自身没有支持类似于java的 LocalThread变量, 也不推荐使用(如用协程id+map), 而是推荐使用一个上下文变量显示的传递。 而在实际使用(如记录请求的request_id)时, go语言自带的context.Context并不能很好的满足需求(取值时需要断言, 不方便维护也容易出问题)。

    实践中一个比较好的办法就是实现一个自定义的context

    common/context.go

    zap.Logger的用法不是重点, 这里只是简单的初始化

    package common
    
    import (
        "context"
        "os"
    
        "github.com/google/uuid"
        "go.uber.org/zap"
        "go.uber.org/zap/zapcore"
    )
    
    type contextKey struct{}
    
    var (
        logger *zap.Logger
        cKey   = contextKey{}
    )
    
    func init() {
        config := zap.NewProductionEncoderConfig()
        config.EncodeDuration = zapcore.MillisDurationEncoder
        config.EncodeTime = zapcore.ISO8601TimeEncoder
        core := zapcore.NewCore(zapcore.NewConsoleEncoder(config), zapcore.AddSync(os.Stdout), zapcore.InfoLevel)
        logger = zap.New(core, zap.AddCaller())
    }
    
    type Context struct {
        context.Context
    
        Logger *zap.Logger // 带上下文信息的logger, 如request_id
    }
    
    func NewContext(ctx context.Context) *Context {
        c := &Context{}
        c.Context = storeContext(ctx, c)
        requestID, _ := uuid.NewRandom()
        c.Logger = logger.With(zap.String("request_id", requestID.String()))
        return c
    }
    
    // 拦截器之间直接只能通过context.Context传递, 所以需要将自定义context存到go的context里向下传
    func storeContext(c context.Context, ctx *Context) context.Context {
        return context.WithValue(c, cKey, ctx)
    }
    
    func GetContext(c context.Context) *Context {
        return c.Value(cKey).(*Context)
    }
    

    拦截器

    middleware/context.go

    生成自定义context

    package middleware
    
    import (
        "context"
    
        "github.com/Me1onRind/go-demo/internal/core/common"
        "google.golang.org/grpc"
    )
    
    func GrpcContext() grpc.UnaryServerInterceptor {
        return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
            commonCtx := common.NewContext(ctx)
            return handler(commonCtx, req)
        }   
    }
    

    middleware/recover.go

    recover防止单个请求中的panic, 导致整个进程挂掉, 同时将panic时的堆栈信息保存到日志文件, 以及返回error信息

    package middleware
    
    import (
        "context"
        "errors"
        "fmt"
        "runtime/debug"
    
        "github.com/Me1onRind/go-demo/internal/core/common"
        "go.uber.org/zap"
        "google.golang.org/grpc"
    )
    
    func GrpcRecover() grpc.UnaryServerInterceptor {
        return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
            defer func() {
                commonCtx := common.GetContext(ctx)
                if e := recover(); e != nil {
                    commonCtx.Logger.Error("server panic", zap.Any("panicErr", e)) 
                    commonCtx.Logger.Sugar().Errorf("%s", debug.Stack())
                    err = errors.New(fmt.Sprintf("panic:%v", e)) 
                }   
            }() 
            resp, err = handler(ctx, req)
            return resp, err 
        }   
    }
    

    middleware/logger.go

    记录请求的入参、返回值、请求方法和耗时

    使用defer而不是放在handler之后是 防止打印日志之前代码panic, 类似的场景都可以使用defer来保证函数退出时某些步骤必须执行

    package middleware
    
    import (
        "context"
        "time"
    
        "github.com/Me1onRind/go-demo/internal/core/common"
        "go.uber.org/zap"
        "google.golang.org/grpc"
    )
    
    func GrpcLogger() grpc.UnaryServerInterceptor {
        return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
            begin := time.Now()
            defer func() {
                commonCtx := common.GetContext(ctx)
                commonCtx.Logger.Info("access request", zap.Reflect("req", req), zap.Reflect("resp", resp),
                    zap.String("method", info.FullMethod), zap.Error(err), zap.Duration("cost", time.Since(begin)),
                )
            }()                         
            resp, err = handler(ctx, req)
            return resp, err
        }                                          
    }
    

    将拦截器加载到grpc.Server中

    原生的grpc.Server只支持加载一个拦截器, 为了避免将所有拦截器功能写到一个函数里 使用go-grpc-middleware这个第三方包, 相当于提供一个使用多个拦截器的语法糖

    拦截器执行顺序和入参顺序保持一致

    package main
    
    import (
        // ...
        "github.com/Me1onRind/go-demo/internal/core/middleware"
        grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
    )
    
    func main() {
        // ...
        s := grpc.NewServer(grpc_middleware.WithUnaryServerChain(
            middleware.GrpcContext(),
            middleware.GrpcRecover(),
            middleware.GrpcLogger(),
        ))
        // ...
    }
    

    验证

    给FooServer新增两个方法并实现:

    • ErrorResult 返回错误
    • PanicResult 直接panic

    调用结果符合预期

  • 相关阅读:
    mybatis学习笔记
    markdownPad常用功能示例
    2018-2019-2 《Java程序设计》第3周学习总结
    2018-2019-2 《Java程序设计》第2周学习总结
    2018-2019-2 《Java程序设计》第1周学习总结
    Djnago models 一对多、多对多
    Superset 安装
    lvm 添加分区
    partprobe 重新检测Linux系统分区
    Docker 、Docker Compose 安装
  • 原文地址:https://www.cnblogs.com/Me1onRind/p/15201677.html
Copyright © 2011-2022 走看看