怎样开发一个grpc拦截器

怎样开发一个grpc拦截器

October 2, 2023
后端开发
Grpc

在开发grpc服务时,我们经常会遇到一些通用的需求,比如:日志、链路追踪、鉴权等。这些需求可以通过grpc拦截器来实现。本文使用go语言来实现一个 grpc一元模式(Unary)拦截器,上报链路追踪信息。

原始类型定义 #

我们可以在grpc的源码包里(interceptor.go),找到一元模式拦截器的类型定义:

// UnaryServerInterceptor provides a hook to intercept the execution of a unary RPC on the server. info
// contains all the information of this RPC the interceptor can operate on. And handler is the wrapper
// of the service method implementation. It is the responsibility of the interceptor to invoke handler
// to complete the RPC.
type UnaryServerInterceptor func(ctx context.Context, req any, info *UnaryServerInfo, handler UnaryHandler) (resp any, err error)

从上面的定义可以看出,一元模式拦截器是一个函数,接收四个参数,返回两个参数。下面我们来看一下这四个参数的含义:

  • ctx:上下文对象。
  • req:请求参数
  • info:包含了RPC的元信息,比如服务名、方法名等。
  • handler 实例的方法,用来调用实际的RPC方法。

我们只需要实现一个上述类型的函数,在里面实现我们的功能,然后再执行handler函数,就可以实现一个拦截器了。

实现拦截器 #

我们新建一个项目grpcdemo。

服务定义 #

我们先在项目目录下新建一个proto文件,定义一个服务:

hello.proto
syntax = "proto3";

package pb;

option go_package = "grpcdemo/pb";


service HelloService {
    rpc Hello(HelloRequest) returns (HelloResponse);
    rpc HelloAgain(HelloRequest) returns (HelloResponse);
}

message HelloRequest {
    string name = 1;
}

message HelloResponse {
    string message = 1;
}

定义一个Makefile:

protos:
	protoc --proto_path=./ --go_out=pb --go-grpc_out=pb --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative ./*.proto
tidy:
	go mod tidy

run:
	go mod tidy
	go run main.go

执行以下命令,生成go代码:

make protos

代码开发 #

第一步,新建一个tracing.go,初始化链路追踪器:

tracing.go
package main

import (
	"context"

	"go.opentelemetry.io/otel"

	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	sc "go.opentelemetry.io/otel/semconv/v1.4.0"
)

type ShutdownTracing func(ctx context.Context) error

func InitTracing(service string) (ShutdownTracing, error) {
	// Create the Jaeger exporter.
	exp, err := jaeger.New(jaeger.WithCollectorEndpoint())
	if err != nil {
		return func(ctx context.Context) error { return nil }, err
	}

	// Create the TracerProvider.
	tp := trace.NewTracerProvider(
		trace.WithBatcher(exp),
		trace.WithResource(resource.NewWithAttributes(
			sc.SchemaURL,
			sc.ServiceNameKey.String(service),
		)),
	)

	otel.SetTracerProvider(tp)
	return tp.Shutdown, nil
}

第二步,在main.go文件中,添加相关代码:

main.go
package main

import (
	"context"
	"go.opentelemetry.io/otel"
	"google.golang.org/grpc"
	"grpcdemo/pb"
	"log"
	"net"
)

type HelloServiceImpl struct {
	pb.UnimplementedHelloServiceServer
}

func (h *HelloServiceImpl) Hello(ctx context.Context, request *pb.HelloRequest) (resp *pb.HelloResponse, err error) {
	resp = &pb.HelloResponse{
		Message: "hello, " + request.Name,
	}

	return
}

func (h *HelloServiceImpl) HelloAgain(ctx context.Context, request *pb.HelloRequest) (resp *pb.HelloResponse, err error) {
	resp = &pb.HelloResponse{
		Message: "hello again, " + request.Name,
	}

	return
}

var (
	serviceName = "grpcdemo"
)

func main() {
	// 初始化链路追踪
	shutdown, err := InitTracing(serviceName)

	if err != nil {
		log.Fatalf("failed to init tracing:%v", err)
		return
	}

	defer shutdown(context.Background())

	lis, err := net.Listen("tcp", ":8091")

	if err != nil {
		log.Fatalf("failed to listen:%v", err)
		return
	}

	log.Println("listen success")

	s := grpc.NewServer(grpc.UnaryInterceptor(MyInterceptor))

	pb.RegisterHelloServiceServer(s, &HelloServiceImpl{})

	// 启动服务
	if err = s.Serve(lis); err != nil {
		log.Fatalf("failed to serve:%v", err)
		return
	}
}

// MyInterceptor 自定义拦截器
func MyInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
	_, s := otel.Tracer(serviceName).Start(ctx, info.FullMethod)
	defer s.End()

	return handler(ctx, req)
}

在上面的代码中,我们启动了一个grpc服务,监听8091端口。在启动grpc服务前,初始化了链路追踪信息,然后在grpc服务中,使用了自定义的拦截器。 在自定义拦截器中,我们上报了链路追踪信息。

启动jaeger服务 #

具体的启动方式,可以参考官方文档:https://www.jaegertracing.io/docs/1.26/getting-started/#all-in-one。

测试 #

我们使用goland的grpc插件,来测试一下:

### Hello
GRPC localhost:8091/pb.HelloService/Hello

{
  "name": "ZhangSan"
}



### HelloAgain
GRPC localhost:8091/pb.HelloService/HelloAgain

{
  "name": "ZhangSan"
}

测试结果: 测试结果

我们再打开jaeger的UI,查看链路追踪信息: 链路追踪 可以看到,我们的链路追踪信息已经上报到了jaeger服务。