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: 1_234_567_890) do
event_store.with_metadata(correlation_id: 987_654_321) { event_store.publish(MyEvent.new(data: { foo: "bar" })) }
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.