gRPC's Go library provides interfaces for creating your own custom interceptors (i.e. middleware functions), and I'm attempting to write two logging interceptors. The first is a Unary Server Interceptor where I'm easily able to log the request parameters using the object passed into the interceptor function.
func loggingUnary(context context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
resp, err := handler(context, req)
printLogMessage(err, info.FullMethod, context, time.Since(start), req)
return resp, err
}
How can I do the same with the Stream Server Interceptor which doesn't conveniently pass the request object as a parameter? Is there another way to access the request?
func loggingStream(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
start := time.Now()
err := handler(srv, stream)
printLogMessage(err, info.FullMethod, stream.Context(), time.Since(start), "")
return err
}
This is a bit old now, but the easiest way to extend your interception into the stream is to create a grpc.ServerStream wrapper, then wrap the real ServerStream in your interceptor. In that way, your intercepting code can handle the received and sent messages in the stream.
// A wrapper for the real grpc.ServerStream
type LoggingServerStream struct {
inner grpc.ServerStream
}
func (l LoggingServerStream) SetHeader(m metadata.MD) error {
return l.SetHeader(m)
}
func (l LoggingServerStream) SendHeader(m metadata.MD) error {
return l.SendHeader(m)
}
func (l LoggingServerStream) SetTrailer(m metadata.MD) {
l.SetTrailer(m)
}
func (l LoggingServerStream) Context() context.Context {
return l.Context()
}
func (l LoggingServerStream) SendMsg(m interface{}) error {
fmt.Printf("Sending Message: type=%s\n", reflect.TypeOf(m).String())
return l.SendMsg(m)
}
func (l LoggingServerStream) RecvMsg(m interface{}) error {
fmt.Printf("Receiving Message: type=%s\n", reflect.TypeOf(m).String())
return l.RecvMsg(m)
}
The interceptor:
func LoggingStreamInterceptor() grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
return handler(srv, LoggingServerStream{inner:ss})
}
}
Any state you need to keep and go in your wrapper.
The stream handler can be called multiple times during the lifetime of the request that created the stream, that's why the request is not part of the arguments to the handler (and to any interceptor, either). You could place the request (or better, a copy of the data that you want logged and not a reference to the request itself) in the stream context (assuming you are in control of the code that creates the ServerStream object). I would rather log the request parameters once, when the stream is created and not on every call to the handler (so each request is logged once only).
Related
I have the following gRPC interceptor running serverside, which wraps a serverstream and passes it on to the next handler:
// HarmonyContext contains a custom context for passing data from middleware to handlers
type HarmonyContext struct {
context.Context
Request interface{}
UserID uint64
Limiter *rate.Limiter
}
type IHarmonyWrappedServerStream interface {
GetWrappedContext() HarmonyContext
}
type HarmonyWrappedServerStream struct {
grpc.ServerStream
WrappedContext HarmonyContext
}
func (ss HarmonyWrappedServerStream) GetWrappedContext() HarmonyContext {
return ss.WrappedContext
}
func (m Middlewares) HarmonyContextInterceptorStream(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
wrapped := WrapServerStream(ss)
return handler(srv, wrapped)
}
func WrapServerStream(stream grpc.ServerStream) HarmonyWrappedServerStream {
if existing, ok := stream.(HarmonyWrappedServerStream); ok {
return existing
}
return HarmonyWrappedServerStream{ServerStream: stream, WrappedContext: HarmonyContext{
Context: stream.Context(),
}}
}
and in the handler itself, I have the following code:
func (v1 *V1) StreamGuildEvents(r *corev1.StreamGuildEventsRequest, s corev1.CoreService_StreamGuildEventsServer) error {
wrappedStream := s.(middleware.IHarmonyWrappedServerStream)
println(wrappedStream)
return nil
}
However, I get the following runtime error when sending a streaming request:
interface conversion: *corev1.coreServiceStreamGuildEventsServer is not middleware.IHarmonyWrappedServerStream: missing method GetWrappedContext
In fact, the ServerStream in the handler is completely different from the one in the interceptors. Is there any way to make the interceptor pass the custom ServerStream properly?
We've opened up one of our repos to demonstrate this if it helps anybody.
https://github.com/drud/api-common/blob/main/interceptors/state.go#L207
Very similar to #Bluskript impl however I do not have the same issue encountered above. I believe this might be from the return type grpc.StreamServerInterceptor and where I am using the context getter.
I have a unary interceptor that contains the following code:
func (m Middlewares) LocationInterceptor(c context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
ctx := c.(HarmonyContext)
location, ok := req.(interface{ GetLocation() *corev1.Location })
if !ok {
panic("location middleware used on message without a location")
}
ctx.Location := location.GetLocation()
return handler(c, req)
}
How would I be able to convert this to a stream interceptor, if I know that the stream will definitely only stream from server to client? In addition, is there any way to make it only intercept when the moment the stream begins?
func (m Middlewares) LocationInterceptorStream(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
wrappedStream := ss.(HarmonyWrappedServerStream)
return handler(srv, wrappedStream)
}
For a "server streaming RPC", the client sends one message (the request), and the server responds with multiple messages. In the stream interceptor, you need to call ServerStream.RecvMsg(...) once to get the request from the client. You will then need to pass a "wrapped stream" into handler which will later return this message on the first call to RecvMsg. For gRPC using protobuf messages, you can do something like the following. This serverStreamWrapper will allow you to call peekRequest(...) in the interceptor to get the request, then pass the stream on to the handler.
For this specific example, you may also be able to implement the logic in the wrapped RecvMsg(...) function itself. This function gets called by gRPC to read the request from the client to the server. This may be simpler than trying to store the request.
type serverStreamWrapper struct {
peekedRequest proto.Message
wrappedStream grpc.ServerStream
}
func newServerStreamWrapper(stream grpc.ServerStream) *serverStreamWrapper {
return &serverStreamWrapper{nil, stream}
}
func (s *serverStreamWrapper) peekRequest(msg interface{}) error {
protoMsg := msg.(proto.Message)
if protoMsg == nil {
panic("BUG: msg must not be nil")
}
if s.peekedRequest != nil {
panic("BUG: Must only called peekRequest once")
}
err := s.wrappedStream.RecvMsg(protoMsg)
if err == nil {
s.peekedRequest = protoMsg
}
return err
}
func (s *serverStreamWrapper) RecvMsg(msg interface{}) error {
if s.peekedRequest != nil {
protoMsg := msg.(proto.Message)
proto.Reset(protoMsg)
proto.Merge(protoMsg, s.peekedRequest)
s.peekedRequest = nil
return nil
}
return s.wrappedStream.RecvMsg(msg)
}
I am building an API using gRPC and in server side, I want to receive a notification when a client disconnects, identify it and perform some tasks based on that.
So far, I was able to detect client disconnection using grpc.StatsHandler method HandleConn. I tried passing values using context, but they can't be accessed from server side.
Client side:
conn, err := grpc.DialContext(
context.WithValue(context.Background(), "user_id", 1234),
address,
grpc.WithInsecure(),
)
Server side:
// Build stats handler
type serverStats struct {}
func (h *serverStats) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context {
return ctx
}
func (h *serverStats) HandleRPC(ctx context.Context, s stats.RPCStats) {}
func (h *serverStats) TagConn(ctx context.Context, info *stats.ConnTagInfo) context.Context {
return context.TODO()
}
func (h *serverStats) HandleConn(ctx context.Context, s stats.ConnStats) {
fmt.Println(ctx.Value("user_id")) // Returns nil, can't access the value
switch s.(type) {
case *stats.ConnEnd:
fmt.Println("client disconnected")
break
}
}
// Build server
s := grpc.NewServer(grpc.StatsHandler(&serverStats{}))
I want to access the value passed from client side in server side. What is the right way to do it, or is there any other way to identify the client that has disconnected?
I don't think you may pass that value when you are dialling, but you may tag the connection and than all next client requests will have the same value in its context:
In your serverStats implementation:
func (h *serverStats) TagConn(ctx context.Context, info *stats.ConnTagInfo) context.Context {
return context.WithValue(ctx, "user_counter", userCounter++)
}
In your service implementation
func (s *service) SetUserId(ctx context.Context, v MyMessage) (r MyResponse, err Error) {
s.userIdMap[ctx.Value("user_counter")] = v.userId
...
}
In the other methods:
func (s *service) AnotherMethod(ctx context.Context, v MyMessage) (r MyResponse, err Error) {
userId := s.userIdMap[ctx.Value("user_counter")]
...
}
func (h *serverStats) HandleConn(ctx context.Context, s stats.ConnStats) {
switch s.(type) {
case *stats.ConnEnd:
fmt.Printf("client %d disconnected", s.userIdMap[ctx.Value("user_counter")])
break
}
}
Please let me know if you find a way to pass values in the dialling step.
I tried passing values using context, but they can't be accessed from server side.
You need to set metadata fields to the client context explicitly:
ctx := context.Background()
ctx = metadata.AppendToOutgoingContext(ctx, "user_id", "1234")
conn, err := grpc.DialContext(cxt, address)
On server side you can retrieve them like this:
md, ok := metadata.FromIncomingContext(ctx)
What is the right way to do it, or is there any other way to identify the client that has disconnected?
This really depends on your use case. I think that GRPC Stats API is good for some simple tasks (for example, to compute latency or aggregate network stats), but it's not very useful when some business logic should work when client leaves. I would suggest using defer calls in GRPC handlers directly for that. One more option is to implement custom GRPC interceptor.
In order to perform Authorization, some attributes from the request is to be read so that input for Authorization Server can be made
For example, this is the interceptor. Here prepareAuthZInput is called to preparing the input
func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
input := prepareAuthZInput(info.FullMethod, req)
}
In this function, there's a big if-else part which checks for the actual type for the request, type casts it and then performs the input preparation.
func prepareAuthZInput(method string, req interface{}) {
var input Input
if methodName = "/Data/Call" {
callRequest, ok := req.(CallRequest)
if ok {
// prepare input from callRequest
}
} else if methodName = "/Data/Receive" {
receiveRequest, ok := req.(ReceiveRequest)
if ok {
// prepare input from receiveRequest
}
}
return input
}
How can I improve this code?
When doing something like this, it's typical to add auth data to the metadata instead of the request messages. This way the server doesn't need to inspect all the possible request payload types.
If you must use the request payload, it would be more idiomatic to use a type switch instead:
switch r := req.(type) {
case CallRequest: // r is a CallRequest...
case ReceiveRequest: // r is a ReceiveRequest...
default:
return status.Errorf(codes.Unimplemented, "unknown request type: %T", req)
}
I need to log the response body in a middleware of gin, but I don't find how to get the response body. Can anyone help?
I am using a middleware like this:
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
statusCode := c.Writer.Status()
if statusCode >= 400 {
//ok this is an request with error, let's make a record for it
//log body here
}
}
}
My question is, how to get response body from Context in middleware?
You need to intercept writing of response and store it somewhere first. Then you can log it. And to do that you need to implement your own Writer intercepting Write() calls.
For example, as follows:
type bodyLogWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
func ginBodyLogMiddleware(c *gin.Context) {
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
c.Writer = blw
c.Next()
statusCode := c.Writer.Status()
if statusCode >= 400 {
//ok this is an request with error, let's make a record for it
// now print body (or log in your preferred way)
fmt.Println("Response body: " + blw.body.String())
}
}
Then use this middleware like this:
router.Use(ginBodyLogMiddleware)
Note that this sill won't work for static files as gin does not seem to use c.Writer for them. But in most cases, that's what you want anyway.
If you want to intercept all files, you need to use a slightly more complicated approach. Instead of Middleware, you'll need to implement a wrapper http.Handler that will wrap gin.Engine and will use same approach as shown above to intercept and log whatever is written to http.ResponseWriter. Then run gin server like this:
ginRouter := gin.New()
// configure your middleware and routes as required
// Run http server as follows, where bodyLogHandler is your wrapper handler
http.ListenAndServe(bindAddress, &bodyLogHandler{wrappedHandler: ginRouter}
FYI
Note: implement WriteString() if using c.String() for writing response body
type bodyLogWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
func (w bodyLogWriter) WriteString(s string) (int, error) {
w.body.WriteString(s)
return w.ResponseWriter.WriteString(s)
}
func ginBodyLogMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
c.Writer = blw
c.Next()
fmt.Println("Response body: " + blw.body.String())
}
}
...
// register
router := r.Group("/", ginBodyLogMiddleware())