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

調用結果符合預期


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM