GO-GRPC实践(二) 增加拦截器,实现自定义context(带request_id)、recover以及请求日志打印
2021/8/29 6:10:13
本文主要是介绍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
调用结果符合预期
这篇关于GO-GRPC实践(二) 增加拦截器,实现自定义context(带request_id)、recover以及请求日志打印的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-12-20go-zero 框架的 RPC 服务 启动start和停止 底层是怎么实现的?-icode9专业技术文章分享
- 2024-12-19Go-Zero 框架的 RPC 服务启动和停止的基本机制和过程是怎么实现的?-icode9专业技术文章分享
- 2024-12-18怎么在golang中使用gRPC测试mock数据?-icode9专业技术文章分享
- 2024-12-15掌握PageRank算法核心!你离Google优化高手只差一步!
- 2024-12-15GORM 中的标签 gorm:"index"是什么?-icode9专业技术文章分享
- 2024-12-11怎么在 Go 语言中获取 Open vSwitch (OVS) 的桥接信息(Bridge)?-icode9专业技术文章分享
- 2024-12-11怎么用Go 语言的库来与 Open vSwitch 进行交互?-icode9专业技术文章分享
- 2024-12-11怎么在 go-zero 项目中发送阿里云短信?-icode9专业技术文章分享
- 2024-12-11怎么使用阿里云 Go SDK (alibaba-cloud-sdk-go) 发送短信?-icode9专业技术文章分享
- 2024-12-10搭建个人博客网站之一、使用hugo创建个人博客网站