Back

Interceptors in gRPC

Many people I talk too are well aware of what middleware is. However, Interceptors seems to be less understood. Let’s dissect interceptors

by Percy Bolmér, January 12, 2021

By [Thom] on Unsplash
By [Thom] on Unsplash

Many people I talk to are well aware of what middleware is. However, Interceptors seems to be less understood. Let’s dissect interceptors and learn how to use and write them and why we should do it.

gRPC is a great technology and I’ve been replacing many of my APIs with it. I’ve found that writing gRPC is easier than the regular HTTP based APIs. I suggest you invest time in learning it.

This is part 2 of a series, however, you don’t need prior knowledge and can continue without part 1 and 1.1, but I suggest reading them .

In a regular HTTP server, we would have a middleware wrapping our handler on the server.

This middleware can be used to perform anything the server wants to do before actually serving the correct content, it can be Authentication or logging or anything.

gRPC is different, It allows Interceptors to be used in both the server and client.

This is pretty nice since it allows users or consumers of APIs to add any wanted interceptor, such as custom logging.

Before we dig deep in interceptors I believe there is something in gRPC itself we need to understand. It’s the core concept of gRPC services.

gRPC core concepts

There are two words used in gRPC services that we must understand.

Unary is what most of us are use too. You send ONE request and get ONE response. Stream is when you instead send or receive a data pipeline of protobuf messages. This means that if an gRPC service responds with a stream, the consumer can expect more than one response inside this stream. Streams are noticed by the keyword Stream, easy enough right?

In short gRPC allows four different RPC calls .

  • Unary RPC - Unary RPC is when a client can send ONE request, and get ONE response.
  • Server Streaming RPC - Server streaming RPC is when the client sends a unary request, but is returned with a stream
  • Client Streaming RPC - Client streaming RPC is when the client sends a stream request, and is returned with a unary response. The server response is sent when all messages are consumed.
  • Bidirectional Streaming RPC - Bidirectional streaming means that the client sends a stream request, and is returned with a stream response.

So how do we know what these calls looks like?

Here are some examples from a fictional User RPC service.

// Unary example, One request, One Response
rpc GetUser(UserRequest) returns (UserResponse);
// Server streaming example, Unary request, Stream response
rpc GetNewUsers(EmptyRequest) returns (stream UserResponse);
// Client streaming example, Stream request, Unary Response
rpc RegisterUsers(stream UserRequest) returns (StatusResponse);
// Bidirectional example, Stream request, Stream response
rpc FindUser(stream UserRequest) returns (stream UserResponse);

Now that we understand the messages that gRPC supports it’s easier to learn interceptors. Let’s start by going through what an interceptor is and the different kinds that exists.

An interceptor is what it sounds like, it intercept API requests before they are executed.

This can be used to log, authenticate or anything you want to happen before your API requests are handled. With HTTP APIs this is easy in Golang, you wrap your HTTP handlers with a middleware.

With gRPC it requires some more knowledge, hence the explanations before about Unary and Stream.

There are two kinds of interceptors, and two sides of the communication.

The two kinds of interceptors

  • UnaryInterceptors —Used in API calls which is One client request and one server response.
  • StreamInterceptors — Used in API calls in which a client sends a request but receives a data stream back, allowing the server to respond multiple items over time. Actually, since gRPC is bidirectional this can be used for clients to send data as well.

The two sides of interceptors

  • Client — Interceptors that are triggered on the client side
  • Server — Interceptors that are triggered on the server side

Remember that I said gRPC interceptors can be more difficult to grasp than middlewares in HTTP?

Its because of this, we actually have too know which kind of gRPC call we want to intercept.

2 + 2 = 4

Since gRPC allows interceptors in both Client and Server, and in Unary and Streamed calls. We have 4 different interceptors.

If we go to the go-grpc library to see how they handle this we can see the four different use cases. Both interceptor types are available for both Server and Client.

The codeblock below shows the definition of four kinds. All gRPC interceptor kinds available

type StreamClientInterceptor func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)

type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error

type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error

type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

Metadata

So we now know what interceptors are. It’s time to discuss what I like to use them for and why. gRPC allows custom metadata to be sent.

Metadata is a pretty simple concept of Key Value. If we look at the golang metadata specification we can see its a map[string][]string.

Metadata can be sent as header or trailer.

  • Header - should be sent before the data.
  • Trailer - should be sent after the processing is complete

Metadata allows us to add data to requests without changing the protobuf messages. This is commonly used for adding data that is related to the request but not part of it. For instance, we can add JWT tokens as authentication in the metadata of a request. This allows us to extend an API endpoint with logic without altering the actual server logic. This can be useful for authentication, ratelimiting or loggging.

Lets Go!

Enough theory! I believe we are ready to start testing it.

If you haven’t already, I suggest reading it. Using gRPC With TLS, Golang, React without a Reverse Proxy (Envoy)

If you don’t want too — well here is the repository that we will use as a base.

Its a simple server running a Ping gRPC API. We will update this Ping API with some interceptors to learn how to do them.

Start by grabbing the repository, build the web app and generate valid TLS keys by running

mkdir grpcexample-interceptors
git init
git pull https://github.com/percybolmer/grpcexample
cd cert && sudo bash certgen.sh
cd ../ui/pingpongapp && npm install && npm run build && cd ../../

Make sure everything works before keep moving on by running the program and then visiting localhost .

go run * .go

The first thing we will do is adding a PingCounter to our API. Lets pretend that this amazing Ping application is sold to costumers. And we use the same gRPC Server and Client for many customers. But this one customer we are working for today wants to count the Pings that’s been performed. This is great, we can wrap our Unary Ping function with a Fancy New Interceptor without bloating the code for our Server.

The counting will be done in the Server . This means the first interceptor we will create is a UnaryServerInterceptor If you don’t remember what that means, feel free to reread.

A UnaryServerInterceptor was defined in the go-grpc library as:

type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

Lets begin by making a new folder called interceptors, and in there a file called pingCounter.go.

mkdir interceptors
touch pingCounter.go

Open pingCounter.go , this is the place where we will write the interceptor and the package will be called interceptors . Now we are going to create a struct which holds an integer that counts the number of Ping requests. I like using structs with Interceptors as methods , it’s easy to access needed data this way. It also allows for access to databases since your struct can contain that, which can be good in authentication interceptors. You could use an global variable instead, but I tend to avoid them. I know it’s also common for developers to return a gRPC interceptor in a method. It’s another way of doing it that looks like the following gist, code from go-grpc-middleware.

The code is from go-grpc-middleware repository and shows how they instead return a function.

// UnaryClientInterceptor returns a new unary client interceptor that optionally logs the execution of external gRPC calls.
func UnaryClientInterceptor(logger *zap.Logger, opts ...Option) grpc.UnaryClientInterceptor {
	o := evaluateClientOpt(opts)
	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		fields := newClientLoggerFields(ctx, method)
		startTime := time.Now()
		err := invoker(ctx, method, req, reply, cc, opts...)
		newCtx := ctxzap.ToContext(ctx, logger.With(fields...))
		logFinalClientLine(newCtx, o, startTime, err, "finished client unary call")
		return err
	}
}

Our Interceptor is super easy, it will count the number of Ping requests since starting the API, and attach that to the metadata of the response.

The code shows a super simple UnaryServerInterceptor

// PingCounter is a struct that keeps track of how many Pings that are performed
type PingCounter struct {
	Pings int
}

// ServerCount is a gRPC UnaryServerInterceptor that will count number of API calls.
func (pc *PingCounter) ServerCount(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (response interface{}, err error) {
	// Append to PingCounts
	pc.Pings++
	// We want to extract metadata from the incomming context.
	// We dont create a new context since we dont wanna overwrite old metadata
	meta, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, errors.New("could not grab metadata from context")
	}
	// Set ping-counts into the current ping value
	meta.Set("ping-counts", fmt.Sprintf("%d", pc.Pings))
	// Metadata is sent on its own, so we need to send the header. There is also something called Trailer
	grpc.SendHeader(ctx, meta)
	// Last but super important, execute the handler so that the actualy gRPC request is also performed
	return handler(ctx, req)
}

Actually once we are inside pingCounter.go, we can take the time to also create a UnaryClientInterceptor They are defined as

Example of a UnaryClientInterceptor.

type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
// ClientPingCounter is a UnaryClientInterceptor that will count the number of API calls on the Client side
func (pc *PingCounter) ClientPingCounter(ctx context.Context, method string, req interface{}, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	pc.Pings++
	// Run regular gRPC call after
	// If you dont run the invoker, the gRPC call wont be sent to the server
	return invoker(ctx, method, req, reply, cc, opts...)
}

Now my pingCounter.go looks like the following gist. A full example of how pingCounter.go looks

package interceptors

import (
	"context"
	"errors"
	"fmt"

	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

// PingCounter is a struct that keeps track of how many Pings that are performed
type PingCounter struct {
	Pings int
}

// ServerCount is a gRPC UnaryServerInterceptor that will count number of API calls.
func (pc *PingCounter) ServerCount(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (response interface{}, err error) {
	// Append to PingCounts
	pc.Pings++
	// We want to extract metadata from the incomming context.
	// We dont create a new context since we dont wanna overwrite old metadata
	meta, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, errors.New("could not grab metadata from context")
	}
	// Set ping-counts into the current ping value
	meta.Set("ping-counts", fmt.Sprintf("%d", pc.Pings))
	// Metadata is sent on its own, so we need to send the header. There is also something called Trailer
	grpc.SendHeader(ctx, meta)
	// Last but super important, execute the handler so that the actualy gRPC request is also performed
	return handler(ctx, req)
}

// ClientPingCounter is a UnaryClientInterceptor that will count the number of API calls on the Client side
func (pc *PingCounter) ClientPingCounter(ctx context.Context, method string, req interface{}, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	pc.Pings++
	// Run regular gRPC call after
	// If you dont run the invoker, the gRPC call wont be sent to the server
	return invoker(ctx, method, req, reply, cc, opts...)
}

We will now need to apply this interceptor to the server and the client.

Open up main.go and scroll down to line 68 where you will find GenerateTLSApi This function returns a grpc.Server, lets apply the interceptor to it. Adding a interceptor is done by adding it as an argument to the grpc.NewServer(). We will look at chaining interceptors later on. As you can see, we haven’t actually changed any logic on the grpc service, only the initialization of it. This is nice if our server were some super advanced authorization service. We easily added functionality without touching any logic in server.go

We have added a PingCounter interceptor to the server

// GenerateTLSApi will load TLS certificates and key and create a grpc server with those.
func GenerateTLSApi(pemPath, keyPath string) (*grpc.Server, error) {
	cred, err := credentials.NewServerTLSFromFile(pemPath, keyPath)
	if err != nil {
		return nil, err
	}

	// Add PingCounter from the interceptors package
	pc := interceptors.PingCounter{}
	s := grpc.NewServer(
		grpc.Creds(cred),
		// Here we add the ServerCount interceptor
		grpc.UnaryInterceptor(pc.ServerCount),
	)
	return s, nil
}

Lets also open up client/main.go We will use the client to make sure everything works before we move on into also adding the new features to the react application. We will want to wrap our gRPC connection with our ClientPingCounter to see how many pings are performed by the client.

Example of how to adding an interceptor to the Client.

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"log"

	"github.com/percybolmer/grpcexample/interceptors"
	"github.com/percybolmer/grpcexample/pingpong"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/metadata"
)

func main() {
	ctx := context.Background()
	// Load our TLS certificate and use grpc/credentials to create new transport credentials
	creds := credentials.NewTLS(loadTLSCfg())
	// Create a new connection using the transport credentials
	// Create a new pingCounter object that counts the pings the client performs
	pingCounter := interceptors.PingCounter{}
	// Create a connection and add our ClientPingCounter interceptor as a UnaryInterceptor to the connection
	conn, err := grpc.DialContext(ctx, "localhost:9990", grpc.WithTransportCredentials(creds), grpc.WithUnaryInterceptor(pingCounter.ClientPingCounter))

	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
	// A new GRPC client to use
	client := pingpong.NewPingPongClient(conn)
	// Create a metadata header we can use.
	// This is needed so we can read the metadata that the response carries.
	// You can also append an Trailer if any service sends trailing metadata
	var header metadata.MD
	// Use grpc.Header to read the metadata
	_, err = client.Ping(ctx, &pingpong.PingRequest{}, grpc.Header(&header))
	if err != nil {
		log.Fatal(err)
	}
	// Get the ping counts from the Server and see how many of them that are done by this client
	pings := header.Get("ping-counts")
	if len(pings) != 0 {
		fmt.Printf("This client has performed %d out of %s pings\n", pingCounter.Pings, pings[0])
	}
}

// loadTLSCfg will load a certificate and create a tls config
func loadTLSCfg() *tls.Config {
	b, _ := ioutil.ReadFile("../cert/server.crt")
	cp := x509.NewCertPool()
	if !cp.AppendCertsFromPEM(b) {
		log.Fatal("credentials: failed to append certificates")
	}
	config := &tls.Config{
		InsecureSkipVerify: false,
		RootCAs:            cp,
	}
	return config
}

Now run the server and also run the client to test things out. You should see a message displaying how many pings your client is responsible for (always 1).

Example of running the server and testing the client
Example of running the server and testing the client

Lets also update our web application so that we can use the metadata attached to the header.

Open ui/pingpongapp/src/App.js The first thing I want to do is add the metadata that the server sends us. This can be done by listening on the ‘metadata’ event. gRPC-web has two events that are related to metadata. The header metadata can be found in the metadata event. The Trailer metadata can be found in the status event. I want to have the same output as the golang client has, so lets add a listener on metadata.

An example of how to listen on the metadata event which carries our ping-counts

// lets bind a function to change the counter based on the metadata field
request.on('metadata', function(status) {
   // pingCounts are stored in Metadata, and metadata is a key value map
   setServerPings(status['ping-counts']);
})

Great, lets also update our web application to count the number of requests it makes. First we need to clear something out.

gRPC interceptors are intercepting the actual request/responses from leaving. What we will do in our react application wont actually be a interceptor, but will provide the same functionality. Usually what you want to do in a interceptor is grab / add authorization data like a token.

If you want to send metadata to the server in grpc-web I strongly suggest doing it in the client method call.

The gRPC calls in the javascript client looks like the code snippet below. It’s super easy to wrap that and add for example a authentication-token to the metadata object.

client.functionname(request, metadata,callback)
// An example what we use
client.ping(pingRequest, metadata,function(err,response){
    var pong = response.toObject();
    setStatus(pong.ok);
});

The reason behind me saying this is because it’s A LOT easier to add metadata to your client call than intercepting it and adding the metadata in there.

I’m going to do it the way I think is easier in this tutorial, if you want to do a proper gRPC-web interceptor, there are examples. Here is an example of a real interceptor , also here for web clients, and here is the discussion about why Unary interceptors are called Stream in gRPC-web.

Feel free to implement the gRPC-web interceptor based on the links above. I’m going to use my own way of doing it, the gist below shows an example where I add a JWT token to the metadata before sending the request.

An example where I add a JWT token to metadata before sending a gRPC request.

export function GetUser(client, callback) { 
  var user = JSON.parse(localStorage.getItem('user'));
  var userRequest = new UserRequest();
  if (user !== null) { 
    userRequest.setId(user.id);
    var metadata = { "x-user-auth-token": user.token, "x-user-auth-id": user.id, }
    client.getUser(userRequest, metadata, callback);
  }
}

In this PingPong app, we only want to count the client pings. So I’ll add a new state that counts them, and increase it as I send the request. This is my full App.js afterward.

An app that shows the metadata from the server and the client count

import './App.css';
import React, {useState, useEffect } from 'react';
import { PingPongClient } from './proto/service_grpc_web_pb';
import { PingRequest } from './proto/service_pb';

 // We create a client that connects to the api
var client = new PingPongClient("https://localhost:8080");

function App() {
  // Create a const named status and a function called setStatus
  const [status, setStatus] = useState(false);
  // create a const named pingCount 
  const [pingCounter, setPingCounter] = useState(0); 
  // serverPings is a counter for how many ping the server has
  const [serverPings, setServerPings] = useState(0); 
  // sendPing is a function that will send a ping to the backend
  const sendPing = () => {
    var pingRequest = new PingRequest();
    // use the client to send our pingrequest, the function that is passed
    // as the third param is a callback. 
    var metadata;
    // I store the request since we want to listen on metadata exchanges
    var request = client.ping(pingRequest, metadata, function(err, response) {
      // serialize the response to an object 
      var pong = response.toObject();
      // set our JS clients ping counter to +1
      setPingCounter(prevPingCounter => prevPingCounter +1);
      // call setStatus to change the value of status
      setStatus(pong.ok);
    });
    // lets bind a function to change the counter based on the metadata field
    request.on('metadata', function(status) {
      // pingCounts are stored in Metadata, and metadata is a key value map
      setServerPings(status['ping-counts']);
    })
  }

  useEffect(() => {
    // Start a interval each 3 seconds which calls sendPing. 
    const interval = setInterval(() => sendPing(), 3000)
    return () => {
      // reset timer
      clearInterval(interval);
    }
  },[status]);
  
  // we will return the HTML. Since status is a bool
  // we need to + '' to convert it into a string
  return (
    <div className="App">
      <p>Status: {status + ''}</p><br/>
      <p>Requests: This client has performed {pingCounter} out of {serverPings}</p>
    </div>
  );


}

export default App;

It’s time to make sure everything work, so lets rebuild the react application after modifying App.js. And then restart the server, lets then visit the application. I’ll also try using the golang client after visiting the website to make sure the server interceptor works correctly.

This is how the website looks for me.
This is how the website looks for me.

So, we can check UnaryClientInterceptor and UnaryServerInterceptor off our to-do list.

Before we conclude this part, lets try chaining interceptors.. I’ve created a file called logger.go inside the interceptors folder. Its a super simple logger that prints to stdout that a request has been made, and for what gRPC endpoint.

This interceptor will log the paint /main.PingPong/Ping to stdout

package interceptors

import (
	"context"
	"fmt"

	"google.golang.org/grpc"
)

// LogRequest is a gRPC UnaryServerInterceptor that will log the API call to stdOut
func LogRequest(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (response interface{}, err error) {

	fmt.Printf("Request for : %s\n", info.FullMethod)
	// Last but super important, execute the handler so that the actualy gRPC request is also performed
	return handler(ctx, req)
}

We also need to update main.go to use this new logger. We will modify the GenerateTLSApi again. We will add grpc.ChainUnaryInterceptor which takes a variable amount of interceptors and executes them in order.

The new GenerateTLSApi with chaining

// GenerateTLSApi will load TLS certificates and key and create a grpc server with those.
func GenerateTLSApi(pemPath, keyPath string) (*grpc.Server, error) {
	cred, err := credentials.NewServerTLSFromFile(pemPath, keyPath)
	if err != nil {
		return nil, err
	}

	// Add PingCounter from the interceptors package
	pc := interceptors.PingCounter{}
	s := grpc.NewServer(
		grpc.Creds(cred),
		// Here we add the chaining of interceptors, they will execute in order
		grpc.ChainUnaryInterceptor(
			pc.ServerCount,
			interceptors.LogRequest,
		),
	)
	return s, nil
}

The final main.go

package main

import (
	"log"
	"net"
	"net/http"
	"time"

	"github.com/improbable-eng/grpc-web/go/grpcweb"
	"github.com/percybolmer/grpcexample/interceptors"
	pingpong "github.com/percybolmer/grpcexample/pingpong"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

func main() {

	// We Generate a TLS grpc API
	apiserver, err := GenerateTLSApi("cert/server.crt", "cert/server.key")
	if err != nil {
		log.Fatal(err)
	}
	// Start listening on a TCP Port
	lis, err := net.Listen("tcp", "127.0.0.1:9990")
	if err != nil {
		log.Fatal(err)
	}
	// We need to tell the code WHAT TO do on each request, ie. The business logic.
	// In GRPC cases, the Server is acutally just an Interface
	// So we need a struct which fulfills the server interface
	// see server.go
	s := &Server{}
	// Register the API server as a PingPong Server
	// The register function is a generated piece by protoc.
	pingpong.RegisterPingPongServer(apiserver, s)
	// Start serving in a goroutine to not block
	go func() {
		log.Fatal(apiserver.Serve(lis))
	}()
	// Wrap the GRPC Server in grpc-web and also host the UI
	grpcWebServer := grpcweb.WrapServer(apiserver)
	// Lets put the wrapped grpc server in our multiplexer struct so
	// it can reach the grpc server in its handler
	multiplex := grpcMultiplexer{
		grpcWebServer,
	}

	// We need a http router
	r := http.NewServeMux()
	// Load the static webpage with a http fileserver
	webapp := http.FileServer(http.Dir("ui/pingpongapp/build"))
	// Host the Web Application at /, and wrap it in the GRPC Multiplexer
	// This allows grpc requests to transfer over HTTP1. then be
	// routed by the multiplexer
	r.Handle("/", multiplex.Handler(webapp))
	// Create a HTTP server and bind the router to it, and set wanted address
	srv := &http.Server{
		Handler:      r,
		Addr:         "localhost:8080",
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
	}
	// Serve the webapp over TLS
	log.Fatal(srv.ListenAndServeTLS("cert/server.crt", "cert/server.key"))

}

// GenerateTLSApi will load TLS certificates and key and create a grpc server with those.
func GenerateTLSApi(pemPath, keyPath string) (*grpc.Server, error) {
	cred, err := credentials.NewServerTLSFromFile(pemPath, keyPath)
	if err != nil {
		return nil, err
	}

	// Add PingCounter from the interceptors package
	pc := interceptors.PingCounter{}
	s := grpc.NewServer(
		grpc.Creds(cred),
		// Here we add the chaining of interceptors, they will execute in order
		grpc.ChainUnaryInterceptor(
			pc.ServerCount,
			interceptors.LogRequest,
		),
	)
	return s, nil
}

type grpcMultiplexer struct {
	*grpcweb.WrappedGrpcServer
}

// Handler is used to route requests to either grpc or to regular http
func (m *grpcMultiplexer) Handler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if m.IsGrpcWebRequest(r) {
			m.ServeHTTP(w, r)
			return
		}
		next.ServeHTTP(w, r)
	})
}

After running the server and then using the golang client we should see an output in the terminal like this

We can see the logger is working.
We can see the logger is working.

This is great, we now have an gRPC API with Unary interceptors and are using the metadata. I recommend checking out gRPC-ecosystems . They try to create plug and playable interceptors that anyone can use. The full code can be found at github

The next part of the series can be found here, in which we discuss streaming data.

If you enjoyed my writing, please support future articles by buying me an Coffee

Sign up for my Awesome newsletter