DEV Community

Cover image for Distributed request tracing in Rails

Distributed request tracing in Rails

Read, code, repeat.
・2 min read

The microservices pattern is a highly debated topic. The pros and cons are heatedly discussed over forums, blogs, podcasts, social media, and literally everywhere else. We'll skip that argument for another day. Let's dive into how we can enable better request tracing in a microservices architecture in a pure Ruby on Rails world. Distributed tracing / debugging is one of the biggest challenges in a microservice architecture.

The X-Request-ID is a standard HTTP header. The header, as defined in the blog post, is :

A unique request ID, represented by a UUID, is generated at each HTTP request received by the platform routing servers. It is added to the request which is passed to your application containers.
If the X-Request-ID header is already defined by the client, it won’t be overridden except if it doesn’t respect the following format:
20-128 alphanumerical characters and the symbols +, =, / and -.

The key point to focus here is:

If the X-Request-ID header is already defined by the client, it won’t be overridden

We will use the same header to our advantage when making calls to all our external microservices.

The ActionDispatch::Request module in rails makes the uuid method available on the request object. We can use this in our controllers:

class ApplicationController < ActionController::Base
  before_action :set_thread_data

  def set_thread_data
    Thread.current[:uuid] = request.uuid

We can then leverage this Thread context from the Proxy classes making requests to our microservices.

class ServiceProxy
  attr_reader :headers, :params, :method, :url, :handler

  def initialize(headers:, params:, method:, url:, handler:)
    @headers = headers
    @params = params
    @method = method
    @url = url
    @handler = handler

  def make_request do
        method: method, url: url, payload: params, 
        headers: headers, read_timeout: CircuitConstants[handler][:read_timeout],
        open_timeout: CircuitConstants[handler][:open_timeout]

  def circuit
    Circuitbox.circuit(handler, CircuitConstants[handler])

  def headers
    @headers_with_request_id ||= begin
      return @headers unless @headers.is_a?(Hash)
      @headers['X-Request-Id'] = Thread.current[:uuid]

All modern web frameworks will respect this header and use it to set the request level UUID. In Rails, this is handled by the ActionDispatch::RequestId middleware.

We should also set the application level tagged logging to make use of these request uuids:

# config/application.rb
config.log_tags = [ :uuid ]

After implementing the above, logs will be tagged to the request uuid and will start looking like the log snippet below:


With the above setup, all requests flowing through all the microservices will have the same request-id set, enabling easy request tracing and in-turn, all application issues, easily debuggable.

Discussion (0)