Mortar

Interceptors

Interceptors are a powerful tool that enables you to log, monitor, mutate, redirect and sometimes even fail a request or response. The beauty of interceptors is, that to the user everything looks as a regular Client.

With Client Interceptors you can:

  • Dump request and/or response to a Log
  • Alter your requests before they sent and/or responses before they returned.
  • Add Tracing Information
  • Collect metrics about your remote calls.

Mortar comes with some useful client Interceptors both for gRPC and HTTP, they are mentioned here.

HTTP Client Interceptor

Mortar introduces a way to add Interceptors to a default http.Client.

If we look at http.Client struct we will find a RoundTripper there.

// Transport specifies the mechanism by which individual
// HTTP requests are made.
// If nil, DefaultTransport is used.
type RoundTripper interface {
    // RoundTrip executes a single HTTP transaction, returning
    // a Response for the provided Request.
    // ...
    RoundTrip(*Request) (*Response, error)
}

Mortar defines HTTPHandler and HTTPClientInterceptor type aliases in mortar/interfaces/http/client/interfaces.go.

They mimic their gRPC counterparts.

// HTTPHandler is just an alias to http.RoundTriper.RoundTrip function
type HTTPHandler func(*http.Request) (*http.Response, error)

// HTTPClientInterceptor is a user defined function that can alter a request before it's sent
// and/or alter a response before it's returned to the caller
type HTTPClientInterceptor func(*http.Request, HTTPHandler) (*http.Response, error)

You can see that HTTPHandler signature is very similar to RoundTrip(*Request) (*Response, error)

That’s everything we need to enable HTTP Client interceptors.

Dump

When we create an HTTP Request we use different Structs and Functions, but we don’t set Content-Length Header for example. Dumping Utilities allow you to see what is actually sent and what is actually received. Mortar comes with an HTTP Client Interceptor that dumps(logs) the actual request and response.

func DumpRESTClientInterceptor(deps dumpHTTPDeps) client.HTTPClientInterceptor {
 return func(req *http.Request, handler client.HTTPHandler) (*http.Response, error) {
  reqBody, err := httputil.DumpRequestOut(req, true)
  deps.Logger.WithError(err).Debug(req.Context(), "Request:\n%s\n", reqBody)
  res, err := handler(req)
  if err == nil {
   resBody, dumpErr := httputil.DumpResponse(res, true)
   deps.Logger.WithError(dumpErr).Debug(req.Context(), "Response:\n%s\n", resBody)
  }
  return res, err
 }
}

GRPC Client Interceptor

GRPC Interceptors are part of the gRPC package already. Besides mentioning it Mortar has nothing to add.

Registering

Registering your Interceptors is very similar to everything else in Mortar, using Uber-Fx option. Once created you need to add them to their respectful group.

  • HTTP Client Interceptors group is called restClientInterceptors and referenced by RESTClientInterceptors defined here.

    // TracerRESTClientInterceptorFxOption adds REST trace client interceptor to the graph
    func TracerRESTClientInterceptorFxOption() fx.Option {
        return fx.Provide(
            fx.Annotated{
                Group:  groups.RESTClientInterceptors,
                Target: trace.TracerRESTClientInterceptor,
            })
    }
    
  • GRPC Unary Client Interceptors group is called grpcUnaryClientInterceptors and referenced by GRPCUnaryClientInterceptors defined here.

    // TracerGRPCClientInterceptorFxOption adds grpc trace client interceptor to the graph
    func TracerGRPCClientInterceptorFxOption() fx.Option {
        return fx.Provide(
            fx.Annotated{
                Group:  groups.GRPCUnaryClientInterceptors,
                Target: trace.TracerGRPCClientInterceptor,
            })
    }
    

You can look here to see how the above two are used.