Distributed context propagation with Golang gRPC

At Hootsuite, we operate within complex software architecture, using microservices extensively. In such an intricately connected world, understanding what’s happening to process a single request is crucial. When running a microservice infrastructure, handling an external request will commonly result in the involvement and orchestration of several different microservices. If something fails in the chain, the time needed to detect the problem can increase by magnitude because you need to find the failing service(s) and the reason.

Context propagation

What makes distributed systems a challenging topic is that the ability to observe the application as a whole and understand what’s happening behind the scenes is far more complex. Context propagation is the fundamental tool that spreads information across the entire infrastructure when an action is performed. This is a generic mechanism that serves different purposes. By propagating metadata in the context, you can, for example:

  • tag logs produced by different services and re-aggregate them later
  • measure the application performance and breakdown by different services

Context propagation in golang

We extensively use golang to build microservices, and we use context propagation. The following example will present how you can propagate contextual information across the microservices boundaries in a distributed environment. Bear in mind that there is one object in golang that is passed to every function call and that has some native mechanism to cross the service boundaries, that being the context.

func doSomething(ctx context.Context, params …string) { }

The context package provides functionalities to store request-scoped values, and manage the request deadlines and cancellations. Remember that if you’re going to store something in the context it should be a value strictly related to the request you’re handling. After that, the context and all the contained values should be safely deleted from the memory. The context itself should be created in the top level of your app and should not be stored in any struct type, just passed as a first argument in the other function calls.

If you’re working on a web server application, the context object is usually provided from the networking library. For example, in the net/http it has been integrated since the go.17, with the request struct offering a r.Context() function and the gRPC stack passing the context to any endpoint implemented in the server. Working with the context enables you to store any kind of data. In our tracing case, we will use the context to store the metadata received from the request to share the information with all the involved services.