Skip to main content
Version: 1.3.1

Logging request metadata

In Rails environment, every event is enhanced with the request metadata provided by rack server as long as you configure your event store instance in config.event_store. This can help with debugging and building an audit log from events for the future use.

Setup​

In order to enhance your events with metadata, you need to setup your client as described in Installation.

Defaults​

With default configuration metadata is enhanced by:

  • :remote_ip - IP of the HTTP client which issued the request.
  • :request_id - An unique ID of the request.

This metadata is included only for published events. So creating a new event instance by hand won't add metadata to it:

event = MyEvent.new(event_data)
event.metadata # empty, unless you provided your own data called 'metadata'.

If you publish an event, the special field called metadata will get filled in with request details:

event_store.publish(MyEvent.new(data: { foo: "bar" }))

my_event = event_store.read.last
my_event.metadata[:remote_ip] # your IP
my_event.metadata[:request_id] # unique ID

Configuration​

You can configure which metadata you'd like to catch. To do so, you need to provide a lambda which takes Rack environment and returns a metadata hash/object.

This can be configurable when instantinating the RailsEventStore::Client instance with request_metadata option.

Here is an example of such configuration (in config/application.rb), replicating the default behaviour:

require File.expand_path("../boot", __FILE__)

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "rails/test_unit/railtie"

Bundler.require(*Rails.groups)

module YourAppName
class Application < Rails::Application
config.event_store =
RailsEventStore::Client.new(
request_metadata: ->(env) do
request = ActionDispatch::Request.new(env)
{ remote_ip: request.remote_ip, request_id: request.uuid }
end,
)

# ...
end
end

You can read more about your possible options by reading ActionDispatch::Request documentation.

Passing your own metadata using with_metadata​

Apart from using the middleware, you can also set your metadata with RubyEventStore::Client#with_metadata method. You can specify custom metadata that will be added to all events published inside a block:

event_store.with_metadata(remote_ip: "1.2.3.4", request_id: SecureRandom.uuid) do
event_store.publish(MyEvent.new(data: { foo: "bar" }))
end

my_event = event_store.read.last

my_event.metadata[:remote_ip] #=> '1.2.3.4'
my_event.metadata[:request_id] #=> unique ID

When using with_metadata, the timestamp is still added to the metadata unless you explicitly specify it on your own. Additionally, if you are nesting with_metadata blocks or also using the middleware & request_metadata lambda, your metadata passed as with_metadata argument will be merged with the result of rails_event_store.request_metadata proc:

event_store.with_metadata(causation_id: 1234567890) do
event_store.with_metadata(correlation_id: 987654321) do
event_store.publish(MyEvent.new(data: {foo: 'bar'}))
end
end

my_event = event_store.read.last
my_event.metadata[:remote_ip] #=> your IP from request metadata proc
my_event.metadata[:request_id #=> unique ID from request metadata proc
my_event.metadata[:causation_id] #=> 1234567890 from with_metadata argument
my_event.metadata[:correlation_id] #=> 987654321 from with_metadata argument
my_event.metadata[:timestamp] #=> a timestamp

Recording current user in request–response cycle​

One can use metadata to associate a logged-in user with domain events published in a request-response cycle of a web application. This is useful for auditing purpose.

Consider following Rails ApplicationController with a method returning currently logged-in user or nil:

class ApplicationController < ActionController::Base
def current_user
# ...
end
end

We can use with_metadata combined with an around_action to wrap an action handling incoming request. Every domain event that originated from a controller action would get user_id key in its metadata with the value taken from current_user.id or being nil:

class ApplicationController < ActionController::Base
around_action :use_request_metadata

private

def use_request_metadata(&block)
Rails.configuration.event_store.with_metadata(request_metadata, &block)
end

def request_metadata
{ user_id: current_user&.id }
end
end

This metadata would be added to domain events published in synchronous handlers as well, as they happen immediately in the same request-response cycle.

Passing metadata to asynchronous handlers​

Asynchronous event handling by design happens outside of the request-response cycle — in a different thread or completely different process. Our only source of metadata is the event we're processing in a handler.

Let's look at an example how to retain it's metadata in any domain event published in an asynchronous handler.

OrderPlaced = Class.new(RailsEventStore::Event)

module MetadataHandler
def perform(event)
event_store.with_metadata(**event.metadata.to_h) { super }
end

def event_store
Rails.configuration.event_store
end
end

class OrderHandler < ActiveJob::Base
prepend RubyEventStore::AsyncHandler
prepend MetadataHandler

def perform(event)
# ...
end
end

event_store = RailsEventStore::Client.new
event_store.subscribe(OrderHandler, to: [OrderPlaced])

In MetadataHandler we first read existing metadata from the currently processed event and then decorate any further event publications inside the handler with that metadata.