Basics of gRPC Interceptors

Author:

May 1, 2023

8 min read

800px-Go_Logo_Blue.svg.png

gRPC is an open-source, high-performance framework for developing remote procedure call (RPC) applications. Created by Google, it is now a part of the Cloud Native Computing Foundation (CNCF). One of gRPC's key features is interceptor support. In this blog, we will explore interceptors and their usage in gRPC applications written in the Go programming language, using a monitoring example.

Why Interceptors in gRPC?

Interceptors in gRPC are valuable because they allow developers to add custom logic to the request/response processing pipeline.

One compelling reason for using gRPC interceptors is that they enable the implementation of cross-cutting concerns in a modular and reusable way. For example, if we want to add authentication to all of our gRPC services, instead of modifying each service separately, we can write a single interceptor. This interceptor can check the authentication token and add the user ID to the request context. We can then add this interceptor to any gRPC service we create without needing to make changes to the service code.

Another interesting benefit of using gRPC interceptors is the ability to implement features like tracing and monitoring. By including an interceptor that logs the start and end of each request, we can easily trace the flow of requests through our system and identify any performance or reliability issues. Additionally, by incorporating an interceptor that collects metrics on request/response sizes and latencies, we can monitor the health of our system and detect any anomalies.

What are interceptors in gRPC?

Interceptors are a powerful feature of gRPC that allows you to intercept and modify client-server requests and responses. They act as middleware between the client and server handlers, intercepting requests and responses as they travel through the network stack.

  • Interceptors can ensure that only authorized users have access to resources by handling authentication and authorization.
  • Interceptors can log request and response metadata to assist with debugging and performance analysis.
  • Caching: Interceptors can reduce network requests by caching responses.
  • Compression and encryption: Interceptors can improve performance and security by compressing and encrypting requests and responses.

Using Go to implement interceptors in gRPC

In this blog, we will create a logging middleware to help log messages. Follow the steps below to achieve this.

prerequisite:

Install the required modules from go package

go

go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Create a proto file with the messages and services required.

protobuf

syntax = "proto3"; package demo; option go_package="./;demo"; message DemoRequest { string message = 1; } message DemoResponse { string message = 1; } // gRPC service which has a Demo method returns message as response service MyService { rpc DemoMethod(DemoRequest) returns (DemoResponse) {} }
  • There is only one method named "DemoMethod" in the service, which is defined in the "demo" package.
  • A message of type "DemoRequest" is passed to the "DemoMethod" in order to receive it, and it returns a message of type "DemoResponse" in return.
  • The messages include a single field called "message" that is of the string data type and has the tag value 1.
  • The "rpc" keyword is used in the "MyService" service definition to denote remote procedure calls for client-server communication.

Compile this proto file using protoc with the following command:

shell

mkdir pb && protoc --go_out=./pb --go-grpc_out=./pb proto/*.proto
  • Using Protocol Buffers files in the "proto" directory, this command creates a directory called "pb" and generates Go code for gRPC services and the messages that go along with them.
  • The resulting code comprises message definitions, gRPC server and client stubs, and is stored in the "pb" directory.

Start building your server using Go as explained below. Open Terminal and type the following commands:

shell

# Please use your module name for the further references go mod init YourModuleNameGoesHere # This line will create a main.go file in root dir touch main.go

Open main.go file and Try this code:

go

package main import ( "context" "log" "net" pb "YourModuleNameGoesHere/pb" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) // gRPC loggingInterceptor which helps to log func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { log.Printf("Received request: %v", req) resp, err := handler(ctx, req) return resp, err } type Server struct { pb.UnimplementedMyServiceServer } func main() { // Create a new gRPC server with the logging interceptor s := grpc.NewServer( grpc.UnaryInterceptor(loggingInterceptor), ) // Register your gRPC service with the server myService := &Server{} pb.RegisterMyServiceServer(s, myService) reflection.Register(s) // Listen on port 50051 lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } log.Printf("Starting server in port :%d\\n", 50051) // Start the server if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
  • We import the required packages, such as the gRPC framework from google.golang.org/grpc and our produced protobuf package pb, which defines our gRPC service and messages.
  • To implement the MyServiceServer interface created by protobuf, we define a struct called Server. For each RPC method listed in our protobuf service description file, a corresponding method is present in this interface.
  • The "DemoMethod" method, which takes a Request message as input and returns a Respond message, is what we implement. In this method, we simply send back a Respond message with a Message field that combines the Message field from the incoming Request message with the string "Hello".
  • To log incoming requests, we define the loggingInterceptor function. This function accepts a "grpc.UnaryServerInfo" object, which contains information about the called RPC method, along with a context object representing the incoming request and the context object itself. It also takes a "grpc.UnaryHandler" object, which is a function responsible for processing incoming requests and sending back responses.
  • We use "grpc.NewServer()" to start a new gRPC server. By passing in the loggingInterceptor function as a unary interceptor using the command "grpc.UnaryInterceptor(loggingInterceptor)", we enable logging for incoming requests.
  • Using pb.RegisterMyServiceServer(s, myService), we inform the server about our Server struct. This instructs the gRPC server to use our Server struct to handle requests for our service.
  • To listen for incoming gRPC requests on port 50051, we use net.Listen("tcp", ":50051").
  • Lastly, s.Serve(lis) is used to launch the gRPC server. This starts an infinite loop that listens for incoming requests and processes them using our Server struct.

To check this code, I have used Evans CLI, a development tool specifically designed for creating and testing gRPC APIs. This tool provides an interactive shell with features like auto-completion and syntax highlighting. It also offers the convenience of generating client and server stubs automatically. By using Evans CLI, the development and testing of gRPC APIs are accelerated and simplified.

Another way to test the code is by creating client-side code. For simplicity, I have opted to use the Evans CLI method.

First, run the server using the following step in the terminal:

shell

go run main.go

Open a new terminal and run the evans cli which works like a client:

shell

evans -r repl -p 50051 # This command helps to choose the package package demo # To check the services defined show services :' # Output generated +-----------+------------+--------------+---------------+ | SERVICE | RPC | REQUEST TYPE | RESPONSE TYPE | +-----------+------------+--------------+---------------+ | MyService | DemoMethod | DemoRequest | DemoResponse | +-----------+------------+--------------+---------------+ ' # To call this method use this command call DemoMethod :' # Output generated # Example 1 demo.MyService@127.0.0.1:50051> call DemoMethod message (TYPE_STRING) => Dr.Strange { "message": "Hello Dr.Strange" } # Example 2 demo.MyService@127.0.0.1:50051> call DemoMethod message (TYPE_STRING) => Compage { "message": "Hello Compage" } #logs generated from server 2023/03/31 23:37:40 Received request: message:"Dr.Strange" 2023/03/31 23:39:16 Received request: message:"Compage" ' # To stop CLI use this command exit

Conclusion

In conclusion, we have learned how to use Go to implement an interceptor in a gRPC server. Interceptors allow us to enhance the functionality of our gRPC server, such as logging or authentication, by intercepting incoming requests and performing additional actions on them before they are handled by the server. We covered the steps to define a server struct that implements the protobuf interface, define a logging interceptor function, and create a new gRPC server with the interceptor. In addition, we registered our service with the server in this example and tested it using the Evans CLI. Please feel free to share any suggestions you may have.

Source Code

Please refer to the code here.