From 9efeb7612ef60b33184864877b7f822ee1072046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Mon, 30 Apr 2018 09:26:55 +0200 Subject: [PATCH 01/16] Add silence_warnings helper for RSpec --- ruby_event_store/spec/support/rspec_defaults.rb | 3 +++ ruby_event_store/spec/support/silence_warnings.rb | 8 ++++++++ 2 files changed, 11 insertions(+) create mode 100644 ruby_event_store/spec/support/silence_warnings.rb diff --git a/ruby_event_store/spec/support/rspec_defaults.rb b/ruby_event_store/spec/support/rspec_defaults.rb index f5f30996e5..ffd9d2693d 100644 --- a/ruby_event_store/spec/support/rspec_defaults.rb +++ b/ruby_event_store/spec/support/rspec_defaults.rb @@ -1,4 +1,7 @@ +require_relative './silence_warnings' + RSpec.configure do |config| + config.include SilenceWarnings config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end diff --git a/ruby_event_store/spec/support/silence_warnings.rb b/ruby_event_store/spec/support/silence_warnings.rb new file mode 100644 index 0000000000..f1715ba4b0 --- /dev/null +++ b/ruby_event_store/spec/support/silence_warnings.rb @@ -0,0 +1,8 @@ +module SilenceWarnings + def silence_warnings + old_verbose, $VERBOSE = $VERBOSE, nil + yield + ensure + $VERBOSE = old_verbose + end +end From 06a7785a2c951c11fee5f68bf9c08824ac711ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Mon, 30 Apr 2018 09:37:34 +0200 Subject: [PATCH 02/16] Allow to dynamically set events' metadata with with_metadata Instead of using metadata_proc when initializing RubyEventStore::Client one can now use with_context instance method to dynamically set the event metadata for all events published inside a block, e.g. ``` event_store.with_metadata(request_ip: '127.0.0.1', correlation_id: '1234567890') do event_store.publish(event) end ``` `with_metadata` calls can be nested but the passed metadata will not be merged with one from the upper level. This allows to clear the metadata set in the upper level by calling `with_metadata(nil)` - in this case only default timestamp will be added to published events metadata --- .../lib/ruby_event_store/client.rb | 25 ++++- ruby_event_store/spec/client_spec.rb | 93 +++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/ruby_event_store/lib/ruby_event_store/client.rb b/ruby_event_store/lib/ruby_event_store/client.rb index 965520216c..a56efb5af9 100644 --- a/ruby_event_store/lib/ruby_event_store/client.rb +++ b/ruby_event_store/lib/ruby_event_store/client.rb @@ -171,6 +171,16 @@ def within(&block) Within.new(block, event_broker) end + def with_metadata(metadata, &block) + previous_metadata = self.metadata + begin + self.metadata = metadata + block.call if block_given? + ensure + self.metadata = previous_metadata + end + end + private def serialized_events(events) @@ -198,13 +208,26 @@ def normalize_to_array(events) end def enrich_event_metadata(event) - event.metadata[:timestamp] ||= clock.() if metadata_proc md = metadata_proc.call || {} md.each{|k,v| event.metadata[k]=(v) } end + if metadata + metadata.each { |key, value| event.metadata[key] = value } + end + event.metadata[:timestamp] ||= clock.call end attr_reader :repository, :mapper, :event_broker, :clock, :metadata_proc, :page_size + + protected + + def metadata + Thread.current[:ruby_event_store] + end + + def metadata=(value) + Thread.current[:ruby_event_store] = value + end end end diff --git a/ruby_event_store/spec/client_spec.rb b/ruby_event_store/spec/client_spec.rb index 5ea0788c06..4e3bd5d9ce 100644 --- a/ruby_event_store/spec/client_spec.rb +++ b/ruby_event_store/spec/client_spec.rb @@ -135,6 +135,99 @@ module RubyEventStore expect(published.first.metadata[:timestamp]).to be_a Time end + specify 'published event metadata will be enriched by metadata provided in with_metadata when executed inside a block' do + client = RubyEventStore::Client.new(repository: InMemoryRepository.new) + event = TestEvent.new + client.with_metadata(request_ip: '127.0.0.1') do + client.publish_event(event) + end + published = client.read_all_streams_forward + expect(published.size).to eq(1) + expect(published.first.metadata[:request_ip]).to eq('127.0.0.1') + expect(published.first.metadata[:timestamp]).to be_a Time + end + + specify 'published event metadata will not be enriched by metadata provided in with_metadata when published outside a block' do + client = RubyEventStore::Client.new(repository: InMemoryRepository.new) + event = TestEvent.new + client.with_metadata(request_ip: '127.0.0.1') + client.publish_event(event) + published = client.read_all_streams_forward + expect(published.size).to eq(1) + expect(published.first.metadata[:request_ip]).to be_nil + expect(published.first.metadata[:timestamp]).to be_a Time + end + + specify 'published event metadata will be enriched by nested metadata provided in with_metadata' do + client = RubyEventStore::Client.new(repository: InMemoryRepository.new) + client.with_metadata(request_ip: '127.0.0.1') do + client.publish_event(TestEvent.new) + client.with_metadata(request_ip: '1.2.3.4', nested: true) do + client.publish_event(TestEvent.new) + end + client.publish_event(TestEvent.new) + end + published = client.read_all_streams_forward + expect(published.size).to eq(3) + expect(published[0].metadata.keys).to match_array([:timestamp, :request_ip]) + expect(published[0].metadata[:request_ip]).to eq('127.0.0.1') + expect(published[0].metadata[:timestamp]).to be_a Time + expect(published[1].metadata.keys).to match_array([:timestamp, :request_ip, :nested]) + expect(published[1].metadata[:request_ip]).to eq('1.2.3.4') + expect(published[1].metadata[:nested]).to eq true + expect(published[1].metadata[:timestamp]).to be_a Time + expect(published[2].metadata.keys).to match_array([:timestamp, :request_ip]) + expect(published[2].metadata[:request_ip]).to eq('127.0.0.1') + expect(published[2].metadata[:timestamp]).to be_a Time + end + + specify 'with_metadata can be cleared by using nil argument' do + client = RubyEventStore::Client.new(repository: InMemoryRepository.new) + client.with_metadata(request_ip: '127.0.0.1') do + client.publish_event(TestEvent.new) + client.with_metadata(nil) do + client.publish_event(TestEvent.new) + end + client.publish_event(TestEvent.new) + end + published = client.read_all_streams_forward + expect(published.size).to eq(3) + expect(published[0].metadata.keys).to match_array([:timestamp, :request_ip]) + expect(published[0].metadata[:request_ip]).to eq('127.0.0.1') + expect(published[0].metadata[:timestamp]).to be_a Time + expect(published[1].metadata.keys).to match_array([:timestamp]) + expect(published[1].metadata[:timestamp]).to be_a Time + expect(published[2].metadata.keys).to match_array([:timestamp, :request_ip]) + expect(published[2].metadata[:request_ip]).to eq('127.0.0.1') + expect(published[2].metadata[:timestamp]).to be_a Time + end + + specify 'when both metadata_proc & with_metadata block are used, the event\'s metadata will be enriched first from the proc and then from with_metadata argument' do + client = RubyEventStore::Client.new(repository: InMemoryRepository.new, metadata_proc: ->{ {proc: true, request_ip: '127.0.0.1'} }) + event = TestEvent.new + client.with_metadata(request_ip: '1.2.3.4', meta: true) do + client.publish_event(event) + end + published = client.read_all_streams_forward + expect(published.size).to eq(1) + expect(published.first.metadata[:request_ip]).to eq('1.2.3.4') + expect(published.first.metadata[:proc]).to eq(true) + expect(published.first.metadata[:meta]).to eq(true) + expect(published.first.metadata[:timestamp]).to be_a Time + end + + specify 'timestamp can be overwritten by using with_metadata' do + client = RubyEventStore::Client.new(repository: InMemoryRepository.new) + event = TestEvent.new + client.with_metadata(timestamp: '2018-01-01T00:00:00Z') do + client.append_to_stream(event) + end + published = client.read_all_streams_forward + expect(published.size).to eq(1) + expect(published.first.metadata.to_h.keys).to eq([:timestamp]) + expect(published.first.metadata[:timestamp]).to eq('2018-01-01T00:00:00Z') + end + specify 'only timestamp set inn metadata when event stored in stream if metadata proc return nil' do client = RubyEventStore::Client.new(repository: InMemoryRepository.new, metadata_proc: ->{ nil }) event = TestEvent.new From b8cc2ed8b695d288de02edd14708d1b02b104b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Mon, 30 Apr 2018 09:38:25 +0200 Subject: [PATCH 03/16] Deprecate using metadata_proc --- ruby_event_store/lib/ruby_event_store/client.rb | 1 + ruby_event_store/spec/client_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ruby_event_store/lib/ruby_event_store/client.rb b/ruby_event_store/lib/ruby_event_store/client.rb index a56efb5af9..a239679bc1 100644 --- a/ruby_event_store/lib/ruby_event_store/client.rb +++ b/ruby_event_store/lib/ruby_event_store/client.rb @@ -209,6 +209,7 @@ def normalize_to_array(events) def enrich_event_metadata(event) if metadata_proc + warn "`RubyEventStore::Client#metadata_proc` has been deprecated. Use `RubyEventStore::Client#with_metadata` instead." md = metadata_proc.call || {} md.each{|k,v| event.metadata[k]=(v) } end diff --git a/ruby_event_store/spec/client_spec.rb b/ruby_event_store/spec/client_spec.rb index 4e3bd5d9ce..37df8d8763 100644 --- a/ruby_event_store/spec/client_spec.rb +++ b/ruby_event_store/spec/client_spec.rb @@ -128,7 +128,7 @@ module RubyEventStore specify 'published event metadata will be enriched by proc execution' do client = RubyEventStore::Client.new(repository: InMemoryRepository.new, metadata_proc: ->{ {request_id: '127.0.0.1'} }) event = TestEvent.new - client.publish_event(event) + silence_warnings { client.append_to_stream(event) } published = client.read_all_streams_forward expect(published.size).to eq(1) expect(published.first.metadata[:request_id]).to eq('127.0.0.1') @@ -206,7 +206,7 @@ module RubyEventStore client = RubyEventStore::Client.new(repository: InMemoryRepository.new, metadata_proc: ->{ {proc: true, request_ip: '127.0.0.1'} }) event = TestEvent.new client.with_metadata(request_ip: '1.2.3.4', meta: true) do - client.publish_event(event) + silence_warnings { client.append_to_stream(event) } end published = client.read_all_streams_forward expect(published.size).to eq(1) @@ -231,7 +231,7 @@ module RubyEventStore specify 'only timestamp set inn metadata when event stored in stream if metadata proc return nil' do client = RubyEventStore::Client.new(repository: InMemoryRepository.new, metadata_proc: ->{ nil }) event = TestEvent.new - client.append_to_stream(event) + silence_warnings { client.append_to_stream(event) } published = client.read_all_streams_forward expect(published.size).to eq(1) expect(published.first.metadata.to_h.keys).to eq([:timestamp]) From e015c42df1d75b2fbf6eb4340c4d4b378b1d4d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Mon, 30 Apr 2018 09:55:12 +0200 Subject: [PATCH 04/16] No need for aditional begin...ensure block here --- ruby_event_store/lib/ruby_event_store/client.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ruby_event_store/lib/ruby_event_store/client.rb b/ruby_event_store/lib/ruby_event_store/client.rb index a239679bc1..a8a9e213a6 100644 --- a/ruby_event_store/lib/ruby_event_store/client.rb +++ b/ruby_event_store/lib/ruby_event_store/client.rb @@ -173,12 +173,10 @@ def within(&block) def with_metadata(metadata, &block) previous_metadata = self.metadata - begin - self.metadata = metadata - block.call if block_given? - ensure - self.metadata = previous_metadata - end + self.metadata = metadata + block.call if block_given? + ensure + self.metadata = previous_metadata end private From 5a894c45cb981bcee87eba34ad85e975a9cbd920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Mon, 30 Apr 2018 10:21:18 +0200 Subject: [PATCH 05/16] Move metadata_proc warning to constructor and kill some mutants --- .../lib/ruby_event_store/client.rb | 4 ++-- ruby_event_store/spec/client_spec.rb | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/ruby_event_store/lib/ruby_event_store/client.rb b/ruby_event_store/lib/ruby_event_store/client.rb index a8a9e213a6..008fac52e4 100644 --- a/ruby_event_store/lib/ruby_event_store/client.rb +++ b/ruby_event_store/lib/ruby_event_store/client.rb @@ -10,6 +10,7 @@ def initialize(repository:, @mapper = mapper @event_broker = event_broker @page_size = page_size + warn "`RubyEventStore::Client#metadata_proc` has been deprecated. Use `RubyEventStore::Client#with_metadata` instead." if metadata_proc @metadata_proc = metadata_proc @clock = clock end @@ -172,7 +173,7 @@ def within(&block) end def with_metadata(metadata, &block) - previous_metadata = self.metadata + previous_metadata = metadata() self.metadata = metadata block.call if block_given? ensure @@ -207,7 +208,6 @@ def normalize_to_array(events) def enrich_event_metadata(event) if metadata_proc - warn "`RubyEventStore::Client#metadata_proc` has been deprecated. Use `RubyEventStore::Client#with_metadata` instead." md = metadata_proc.call || {} md.each{|k,v| event.metadata[k]=(v) } end diff --git a/ruby_event_store/spec/client_spec.rb b/ruby_event_store/spec/client_spec.rb index 37df8d8763..4af1fa27af 100644 --- a/ruby_event_store/spec/client_spec.rb +++ b/ruby_event_store/spec/client_spec.rb @@ -5,6 +5,13 @@ module RubyEventStore RSpec.describe Client do + specify 'deprecates using metadata_proc' do + deprecation_warning = "`RubyEventStore::Client#metadata_proc` has been deprecated. Use `RubyEventStore::Client#with_metadata` instead.\n" + expect { + RubyEventStore::Client.new(repository: InMemoryRepository.new, metadata_proc: ->{ {} }) + }.to output(deprecation_warning).to_stderr + end + specify 'publish_event returns :ok when success' do client = RubyEventStore::Client.new(repository: InMemoryRepository.new) expect(client.publish_event(TestEvent.new)).to eq(:ok) @@ -126,9 +133,9 @@ module RubyEventStore end specify 'published event metadata will be enriched by proc execution' do - client = RubyEventStore::Client.new(repository: InMemoryRepository.new, metadata_proc: ->{ {request_id: '127.0.0.1'} }) + client = silence_warnings { RubyEventStore::Client.new(repository: InMemoryRepository.new, metadata_proc: ->{ {request_id: '127.0.0.1'} }) } event = TestEvent.new - silence_warnings { client.append_to_stream(event) } + client.append_to_stream(event) published = client.read_all_streams_forward expect(published.size).to eq(1) expect(published.first.metadata[:request_id]).to eq('127.0.0.1') @@ -203,10 +210,10 @@ module RubyEventStore end specify 'when both metadata_proc & with_metadata block are used, the event\'s metadata will be enriched first from the proc and then from with_metadata argument' do - client = RubyEventStore::Client.new(repository: InMemoryRepository.new, metadata_proc: ->{ {proc: true, request_ip: '127.0.0.1'} }) + client = silence_warnings { RubyEventStore::Client.new(repository: InMemoryRepository.new, metadata_proc: ->{ {proc: true, request_ip: '127.0.0.1'} }) } event = TestEvent.new client.with_metadata(request_ip: '1.2.3.4', meta: true) do - silence_warnings { client.append_to_stream(event) } + client.append_to_stream(event) end published = client.read_all_streams_forward expect(published.size).to eq(1) @@ -229,9 +236,9 @@ module RubyEventStore end specify 'only timestamp set inn metadata when event stored in stream if metadata proc return nil' do - client = RubyEventStore::Client.new(repository: InMemoryRepository.new, metadata_proc: ->{ nil }) + client = silence_warnings { RubyEventStore::Client.new(repository: InMemoryRepository.new, metadata_proc: ->{ nil }) } event = TestEvent.new - silence_warnings { client.append_to_stream(event) } + client.append_to_stream(event) published = client.read_all_streams_forward expect(published.size).to eq(1) expect(published.first.metadata.to_h.keys).to eq([:timestamp]) From 682bd2a991670218b60488000c76ca25ea18f577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Mon, 30 Apr 2018 11:03:37 +0200 Subject: [PATCH 06/16] Add some docs for with_metadata method --- .../source/docs/request_metadata.html.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/railseventstore.org/source/docs/request_metadata.html.md b/railseventstore.org/source/docs/request_metadata.html.md index bb2011e8af..02d6ef2fc4 100644 --- a/railseventstore.org/source/docs/request_metadata.html.md +++ b/railseventstore.org/source/docs/request_metadata.html.md @@ -66,3 +66,60 @@ end ``` You can read more about your possible options by reading [ActionDispatch::Request](http://api.rubyonrails.org/classes/ActionDispatch/Request.html) documentation. + +## Passing your own metadata using `with_metadata` method + +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: + +```ruby +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_all_events(RailsEventStore::GLOBAL_STREAM).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 also using the middleware & `request_metadata`, your metadata passed as `with_metadata` argument will be merged with the result of `rails_event_store.request_metadata` proc: + +```ruby +event_store.with_metadata(causation_id: 1234567890) do + event_store.publish(MyEvent.new(data: {foo: 'bar'}) +end + +my_event = event_store.read_all_events(RailsEventStore::GLOBAL_STREAM).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[:timestamp] #=> a timestamp +``` + +You can nest multiple `with_metadata` calls, in such case the inmost argument is used: + +```ruby +event_store.with_metadata(foo: 'bar') do + event_store.with_metadata(foo: 'baz') do + event_store.publish(MyEvent.new) + end +end + +my_event = event_store.read_all_events(RailsEventStore::GLOBAL_STREAM).last +my_event.metadata[:foo] #=> 'baz' +``` + +When you want to clear the metadata for some published events while having them set with `with_metadata`, you can just pass `nil` as an argument (please note that timestamp will still be included in the metadata hash): + +```ruby +event_store.with_metadata(foo: 'bar') do + event_store.with_metadata(nil) do + event_store.publish(MyEvent.new) + end +end + +my_event = event_store.read_all_events(RailsEventStore::GLOBAL_STREAM).last +my_event.metadata[:foo] #=> nil +my_event.metadata[:timestamp] #=> a timestamp +``` + From 40bf44df476ec7074f3040fc00a10677f6be1c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Mon, 30 Apr 2018 11:16:47 +0200 Subject: [PATCH 07/16] Add missing parenthesis --- railseventstore.org/source/docs/request_metadata.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/railseventstore.org/source/docs/request_metadata.html.md b/railseventstore.org/source/docs/request_metadata.html.md index 02d6ef2fc4..543040be1c 100644 --- a/railseventstore.org/source/docs/request_metadata.html.md +++ b/railseventstore.org/source/docs/request_metadata.html.md @@ -86,7 +86,7 @@ When using `with_metadata`, the `timestamp` is still added to the metadata unles ```ruby event_store.with_metadata(causation_id: 1234567890) do - event_store.publish(MyEvent.new(data: {foo: 'bar'}) + event_store.publish(MyEvent.new(data: {foo: 'bar'})) end my_event = event_store.read_all_events(RailsEventStore::GLOBAL_STREAM).last From 20371ebfde8d18e2b93ad7ab1ed39347839e5e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Mon, 30 Apr 2018 14:24:12 +0200 Subject: [PATCH 08/16] Rewrite the middleware Instead of setting thread local variable, use with_metadata method from the event_store instance taken from the app configuration. If `config.event_store` is not set, no action is performed. Custom request metadata proc is still taken from `config.x.rails_event_store.request_metadata` If that is not present, the default proc is called --- .../lib/rails_event_store/middleware.rb | 34 ++++++-- .../lib/rails_event_store/railtie.rb | 23 +----- .../spec/middleware_integration_spec.rb | 39 +++++++-- rails_event_store/spec/middleware_spec.rb | 81 +++++++++++++------ rails_event_store/spec/railtie_spec.rb | 50 ------------ .../spec/request_metadata_spec.rb | 21 ----- rails_event_store/spec/support/test_rails.rb | 33 -------- 7 files changed, 120 insertions(+), 161 deletions(-) delete mode 100644 rails_event_store/spec/railtie_spec.rb delete mode 100644 rails_event_store/spec/request_metadata_spec.rb delete mode 100644 rails_event_store/spec/support/test_rails.rb diff --git a/rails_event_store/lib/rails_event_store/middleware.rb b/rails_event_store/lib/rails_event_store/middleware.rb index 1774ba56ec..3918364f49 100644 --- a/rails_event_store/lib/rails_event_store/middleware.rb +++ b/rails_event_store/lib/rails_event_store/middleware.rb @@ -1,15 +1,37 @@ module RailsEventStore class Middleware - def initialize(app, request_metadata_proc) + def initialize(app) @app = app - @request_metadata_proc = request_metadata_proc end def call(env) - Thread.current[:rails_event_store] = @request_metadata_proc.(env) - @app.call(env) - ensure - Thread.current[:rails_event_store] = nil + if @app.config.respond_to?(:event_store) + @app.config.event_store.with_metadata(request_metadata(env)) do + @app.call(env) + end + else + @app.call(env) + end + end + + def request_metadata(env) + (metadata_proc || default_request_metadata).call(env) + end + + private + + def metadata_proc + @app.config.x.rails_event_store.request_metadata if @app.config.x.rails_event_store.request_metadata.respond_to?(:call) + end + + def default_request_metadata + ->(env) do + request = ActionDispatch::Request.new(env) + { + remote_ip: request.remote_ip, + request_id: request.uuid + } + end end end end diff --git a/rails_event_store/lib/rails_event_store/railtie.rb b/rails_event_store/lib/rails_event_store/railtie.rb index 780c3a87c7..ac0c513cd1 100644 --- a/rails_event_store/lib/rails_event_store/railtie.rb +++ b/rails_event_store/lib/rails_event_store/railtie.rb @@ -4,28 +4,7 @@ module RailsEventStore class Railtie < ::Rails::Railtie initializer 'rails_event_store.middleware' do |rails| - rails.middleware.use(::RailsEventStore::Middleware, RailsConfig.new(rails.config).request_metadata) - end - - class RailsConfig - def initialize(config) - @config = config - end - - def request_metadata - return default_request_metadata unless @config.respond_to?(:rails_event_store) - @config.rails_event_store.fetch(:request_metadata, default_request_metadata) - end - - private - def default_request_metadata - ->(env) do - request = ActionDispatch::Request.new(env) - { remote_ip: request.remote_ip, - request_id: request.uuid, - } - end - end + rails.middleware.use(::RailsEventStore::Middleware) end end end diff --git a/rails_event_store/spec/middleware_integration_spec.rb b/rails_event_store/spec/middleware_integration_spec.rb index 05fc6f66a1..f5bfc52653 100644 --- a/rails_event_store/spec/middleware_integration_spec.rb +++ b/rails_event_store/spec/middleware_integration_spec.rb @@ -1,4 +1,7 @@ require 'spec_helper' +require 'action_controller/railtie' +require 'rails_event_store/railtie' +require 'securerandom' require 'rails_event_store/middleware' require 'rack/test' require 'rack/lint' @@ -7,16 +10,40 @@ module RailsEventStore RSpec.describe Middleware do DummyEvent = Class.new(RailsEventStore::Event) - specify do - event_store = Client.new + specify 'works without event store instance' do + request = ::Rack::MockRequest.new(Middleware.new(app)) + expect {request.get('/')}.not_to raise_error + end + + specify 'sets domain events metadata for events published with global event store instance' do + app.config.event_store = event_store + app.config.x.rails_event_store = { + request_metadata: -> env { {server_name: env['SERVER_NAME']} } + } - request = ::Rack::MockRequest.new(Middleware.new( - ->(env) { event_store.publish_event(DummyEvent.new); [200, {}, ["Hello World from #{env["SERVER_NAME"]}"]] }, - ->(env) { { server_name: env['SERVER_NAME'] }})) + request = ::Rack::MockRequest.new(Middleware.new(app)) request.get('/') event_store.read_all_streams_forward.map(&:metadata).each do |metadata| - expect(metadata[:server_name]).to eq('example.org') + expect(metadata[:server_name]).to eq('example.org') + end + end + + def event_store + @event_store ||= Client.new + end + + def app + Class.new(::Rails::Application) do + def self.name + "TestRails::Application" + end + end.tap do |app| + app.config.eager_load = false + app.config.secret_key_base = SecureRandom.hex + app.initialize! + app.routes.draw { root(to: ->(env) {event_store.publish_event(DummyEvent.new); [200, {}, ['']]}) } + app.default_url_options = { host: 'example.com' } end end end diff --git a/rails_event_store/spec/middleware_spec.rb b/rails_event_store/spec/middleware_spec.rb index 8586fc6f90..bbe34717c9 100644 --- a/rails_event_store/spec/middleware_spec.rb +++ b/rails_event_store/spec/middleware_spec.rb @@ -1,42 +1,77 @@ require 'spec_helper' +require 'action_controller/railtie' +require 'rails_event_store/railtie' +require 'securerandom' require 'rails_event_store/middleware' require 'rack/lint' module RailsEventStore RSpec.describe Middleware do - specify 'lint' do - request = ::Rack::MockRequest.new(::Rack::Lint.new(Middleware.new( - ->(env) { [200, {}, ['Hello World']] }, - ->(env) { { kaka: 'dudu' } } ))) + specify 'calls app within with_metadata block when app has configured the event store instance' do + app.config.event_store = event_store = Client.new + expect(app).to receive(:call).with(dummy_env) + middleware = Middleware.new(app) + expect(event_store).to receive(:with_metadata).with(request_id: 'dummy_id', remote_ip: 'dummy_ip').and_call_original + middleware.call(dummy_env) + end + + specify 'just calls the app when app has not configured the event store instance' do + expect(app).to receive(:call).with(dummy_env) + middleware = Middleware.new(app) + middleware.call(dummy_env) + end + + specify 'use config.rails_event_store.request_metadata' do + app.config.x.rails_event_store.request_metadata = kaka_dudu + middleware = Middleware.new(app) - expect { request.get('/') }.to_not raise_error + expect(middleware.request_metadata(dummy_env)).to eq({ + kaka: 'dudu' + }) end - specify do - request = ::Rack::MockRequest.new(Middleware.new( - ->(env) { [200, {}, ['Hello World']] }, - ->(env) { { kaka: 'dudu' } } )) - request.get('/') + specify 'use config.rails_event_store.request_metadata is not callable' do + app.config.x.rails_event_store.request_metadata = {} + middleware = Middleware.new(app) - expect(Thread.current[:rails_event_store]).to be_nil + expect(middleware.request_metadata(dummy_env)).to eq({ + request_id: 'dummy_id', + remote_ip: 'dummy_ip' + }) end - specify do - request = ::Rack::MockRequest.new(Middleware.new( - ->(env) { raise }, - ->(env) { { kaka: 'dudu' } } )) + specify 'use config.rails_event_store.request_metadata is not set' do + middleware = Middleware.new(app) - expect { request.get('/') }.to raise_error(RuntimeError) - expect(Thread.current[:rails_event_store]).to be_nil + expect(middleware.request_metadata(dummy_env)).to eq({ + request_id: 'dummy_id', + remote_ip: 'dummy_ip' + }) end - specify do - request = ::Rack::MockRequest.new(Middleware.new( - ->(env) { [200, {}, ['Hello World']] }, - ->(env) { raise } )) + def kaka_dudu + ->(env) { { kaka: 'dudu' } } + end + + def dummy_env + { + 'action_dispatch.request_id' => 'dummy_id', + 'action_dispatch.remote_ip' => 'dummy_ip' + } + end - expect { request.get('/') }.to raise_error(RuntimeError) - expect(Thread.current[:rails_event_store]).to be_nil + def app + @app ||= Class.new(::Rails::Application) do + def self.name + "TestRails::Application" + end + end.tap do |app| + app.config.eager_load = false + app.config.secret_key_base = SecureRandom.hex + app.initialize! + app.routes.draw { root(to: ->(env) {[200, {}, ['']]}) } + app.default_url_options = { host: 'example.com' } + end end end end diff --git a/rails_event_store/spec/railtie_spec.rb b/rails_event_store/spec/railtie_spec.rb deleted file mode 100644 index 84473f7b0c..0000000000 --- a/rails_event_store/spec/railtie_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' -require 'action_controller/railtie' -require 'rails_event_store/railtie' - -module RailsEventStore - RSpec.describe Railtie::RailsConfig do - specify 'no config, use defaults' do - rails_config = Railtie::RailsConfig.new(app_configuration) - - expect(rails_config.request_metadata.(dummy_env)) - .to(eq({ - request_id: 'dummy_id', - remote_ip: 'dummy_ip' - })) - end - - specify 'config present' do - app_configuration.rails_event_store = { request_metadata: kaka_dudu } - rails_config = Railtie::RailsConfig.new(app_configuration) - - expect(rails_config.request_metadata.(dummy_env)).to eq({ kaka: 'dudu' }) - end - - specify 'config present, no callable' do - app_configuration.rails_event_store = {} - rails_config = Railtie::RailsConfig.new(app_configuration) - - expect(rails_config.request_metadata.(dummy_env)) - .to(eq({ - request_id: 'dummy_id', - remote_ip: 'dummy_ip' - })) - end - - def app_configuration - @app_configuration ||= FakeConfiguration.new - end - - def kaka_dudu - ->(env) { { kaka: 'dudu' } } - end - - def dummy_env - { - 'action_dispatch.request_id' => 'dummy_id', - 'action_dispatch.remote_ip' => 'dummy_ip' - } - end - end -end diff --git a/rails_event_store/spec/request_metadata_spec.rb b/rails_event_store/spec/request_metadata_spec.rb deleted file mode 100644 index 2eec4a8dd4..0000000000 --- a/rails_event_store/spec/request_metadata_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' -require 'support/test_rails' - -module RailsEventStore - FoobarEvent = Class.new(RailsEventStore::Event) - UUID_REGEX = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/ - - RSpec.describe Client do - specify 'no config' do - event_store = Client.new - - TestRails.new.(->{ event_store.publish_event(FoobarEvent.new) }) - - expect(event_store.read_all_streams_forward).to_not be_empty - event_store.read_all_streams_forward.map(&:metadata).each do |metadata| - expect(metadata[:remote_ip]).to eq('127.0.0.1') - expect(metadata[:request_id]).to match(UUID_REGEX) - end - end - end -end diff --git a/rails_event_store/spec/support/test_rails.rb b/rails_event_store/spec/support/test_rails.rb deleted file mode 100644 index 706c147643..0000000000 --- a/rails_event_store/spec/support/test_rails.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'rack/test' -require 'action_controller/railtie' -require 'rails_event_store/railtie' -require 'securerandom' - -class TestRails - include Rack::Test::Methods - - attr_reader :app - - def initialize(test_config = {}) - @app = Class.new(::Rails::Application) do - def self.name - "TestRails::Application" - end - end - @test_config = test_config - end - - def call(action) - @test_config - .merge( - { eager_load: false, - secret_key_base: SecureRandom.hex - }) - .each { |k, v| app.config.send("#{k}=", v) } - app.initialize! - app.routes.draw { root(to: ->(env) { action.(); [200, {}, ['']] }) } - app.default_url_options = { host: 'example.com' } - get('/') - end -end - From 55ef497dea0213fdbe4302903953fda48b1eb957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Mon, 30 Apr 2018 14:32:12 +0200 Subject: [PATCH 09/16] Update docs [ci skip] --- railseventstore.org/source/docs/request_metadata.html.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/railseventstore.org/source/docs/request_metadata.html.md b/railseventstore.org/source/docs/request_metadata.html.md index 543040be1c..ae375af8cc 100644 --- a/railseventstore.org/source/docs/request_metadata.html.md +++ b/railseventstore.org/source/docs/request_metadata.html.md @@ -1,6 +1,10 @@ # Logging request metadata -In Rails environment, every event is enhanced with the request metadata provided by `rack` server. This can help with debugging and building an audit log from events for the future use. +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](/docs/install). ## Defaults @@ -31,7 +35,7 @@ my_event.metadata[:request_id] # unique ID 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 using `rails_event_store.request_metadata` field in your Rails configuration. +This can be configurable using `x.rails_event_store.request_metadata` field in your Rails configuration. You should set it up globally (`config/application.rb`) or locally for each environment (`config/environments/test.rb`, `config/environments/development.rb`, `config/environments/production.rb`, ...). If you don't provide your own, the default implementation will be used. From b1eea404e9fa0a500a29e16574648963c4b1939b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Mon, 30 Apr 2018 14:32:24 +0200 Subject: [PATCH 10/16] No need to pass metadata_proc anymore --- rails_event_store/lib/rails_event_store/client.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/rails_event_store/lib/rails_event_store/client.rb b/rails_event_store/lib/rails_event_store/client.rb index 5086a764d8..62a5be0e3b 100644 --- a/rails_event_store/lib/rails_event_store/client.rb +++ b/rails_event_store/lib/rails_event_store/client.rb @@ -4,13 +4,10 @@ def initialize(repository: RailsEventStoreActiveRecord::EventRepository.new, mapper: RubyEventStore::Mappers::Default.new, event_broker: EventBroker.new(dispatcher: ActiveJobDispatcher.new), page_size: PAGE_SIZE) - capture_metadata = ->{ Thread.current[:rails_event_store] } super(repository: repository, mapper: mapper, event_broker: event_broker, - page_size: page_size, - metadata_proc: capture_metadata) + page_size: page_size) end - end -end \ No newline at end of file +end From ed5eb5929c60b7ad28689a5ad532c5c7b8a6bdf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Mon, 30 Apr 2018 15:32:06 +0200 Subject: [PATCH 11/16] Use Rails.application global in middleware to retrieve the config It looks like the @app in the middleware is not an instance of `Rails::Application` so we cannot just read config from there. But we can use the Rails global and read configuration from there --- rails_event_store/lib/rails_event_store/middleware.rb | 10 +++++++--- rails_event_store/spec/middleware_spec.rb | 8 +++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/rails_event_store/lib/rails_event_store/middleware.rb b/rails_event_store/lib/rails_event_store/middleware.rb index 3918364f49..5bca9cdf82 100644 --- a/rails_event_store/lib/rails_event_store/middleware.rb +++ b/rails_event_store/lib/rails_event_store/middleware.rb @@ -5,8 +5,8 @@ def initialize(app) end def call(env) - if @app.config.respond_to?(:event_store) - @app.config.event_store.with_metadata(request_metadata(env)) do + if config.respond_to?(:event_store) + config.event_store.with_metadata(request_metadata(env)) do @app.call(env) end else @@ -19,9 +19,13 @@ def request_metadata(env) end private + + def config + Rails.application.config + end def metadata_proc - @app.config.x.rails_event_store.request_metadata if @app.config.x.rails_event_store.request_metadata.respond_to?(:call) + config.x.rails_event_store.request_metadata if config.x.rails_event_store.request_metadata.respond_to?(:call) end def default_request_metadata diff --git a/rails_event_store/spec/middleware_spec.rb b/rails_event_store/spec/middleware_spec.rb index bbe34717c9..c071d2fa42 100644 --- a/rails_event_store/spec/middleware_spec.rb +++ b/rails_event_store/spec/middleware_spec.rb @@ -8,9 +8,10 @@ module RailsEventStore RSpec.describe Middleware do specify 'calls app within with_metadata block when app has configured the event store instance' do - app.config.event_store = event_store = Client.new expect(app).to receive(:call).with(dummy_env) middleware = Middleware.new(app) + ::Rails.application.config.event_store = event_store = Client.new + ::Rails.application.config.x = ::Rails::Application::Configuration::Custom.new expect(event_store).to receive(:with_metadata).with(request_id: 'dummy_id', remote_ip: 'dummy_ip').and_call_original middleware.call(dummy_env) end @@ -22,8 +23,8 @@ module RailsEventStore end specify 'use config.rails_event_store.request_metadata' do - app.config.x.rails_event_store.request_metadata = kaka_dudu middleware = Middleware.new(app) + ::Rails.application.config.x.rails_event_store.request_metadata = kaka_dudu expect(middleware.request_metadata(dummy_env)).to eq({ kaka: 'dudu' @@ -31,8 +32,8 @@ module RailsEventStore end specify 'use config.rails_event_store.request_metadata is not callable' do - app.config.x.rails_event_store.request_metadata = {} middleware = Middleware.new(app) + ::Rails.application.config.x.rails_event_store.request_metadata = {} expect(middleware.request_metadata(dummy_env)).to eq({ request_id: 'dummy_id', @@ -42,6 +43,7 @@ module RailsEventStore specify 'use config.rails_event_store.request_metadata is not set' do middleware = Middleware.new(app) + ::Rails.application.config.x = ::Rails::Application::Configuration::Custom.new expect(middleware.request_metadata(dummy_env)).to eq({ request_id: 'dummy_id', From 1d01decccc9ee6f27aba33af078ab22a80a15a97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Mon, 30 Apr 2018 21:29:06 +0200 Subject: [PATCH 12/16] Ensure each instance of RubyEventStore::Client has its own metadata Store metadata in thread local variable that has unique name for each instance so that you can nest `RubyEventStore::Client#with_metadata` calls of different client instances without overwriting the metadata --- .../lib/ruby_event_store/client.rb | 4 ++-- ruby_event_store/spec/client_spec.rb | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/ruby_event_store/lib/ruby_event_store/client.rb b/ruby_event_store/lib/ruby_event_store/client.rb index 008fac52e4..da5890625d 100644 --- a/ruby_event_store/lib/ruby_event_store/client.rb +++ b/ruby_event_store/lib/ruby_event_store/client.rb @@ -222,11 +222,11 @@ def enrich_event_metadata(event) protected def metadata - Thread.current[:ruby_event_store] + Thread.current["ruby_event_store_#{hash}"] end def metadata=(value) - Thread.current[:ruby_event_store] = value + Thread.current["ruby_event_store_#{hash}"] = value end end end diff --git a/ruby_event_store/spec/client_spec.rb b/ruby_event_store/spec/client_spec.rb index 4af1fa27af..7a97a35c43 100644 --- a/ruby_event_store/spec/client_spec.rb +++ b/ruby_event_store/spec/client_spec.rb @@ -223,6 +223,25 @@ module RubyEventStore expect(published.first.metadata[:timestamp]).to be_a Time end + specify 'metadata is bound to the current instance and does not leak to others' do + client_a = RubyEventStore::Client.new(repository: InMemoryRepository.new) + client_b = RubyEventStore::Client.new(repository: InMemoryRepository.new) + + client_a.with_metadata(client: 'a') do + client_b.with_metadata(client: 'b') do + client_a.publish_event(TestEvent.new) + client_b.publish_event(TestEvent.new) + end + end + + published_a = client_a.read_all_streams_forward + published_b = client_b.read_all_streams_forward + expect(published_a.size).to eq(1) + expect(published_b.size).to eq(1) + expect(published_a.last.metadata[:client]).to eq('a') + expect(published_b.last.metadata[:client]).to eq('b') + end + specify 'timestamp can be overwritten by using with_metadata' do client = RubyEventStore::Client.new(repository: InMemoryRepository.new) event = TestEvent.new From 92e6198ac9f491b5c2e72d68e81620956bcc20de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Wed, 2 May 2018 10:40:52 +0200 Subject: [PATCH 13/16] Extract TestApplication for specs --- .../spec/middleware_integration_spec.rb | 14 ++------------ rails_event_store/spec/middleware_spec.rb | 15 ++------------- rails_event_store/spec/spec_helper.rb | 3 +-- .../spec/support/test_application.rb | 10 ++++++++++ 4 files changed, 15 insertions(+), 27 deletions(-) create mode 100644 rails_event_store/spec/support/test_application.rb diff --git a/rails_event_store/spec/middleware_integration_spec.rb b/rails_event_store/spec/middleware_integration_spec.rb index f5bfc52653..f83229d1d5 100644 --- a/rails_event_store/spec/middleware_integration_spec.rb +++ b/rails_event_store/spec/middleware_integration_spec.rb @@ -1,10 +1,8 @@ require 'spec_helper' -require 'action_controller/railtie' -require 'rails_event_store/railtie' -require 'securerandom' require 'rails_event_store/middleware' require 'rack/test' require 'rack/lint' +require 'support/test_application' module RailsEventStore RSpec.describe Middleware do @@ -34,16 +32,8 @@ def event_store end def app - Class.new(::Rails::Application) do - def self.name - "TestRails::Application" - end - end.tap do |app| - app.config.eager_load = false - app.config.secret_key_base = SecureRandom.hex - app.initialize! + TestApplication.tap do |app| app.routes.draw { root(to: ->(env) {event_store.publish_event(DummyEvent.new); [200, {}, ['']]}) } - app.default_url_options = { host: 'example.com' } end end end diff --git a/rails_event_store/spec/middleware_spec.rb b/rails_event_store/spec/middleware_spec.rb index c071d2fa42..cb304181ef 100644 --- a/rails_event_store/spec/middleware_spec.rb +++ b/rails_event_store/spec/middleware_spec.rb @@ -1,9 +1,6 @@ require 'spec_helper' -require 'action_controller/railtie' -require 'rails_event_store/railtie' -require 'securerandom' require 'rails_event_store/middleware' -require 'rack/lint' +require 'support/test_application' module RailsEventStore RSpec.describe Middleware do @@ -63,16 +60,8 @@ def dummy_env end def app - @app ||= Class.new(::Rails::Application) do - def self.name - "TestRails::Application" - end - end.tap do |app| - app.config.eager_load = false - app.config.secret_key_base = SecureRandom.hex - app.initialize! + TestApplication.tap do |app| app.routes.draw { root(to: ->(env) {[200, {}, ['']]}) } - app.default_url_options = { host: 'example.com' } end end end diff --git a/rails_event_store/spec/spec_helper.rb b/rails_event_store/spec/spec_helper.rb index 4d981859c1..799073985a 100644 --- a/rails_event_store/spec/spec_helper.rb +++ b/rails_event_store/spec/spec_helper.rb @@ -4,7 +4,6 @@ require 'support/mutant_timeout' require 'support/fake_configuration' - MigrationCode = File.read( File.expand_path('../../../rails_event_store_active_record/lib/rails_event_store_active_record/generators/templates/migration_template.rb', __FILE__) ) migration_version = Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("5.0.0") ? "" : "[4.2]" MigrationCode.gsub!("<%= migration_version %>", migration_version) @@ -22,4 +21,4 @@ end $verbose = ENV.has_key?('VERBOSE') ? true : false -ActiveJob::Base.logger = nil unless $verbose \ No newline at end of file +ActiveJob::Base.logger = nil unless $verbose diff --git a/rails_event_store/spec/support/test_application.rb b/rails_event_store/spec/support/test_application.rb new file mode 100644 index 0000000000..43e47e5843 --- /dev/null +++ b/rails_event_store/spec/support/test_application.rb @@ -0,0 +1,10 @@ +require 'action_controller/railtie' +require 'rails_event_store/railtie' +require 'securerandom' + +class TestApplication < Rails::Application + config.eager_load = false + config.secret_key_base = SecureRandom.hex(16) + initialize! + routes.default_url_options = { host: 'example.org' } +end From f88922f060106b925ca1e73480b554c10f6c25c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Wed, 2 May 2018 11:24:40 +0200 Subject: [PATCH 14/16] Move setting request_metadata proc to RailsEventStore::Client Simplify middleware so that it only calls the app in Client#with_request_metadata block if config.event_store is set You can now set the custom request_metadata proc in `RailsEventStore::Client`, the default proc returns the remote IP & request ID from ActionDispatch::Request created from the env --- .../lib/rails_event_store/client.rb | 21 +++++++ .../lib/rails_event_store/middleware.rb | 22 +------ rails_event_store/spec/client_spec.rb | 47 +++++++++++++++ .../spec/middleware_integration_spec.rb | 19 +++--- rails_event_store/spec/middleware_spec.rb | 59 +++++-------------- 5 files changed, 96 insertions(+), 72 deletions(-) create mode 100644 rails_event_store/spec/client_spec.rb diff --git a/rails_event_store/lib/rails_event_store/client.rb b/rails_event_store/lib/rails_event_store/client.rb index 62a5be0e3b..355db3fdc0 100644 --- a/rails_event_store/lib/rails_event_store/client.rb +++ b/rails_event_store/lib/rails_event_store/client.rb @@ -1,13 +1,34 @@ module RailsEventStore class Client < RubyEventStore::Client + attr_reader :request_metadata + def initialize(repository: RailsEventStoreActiveRecord::EventRepository.new, mapper: RubyEventStore::Mappers::Default.new, event_broker: EventBroker.new(dispatcher: ActiveJobDispatcher.new), + request_metadata: default_request_metadata, page_size: PAGE_SIZE) super(repository: repository, mapper: mapper, event_broker: event_broker, page_size: page_size) + @request_metadata = request_metadata + end + + def with_request_metadata(env, &block) + with_metadata(request_metadata.call(env)) do + block.call + end + end + + private + def default_request_metadata + ->(env) do + request = ActionDispatch::Request.new(env) + { + remote_ip: request.remote_ip, + request_id: request.uuid + } + end end end end diff --git a/rails_event_store/lib/rails_event_store/middleware.rb b/rails_event_store/lib/rails_event_store/middleware.rb index 5bca9cdf82..e43ee2dc44 100644 --- a/rails_event_store/lib/rails_event_store/middleware.rb +++ b/rails_event_store/lib/rails_event_store/middleware.rb @@ -6,7 +6,7 @@ def initialize(app) def call(env) if config.respond_to?(:event_store) - config.event_store.with_metadata(request_metadata(env)) do + config.event_store.with_request_metadata(env) do @app.call(env) end else @@ -14,28 +14,10 @@ def call(env) end end - def request_metadata(env) - (metadata_proc || default_request_metadata).call(env) - end - private - + def config Rails.application.config end - - def metadata_proc - config.x.rails_event_store.request_metadata if config.x.rails_event_store.request_metadata.respond_to?(:call) - end - - def default_request_metadata - ->(env) do - request = ActionDispatch::Request.new(env) - { - remote_ip: request.remote_ip, - request_id: request.uuid - } - end - end end end diff --git a/rails_event_store/spec/client_spec.rb b/rails_event_store/spec/client_spec.rb new file mode 100644 index 0000000000..4e2a3bb9b8 --- /dev/null +++ b/rails_event_store/spec/client_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +module RailsEventStore + RSpec.describe Client do + TestEvent = Class.new(RailsEventStore::Event) + + specify 'has default request metadata proc if no custom one provided' do + client = Client.new + expect(client.request_metadata.call({ + 'action_dispatch.request_id' => 'dummy_id', + 'action_dispatch.remote_ip' => 'dummy_ip' + })).to eq({ + remote_ip: 'dummy_ip', + request_id: 'dummy_id' + }) + end + + specify 'allows to set custom request metadata proc' do + client = Client.new( + request_metadata: -> env { {server_name: env['SERVER_NAME']} } + ) + expect(client.request_metadata.call({ + 'SERVER_NAME' => 'example.org' + })).to eq({ + server_name: 'example.org' + }) + end + + specify 'published event metadata will be enriched by metadata provided in request metadata when executed inside a with_request_metadata block' do + client = Client.new( + repository: InMemoryRepository.new, + ) + event = TestEvent.new + client.with_request_metadata( + 'action_dispatch.request_id' => 'dummy_id', + 'action_dispatch.remote_ip' => 'dummy_ip' + ) do + client.publish_event(event) + end + published = client.read_all_streams_forward + expect(published.size).to eq(1) + expect(published.first.metadata[:remote_ip]).to eq('dummy_ip') + expect(published.first.metadata[:request_id]).to eq('dummy_id') + expect(published.first.metadata[:timestamp]).to be_a Time + end + end +end diff --git a/rails_event_store/spec/middleware_integration_spec.rb b/rails_event_store/spec/middleware_integration_spec.rb index f83229d1d5..b4c90a70aa 100644 --- a/rails_event_store/spec/middleware_integration_spec.rb +++ b/rails_event_store/spec/middleware_integration_spec.rb @@ -9,28 +9,31 @@ module RailsEventStore DummyEvent = Class.new(RailsEventStore::Event) specify 'works without event store instance' do + event_store = Client.new request = ::Rack::MockRequest.new(Middleware.new(app)) - expect {request.get('/')}.not_to raise_error + request.get('/') + + event_store.read_all_streams_forward.map(&:metadata).each do |metadata| + expect(metadata.keys).to eq([:timestamp]) + expect(metadata[:timestamp]).to be_a(Time) + end end specify 'sets domain events metadata for events published with global event store instance' do - app.config.event_store = event_store - app.config.x.rails_event_store = { + event_store = Client.new( request_metadata: -> env { {server_name: env['SERVER_NAME']} } - } + ) + app.config.event_store = event_store request = ::Rack::MockRequest.new(Middleware.new(app)) request.get('/') event_store.read_all_streams_forward.map(&:metadata).each do |metadata| expect(metadata[:server_name]).to eq('example.org') + expect(metadata[:timestamp]).to be_a(Time) end end - def event_store - @event_store ||= Client.new - end - def app TestApplication.tap do |app| app.routes.draw { root(to: ->(env) {event_store.publish_event(DummyEvent.new); [200, {}, ['']]}) } diff --git a/rails_event_store/spec/middleware_spec.rb b/rails_event_store/spec/middleware_spec.rb index cb304181ef..e93037fd28 100644 --- a/rails_event_store/spec/middleware_spec.rb +++ b/rails_event_store/spec/middleware_spec.rb @@ -4,52 +4,21 @@ module RailsEventStore RSpec.describe Middleware do - specify 'calls app within with_metadata block when app has configured the event store instance' do - expect(app).to receive(:call).with(dummy_env) - middleware = Middleware.new(app) - ::Rails.application.config.event_store = event_store = Client.new - ::Rails.application.config.x = ::Rails::Application::Configuration::Custom.new - expect(event_store).to receive(:with_metadata).with(request_id: 'dummy_id', remote_ip: 'dummy_ip').and_call_original - middleware.call(dummy_env) + before do + allow(Rails.application).to receive(:config).and_return(configuration) end - - specify 'just calls the app when app has not configured the event store instance' do - expect(app).to receive(:call).with(dummy_env) + specify 'calls app within with_request_metadata block when app has configured the event store instance' do + Rails.application.config.event_store = event_store = Client.new + expect(event_store).to receive(:with_request_metadata).with(dummy_env).and_call_original + expect(app).to receive(:call).with(dummy_env).and_call_original middleware = Middleware.new(app) - middleware.call(dummy_env) + expect(middleware.call(dummy_env)).to eq([204, {}, ['']]) end - specify 'use config.rails_event_store.request_metadata' do - middleware = Middleware.new(app) - ::Rails.application.config.x.rails_event_store.request_metadata = kaka_dudu - - expect(middleware.request_metadata(dummy_env)).to eq({ - kaka: 'dudu' - }) - end - - specify 'use config.rails_event_store.request_metadata is not callable' do - middleware = Middleware.new(app) - ::Rails.application.config.x.rails_event_store.request_metadata = {} - - expect(middleware.request_metadata(dummy_env)).to eq({ - request_id: 'dummy_id', - remote_ip: 'dummy_ip' - }) - end - - specify 'use config.rails_event_store.request_metadata is not set' do + specify 'just calls the app when app has not configured the event store instance' do + expect(app).to receive(:call).with(dummy_env).and_call_original middleware = Middleware.new(app) - ::Rails.application.config.x = ::Rails::Application::Configuration::Custom.new - - expect(middleware.request_metadata(dummy_env)).to eq({ - request_id: 'dummy_id', - remote_ip: 'dummy_ip' - }) - end - - def kaka_dudu - ->(env) { { kaka: 'dudu' } } + expect(middleware.call(dummy_env)).to eq([204, {}, ['']]) end def dummy_env @@ -59,10 +28,12 @@ def dummy_env } end + def configuration + @configuration ||= FakeConfiguration.new + end + def app - TestApplication.tap do |app| - app.routes.draw { root(to: ->(env) {[200, {}, ['']]}) } - end + @app ||= -> _ { [204, {}, ['']] } end end end From 7861be5965e2d9d27cf98f7bb6e871a8dc3ccf1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Wed, 2 May 2018 11:29:24 +0200 Subject: [PATCH 15/16] Update docs [ci skip] --- .../source/docs/request_metadata.html.md | 53 +++++-------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/railseventstore.org/source/docs/request_metadata.html.md b/railseventstore.org/source/docs/request_metadata.html.md index ae375af8cc..9932109f85 100644 --- a/railseventstore.org/source/docs/request_metadata.html.md +++ b/railseventstore.org/source/docs/request_metadata.html.md @@ -35,9 +35,7 @@ my_event.metadata[:request_id] # unique ID 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 using `x.rails_event_store.request_metadata` field in your Rails configuration. - -You should set it up globally (`config/application.rb`) or locally for each environment (`config/environments/test.rb`, `config/environments/development.rb`, `config/environments/production.rb`, ...). If you don't provide your own, the default implementation will be used. +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: @@ -58,12 +56,14 @@ Bundler.require(*Rails.groups) module YourAppName class Application < Rails::Application - config.x.rails_event_store.request_metadata = -> (env) do - request = ActionDispatch::Request.new(env) - { remote_ip: request.remote_ip, - request_id: request.uuid, - } - end + 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 @@ -86,44 +86,19 @@ 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 also using the middleware & `request_metadata`, your metadata passed as `with_metadata` argument will be merged with the result of `rails_event_store.request_metadata` proc: +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: ```ruby event_store.with_metadata(causation_id: 1234567890) do - event_store.publish(MyEvent.new(data: {foo: 'bar'})) + event_store.with_metadata(correlation_id: 987654321) do + event_store.publish(MyEvent.new(data: {foo: 'bar'})) + end end my_event = event_store.read_all_events(RailsEventStore::GLOBAL_STREAM).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 ``` - -You can nest multiple `with_metadata` calls, in such case the inmost argument is used: - -```ruby -event_store.with_metadata(foo: 'bar') do - event_store.with_metadata(foo: 'baz') do - event_store.publish(MyEvent.new) - end -end - -my_event = event_store.read_all_events(RailsEventStore::GLOBAL_STREAM).last -my_event.metadata[:foo] #=> 'baz' -``` - -When you want to clear the metadata for some published events while having them set with `with_metadata`, you can just pass `nil` as an argument (please note that timestamp will still be included in the metadata hash): - -```ruby -event_store.with_metadata(foo: 'bar') do - event_store.with_metadata(nil) do - event_store.publish(MyEvent.new) - end -end - -my_event = event_store.read_all_events(RailsEventStore::GLOBAL_STREAM).last -my_event.metadata[:foo] #=> nil -my_event.metadata[:timestamp] #=> a timestamp -``` - From 30866ab23b7c1fdcb095217ba70321e13bb8148e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kosi=C5=84ski?= Date: Wed, 2 May 2018 12:46:39 +0200 Subject: [PATCH 16/16] Composable with_metadata calls When with_metadata calls are nested, merge the metadata instead of overwriting it --- .../lib/ruby_event_store/client.rb | 2 +- ruby_event_store/spec/client_spec.rb | 39 ++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/ruby_event_store/lib/ruby_event_store/client.rb b/ruby_event_store/lib/ruby_event_store/client.rb index da5890625d..30b1a43500 100644 --- a/ruby_event_store/lib/ruby_event_store/client.rb +++ b/ruby_event_store/lib/ruby_event_store/client.rb @@ -174,7 +174,7 @@ def within(&block) def with_metadata(metadata, &block) previous_metadata = metadata() - self.metadata = metadata + self.metadata = (previous_metadata || {}).merge(metadata) block.call if block_given? ensure self.metadata = previous_metadata diff --git a/ruby_event_store/spec/client_spec.rb b/ruby_event_store/spec/client_spec.rb index 7a97a35c43..9ff2a89070 100644 --- a/ruby_event_store/spec/client_spec.rb +++ b/ruby_event_store/spec/client_spec.rb @@ -171,11 +171,15 @@ module RubyEventStore client.publish_event(TestEvent.new) client.with_metadata(request_ip: '1.2.3.4', nested: true) do client.publish_event(TestEvent.new) + client.with_metadata(deeply_nested: true) do + client.publish_event(TestEvent.new) + end end client.publish_event(TestEvent.new) end + client.publish_event(TestEvent.new) published = client.read_all_streams_forward - expect(published.size).to eq(3) + expect(published.size).to eq(5) expect(published[0].metadata.keys).to match_array([:timestamp, :request_ip]) expect(published[0].metadata[:request_ip]).to eq('127.0.0.1') expect(published[0].metadata[:timestamp]).to be_a Time @@ -183,29 +187,38 @@ module RubyEventStore expect(published[1].metadata[:request_ip]).to eq('1.2.3.4') expect(published[1].metadata[:nested]).to eq true expect(published[1].metadata[:timestamp]).to be_a Time - expect(published[2].metadata.keys).to match_array([:timestamp, :request_ip]) - expect(published[2].metadata[:request_ip]).to eq('127.0.0.1') + expect(published[2].metadata.keys).to match_array([:timestamp, :request_ip, :nested, :deeply_nested]) + expect(published[2].metadata[:request_ip]).to eq('1.2.3.4') + expect(published[2].metadata[:nested]).to eq true + expect(published[2].metadata[:deeply_nested]).to eq true expect(published[2].metadata[:timestamp]).to be_a Time + expect(published[3].metadata.keys).to match_array([:timestamp, :request_ip]) + expect(published[3].metadata[:request_ip]).to eq('127.0.0.1') + expect(published[3].metadata[:timestamp]).to be_a Time + expect(published[4].metadata.keys).to match_array([:timestamp]) + expect(published[4].metadata[:timestamp]).to be_a Time end - specify 'with_metadata can be cleared by using nil argument' do + specify 'with_metadata is merged when nested' do client = RubyEventStore::Client.new(repository: InMemoryRepository.new) - client.with_metadata(request_ip: '127.0.0.1') do + client.with_metadata(remote_ip: '127.0.0.1') do client.publish_event(TestEvent.new) - client.with_metadata(nil) do - client.publish_event(TestEvent.new) - end + client.with_metadata(remote_ip: '192.168.0.1', request_id: '1234567890') do + client.publish_event(TestEvent.new) + end client.publish_event(TestEvent.new) end published = client.read_all_streams_forward expect(published.size).to eq(3) - expect(published[0].metadata.keys).to match_array([:timestamp, :request_ip]) - expect(published[0].metadata[:request_ip]).to eq('127.0.0.1') + expect(published[0].metadata.keys).to match_array([:timestamp, :remote_ip]) + expect(published[0].metadata[:remote_ip]).to eq('127.0.0.1') expect(published[0].metadata[:timestamp]).to be_a Time - expect(published[1].metadata.keys).to match_array([:timestamp]) + expect(published[1].metadata.keys).to match_array([:timestamp, :remote_ip, :request_id]) expect(published[1].metadata[:timestamp]).to be_a Time - expect(published[2].metadata.keys).to match_array([:timestamp, :request_ip]) - expect(published[2].metadata[:request_ip]).to eq('127.0.0.1') + expect(published[1].metadata[:remote_ip]).to eq('192.168.0.1') + expect(published[1].metadata[:request_id]).to eq('1234567890') + expect(published[2].metadata.keys).to match_array([:timestamp, :remote_ip]) + expect(published[2].metadata[:remote_ip]).to eq('127.0.0.1') expect(published[2].metadata[:timestamp]).to be_a Time end