Recientemente os hacíamos una introducción a GRPC en la que construíamos una pequeña API de Whislist, muy útil para estas fechas navideñas. Os explicábamos que eran los Protocol Buffer, así como generar nuestro cliente y servidor de manera casi mágica, pero nos dejamos temas en el tintero uno de ellos es el tema de los middlewares que tenemos por la mano en HTTP, pero, ¿cómo se realizan en gRPC?
Los interceptors
Primero que nada, debemos saber que los middlewares en gRPC se les denominan interceptors
, denominados así porque interceptan la ejecución de los métodos RPC tanto en el cliente como en el servidor. En ambos existen dos tipos de interceptors.
- UnaryInterceptor: intercepta las peticiones RPC unarias, en una petición RPC unaria el cliente envía una sola petición al servidor y recibe una sola petición de vuelta.
- StreamInterceptor: intercepta las peticiones RPC de streaming, en una las peticiones RPC de streaming, el cliente o el servidor, o incluso ambos a la vez, obtienen un stream de mensajes, y luego el cliente o el servidor leen dicho stream hasta que no queden mensajes.
Como hemos visto podemos crear ambos tipos de interceptors
tanto para nuestro cliente como para nuestro servidor, así que vamos a ver como crearlos.
Creando Interceptors gRPC en Go
Como hemos dicho tenemos dos tipos de interceptors
no vamos a ver un ejemplo de cada uno ya que nos quedaría un artículo algo extenso, pero si que vamos a ver que necesitamos para crear cada uno de ellos, y finalmente acabaremos montando algún ejemplo y utilizando otros que ya están creados.
UnaryClientInterceptor
Para crear un UnaryClientInterceptor
tendremos que llamar a la función WithUnaryInterceptor
la cual espera como argumento una función del tipo UnaryClientInterceptor
, dicho método nos devolverá un grpc.DialOption
:
func WithUnaryInterceptor(f UnaryClientInterceptor) DialOption
Y la definición del tipo de UnaryClientInterceptor
es la siguiente:
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts …CallOption) error
El parámetro invoker
será el handler
que se encargará de realizar la llamada RPC
, el cual no deja de ser una función, con la siguiente definición:
func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error
Así pues si queremos añadir nuestro nuevo interceptor
al levantar nuestro cliente, necesitaremos de una función auxiliar similar a esta:
func withClientUnaryInterceptor() grpc.DialOption {
return grpc.WithUnaryInterceptor(clientInterceptor)
}
Y luego sólo tendremos que añadir, dicho interceptor
en la construcción de nuestro dialer
:
conn, err := grpc.Dial(addr, grpc.WithInsecure(), withClientUnaryInterceptor())
StreamClientInterceptor
Como podréis suponer la parte del StreamClientInterceptor
es muy similar, salvo que esta vez, tendremos que llamar a la función WithStreamInterceptor
la cual necesitará un nuevo argumento del tipo StreamClientInterceptor
.
func WithStreamInterceptor(f StreamClientInterceptor) DialOption
Y la definición del StreamClientInterceptor
es:
type StreamClientInterceptor func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)
Ahora en vez de un invoker
tendremos un grpc.Streamer
el cual es una función cuya definición es la siguiente:
func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, opts ...CallOption) (ClientStream, error)
Una vez creado nuestro interceptor
podremos crear su método auxiliar y pasarlo nuevamente a nuestro dialer
.
UnaryServerInterceptor
La parte del servidor es más de lo mismo, pero con funciones basadas en la parte de servidor, algo que suele ser muy típico a nivel de middlewares es el poder logar que es lo que está sucediendo en nuestro sistema, y las llamadas que estamos recibiendo, así como cuanto tardan cada una.
Para poder crear un UnaryServerInterceptor
, habrá que llamar a la función UnaryInterceptor
pasando como argumento un UnaryServerInterceptor
, que nos devolverá un grpc.ServerOption
.
func UnaryInterceptor(i UnaryServerInterceptor) ServerOption
Si recordamos del anterior artículo a la hora de levantar un servidor gRPC, lo hacíamos con la función grpc.NewServer()
, pues bien está función admite que le pacemos un slice de grpc.ServerOption
, ¿vais uniendo los cabos?
¿Pero y cuál es la definición de un UnaryServerInterceptor
? Muy buena pregunta ya que tendremos que conocerla, para poder crear nuestros propios unary interceptors
:
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
Bueno, pues ya sabido esto pongámonos manos a la obra, y creemos un interceptor
para logar el tiempo que tardan nuestras peticiones en ser procesadas.
func LoggingServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
// Calls the handler
h, err := handler(ctx, req)
// Logging with grpclog (grpclog.LoggerV2)
grpclog.Infof("Request - Method:%s\tDuration:%s\tError:%v\n",
info.FullMethod,
time.Since(start),
err)
return h, err
}
Para empezar a utilizarlo primero que nada necesitaremos inicializar nuestro logger, vamos a utilizar el grpclog
que nos trae la librería propia de grpc
pero podríamos utilizar el que estemos acostumbrados.
grpcLog := grpclog.NewLoggerV2(os.Stdout, os.Stderr, os.Stderr)
grpclog.SetLoggerV2(grpcLog)
Después crearemos una función que llame como habíamos dicho a la función UnaryInterceptor
la cual nos devolverá el grpc.ServerOption
que necesitamos para pasárselo a nuestro servidor.
func withLoggingUnaryInterceptor() grpc.ServerOption {
return grpc.UnaryInterceptor(interceptor.LoggingServerInterceptor)
}
...
srv := grpc.NewServer(withLoggingUnaryInterceptor())
...
Ahora si ejecutamos nuestro servidor y recibimos alguna petición veremos un output, similar a este:
StreamServerInterceptor
Para crear un StreamServerInterceptor
, nos basaremos esta vez en llamar a la función StreamInterceptor
, la cual espera un StreamServerInterceptor
como parámetro y nos devolverá como ya hemos visto un grpc.ServerOption
func StreamInterceptor(i StreamServerInterceptor) ServerOption
Y la definición de la función de StreamServerInterceptor
no es otra que la siguiente:
type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error
Para aplicar nuestro interceptor
utilizaremos las funciones auxiliares como hemos visto hasta ahora.
Usar varios interceptors a la vez
Algo que gRPC no permite de fabrica ni en el lado cliente ni en el servidor, es utilizar más de un interceptor
del mismo tipo.
Pongamos en nuestro ejemplo de servidor que tenemos dos interceptors
el de logging y otro de authorization, obtendremos un error similar al siguiente:
Y esto es un problema, en HTTP siempre hemos podido utilizar todos aquellos middlewares
que necesitábamos, tranquilos, en gRPC también podréis vamos a ver como solucionar este pequeño problema.
go-grpc-middleware
Vamos a utilizar una librería de terceros para dotar a nuestro, cliente y servidor de la posibilidad de añadir más de un interceptor
, para ello vamos a usar la librería go-grpc-middleware, que es oficial del gRPC Ecosystem.
Vamos a ver un ejemplo de como solucionar nuestro problema, utilizando dicho paquete:
func withUnaryInterceptor() grpc.ServerOption {
return grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
interceptor.LoggingServerInterceptor,
interceptor.AuthorizationServerInterceptor,
))
}
...
srv := grpc.NewServer(withUnaryInterceptor())
...
Como veis nos hemos servido de la función ChainUnaryServer
para añadir todos nuestros interceptors
, los cuales serán ejecutados de izquierda a derecha, es decir, en este caso: logging y authorization.
El paquete obviamente nos da métodos tanto para la parte de streaming como unary, así como para cliente y servidor.
Conclusión
Hasta aquí llegamos con los interceptors
para gRPC hechos en Go, ahora es el momento de animarte tú e implementar tus propios interceptors
¿qué interceptor
crees que podría ser útil para nuestro proyecto de wishlist? Atrévete y mándanos una PR, con tu sugerencia.
Todo el ejercicio que hemos realizado lo podrás encontrar en la tag v0.2
Ya sabéis que si tenéis cualquier duda o comentario podéis dejarlo en los comentarios o en nuestro Twitter @FriendsofGoTech.