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.
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 private def set_thread_data Thread.current[:uuid] = request.uuid end end
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 end def make_request circuit.run do RestClient::Request.execute( method: method, url: url, payload: params, headers: headers, read_timeout: CircuitConstants[handler][:read_timeout], open_timeout: CircuitConstants[handler][:open_timeout] ) end end private def circuit Circuitbox.circuit(handler, CircuitConstants[handler]) end def headers @headers_with_request_id ||= begin return @headers unless @headers.is_a?(Hash) @headers['X-Request-Id'] = Thread.current[:uuid] @headers end end end
All modern web frameworks will respect this header and use it to set the request level UUID. In Rails, this is handled by the
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.