Browser
Browser is a web interface that allows you to inspect existing streams and their contents. You can use it for debugging purpose as well as a built-in audit log frontend.
Adding browser to the project
Rails
Browser is now an integral part of RailsEventStore bundle and comes as a dependency when you install rails_event_store
gem. To enable it in your Rails project, add following line to routes.rb
:
Rails.application.routes.draw { mount RailsEventStore::Browser => "/res" if Rails.env.development? }
It is assumed that you have Rails Event Store configured at Rails.configuration.event_store
, in recommended location.
The RailsEventStore::Browser
is just a wrapper around RubyEventStore::Browser::App
with default options suitable for most applications. Read below the Rack, in case you need this browser outside as a standalone application or you have a different event store location.
Sinatra / Rack
Add this line to your application's Gemfile:
gem "ruby_event_store-browser"
gem "sinatra"
Add this to your config.ru
or wherever you mount your Rack apps to enable web interface. Check the appropriate environment variable (e.g. ENV['RACK_ENV']
) to only mount the browser in the appropriate environment such as development
.
There is a helper method on the Rack app to configure options event_store_locator
, host
and path
.
# e.g. Sinatra rackup file
require "ruby_event_store/browser/app"
# Example RES client you might configure
event_store = RubyEventStore::Client.new(repository: RubyEventStore::InMemoryRepository.new)
run RubyEventStore::Browser::App.for(event_store_locator: -> { event_store }, host: "http://localhost:4567")
Specify the path
option if you are not mounting the browser at the root.
# e.g. mounting the Rack app in Hanami
require "ruby_event_store/browser/app"
run RubyEventStore::Browser::App.for(
event_store_locator: -> { event_store },
host: "http://localhost:2300",
path: "/res",
),
at: "/res"
Usage in production
Rails
In a production environment you'll likely want to protect access to the browser. You can use the constraints feature of routing (in the config/routes.rb
file) to accomplish this:
Devise
Allow any authenticated User
:
Rails.application.routes.draw do
authenticate :user do
mount RailsEventStore::Browser => "/res"
end
end
Allow any authenticated User
for whom User#admin?
returns true
:
Rails.application.routes.draw do
authenticate :user, lambda { |u| u.admin? } do
mount RailsEventStore::Browser => "/res"
end
end
HTTP Basic Auth
Use HTTP Basic Auth with credentials set from RES_BROWSER_USERNAME
and RES_BROWSER_PASSWORD
environment variables:
Rails.application.routes.draw do
browser =
Rack::Builder.new do
use Rack::Auth::Basic do |username, password|
# Protect against timing attacks:
# - See https://codahale.com/a-lesson-in-timing-attacks/
# - See https://thisdata.com/blog/timing-attacks-against-string-comparison/
# - Use & (do not use &&) so that it doesn't short circuit.
# - Use digests to stop length information leaking (see also ActiveSupport::SecurityUtils.variable_size_secure_compare)
ActiveSupport::SecurityUtils.secure_compare(
::Digest::SHA256.hexdigest(username),
::Digest::SHA256.hexdigest(ENV["RES_BROWSER_USERNAME"]),
) &
ActiveSupport::SecurityUtils.secure_compare(
::Digest::SHA256.hexdigest(password),
::Digest::SHA256.hexdigest(ENV["RES_BROWSER_PASSWORD"]),
)
end
map "/" do
run RailsEventStore::Browser
end
end
mount browser => "/res"
end
Sinatra
You can use Rack-based middleware such as HTTP Basic Auth (as illustrated in the Rails example above) to control access to the browser Rack app.
Related Streams
Often you have streams, which are closely related to each other. To ease debugging, you may want to create custom links between streams. For example, if you are viewing stream Ordering::Order$123
, you may want to link to stream Payments::Transaction$456
.
You can do that by passing related streams query object to browser configuration. Related streams query object can be anything, which reponds to call(stream_name)
and always return an array (i.e. it can be simple lambda).
Related streams will be displayed in stream view, at the bottom.
Example usage:
class RelatedStreamsQuery
def call(stream_name)
prefix, suffix = stream_name.split("$")
if prefix == "Ordering::Order"
transaction_id = # some way to fetch transaction id for that order
return ["Payments::Transaction$#{transaction_id}"]
end
return []
end
end
RubyEventStore::Browser::App.for(related_streams_query: RelatedStreamsQuery.new)