ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

GO-GRPC实践(二) 增加拦截器,实现自定义context(带request_id)、recover以及请求日志打印

2021-08-29 01:01:33  阅读:559  来源: 互联网

标签:拦截器 Context 自定义 GRPC middleware grpc context go zap


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

调用结果符合预期

标签:拦截器,Context,自定义,GRPC,middleware,grpc,context,go,zap
来源: https://www.cnblogs.com/Me1onRind/p/15201677.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有