flu-rails 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 95ccc91aa5396a70d42d46300b5d7fc48321a3c3d3bea32e77d88b0b4a542e35
4
+ data.tar.gz: e71784f540b50b412469fb10af164b30942d29190fe2a9e9d7b0725a41b713bd
5
+ SHA512:
6
+ metadata.gz: e85087d058f0ce9e3cb32c7e7a3dc97471f158b9c973f491a66ccdb33f47f84c78cc78a364a16b3345dd0f036e57a1c686495c57ae2eb60e7decdb153c2a0e9f
7
+ data.tar.gz: a15ad5ed11e2c3a2a3cade60e643b4b21b5583196d3c52429b2ae09fdec8445f7ab282f013899218add3c95af4b9a655331fe8a24b9565ab408d969cc8e5fffb
data/CHANGELOG.md ADDED
@@ -0,0 +1,128 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ### [1.2.0] - 2026-07-31
9
+
10
+ * Declare `required_ruby_version` as `>= 3.2`
11
+ * Test the specs on Ruby 3.2, 3.3 and 3.4 through GitHub Actions (`.github/workflows/ci.yml`)
12
+ * Declare `logger` as an explicit runtime dependency (it is required directly, and stops being a default gem in Ruby 3.5)
13
+ * Relax the exact development dependency pins (`rspec`, `sqlite3`, `byebug`) to pessimistic constraints,
14
+ and drop the redundant lower bounds on `bunny` and `rabbitmq_http_api_client`
15
+ * Drop `bundler` as a development dependency
16
+ * Add `ostruct` as a development dependency (used by the specs, becomes a bundled gem in Ruby 3.5)
17
+ * Remove the unused `Flu::Dummy::EventPublisherDummy`, superseded by `Flu::Dummy::InMemoryEventPublisher` since `0.2.0`
18
+ * Fill in the gemspec `homepage`, `description` and `metadata`, and build `spec.files` from `Dir.glob`
19
+ instead of `git ls-files` (which returned nothing outside of a git checkout, such as in the Docker image)
20
+ * Stop tracking `Gemfile.lock`, so that each supported Ruby resolves its own dependencies
21
+
22
+ ### [1.1.0] - 2026-07-30
23
+
24
+ * Upgrade dependencies
25
+ * `bunny`: `~> 3.1`
26
+ * `bundler`: `>= 2.6.9`
27
+ * `sqlite3`: `2.9.5`
28
+ * `byebug`: `13.0.0`
29
+ * Remove `activesupport` runtime dependency (no longer used directly)
30
+ * Replace deprecated `ActiveSupport::Configurable` with plain `attr_accessor` in `Configuration` (removed in Rails 8.2)
31
+
32
+ ### [1.0.8] - 2026-04-08
33
+
34
+ * Upgrade dependencies:
35
+ * `ruby`: `>= 3`
36
+ * `bunny`: `~> 2.23`
37
+ * `activesupport`: `>= 7.0.0`
38
+ * `bundler`: `>= 2.6.9`
39
+ * `sqlite3`: `2.9.2`
40
+ * `byebug`: `13.0.0`
41
+ * Upgrade Docker base image to `ruby:3.4.9-slim-trixie`
42
+
43
+ ### [1.0.7] - 2023-04-04
44
+
45
+ * Upgrade constraints on dependencies:
46
+ * `rabbitmq_http_api_client`: `2.2`
47
+ * `rails` : `>= 7`
48
+
49
+ ### [1.0.6] - 2022-11-18
50
+
51
+ * Make sure the deep camelization when serializing an event has no side-effet
52
+
53
+ ### [1.0.5] - 2022-11-02
54
+
55
+ * Support for requests without any user agent
56
+
57
+ ### [1.0.4] - 2022-11-02
58
+
59
+ * Allow the emitter to be overriden in `track_requests` and `track_entity_changes`
60
+
61
+ ### [1.0.3] - 2022-03-30
62
+
63
+ * Update `Gemfile.lock` to avoid wrong CVE detections. The version of Rails should always be specified by the parent project. This change has no functional impact.
64
+
65
+ ### [1.0.2] - 2021-11-06
66
+
67
+ * Breaking change : force every `port` to be an integer
68
+
69
+ ## [1.0.0] - 2021-11-05
70
+
71
+ * Add `bunny_options`
72
+ * Add `rabbitmq_manage¨ment_scheme`
73
+ * MIT License
74
+
75
+ ## [0.4.2]
76
+
77
+ * Add capability to add request metadata in the entity_change events related to the request
78
+ * Upgrade `rabbitmq_http_api_client`to `2.0`, bunny to `2.19`
79
+
80
+ ## [0.4.1]
81
+
82
+ * Use of `rabbitmq_http_api_client:1.14.0`, which supports `faraday >= 1`
83
+
84
+ ## [0.4.0]
85
+
86
+ * Drop support of Rails 5
87
+ * Upgrade dependencies: `rabbitmq_http_api_client:1.13.0`, `activesupport:>=6.0.0`, `bunny:>=2.14.4`
88
+
89
+ ## [0.3.1]
90
+
91
+ * Eager load with Zeitwerk when available
92
+
93
+ ## [0.3.0]
94
+
95
+ * Support for Rails 6+
96
+
97
+ ## [0.2.0]
98
+
99
+ * Expose `InMemoryEventPublisher` for testing purpose
100
+
101
+ ## [0.1.9]
102
+
103
+ * `publish_events!` allows to publish programmatically all the events that are stacked on an ActiveRecord
104
+
105
+ ## [0.1.8]
106
+
107
+ * Events can be published manually according to a transactionnal context
108
+
109
+ ## [0.1.7]
110
+
111
+ * Support for ActiveRecord >= 5.1
112
+
113
+ ## [0.1.6]
114
+
115
+ * Allow to use the Event and the EventPublisher in non-rails environment
116
+
117
+ ## [0.1.5]
118
+
119
+ * Allow to use the EventFactory in non-rails environment
120
+
121
+ ## [0.1.4]
122
+
123
+ * Prevent events to be published including an invalid Unicode character (such as `\u0000`)
124
+
125
+ ## [0.1.3]
126
+
127
+ * Support for polymorphic one-to-one associations
128
+ * Support for `ActionController:API`
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,272 @@
1
+ # Gem for flu-rails
2
+
3
+ [![CI](https://github.com/crepesourcing/flu-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/crepesourcing/flu-rails/actions/workflows/ci.yml)
4
+
5
+ ## Description
6
+
7
+ This project aims at seamlessly creating/emitting events from an existing Rails project (without changing its codebase) in order to execute asynchronous tasks such as event storing, real time analytics, system monitoring,...
8
+
9
+ For now, events are generated from:
10
+
11
+ * CRUD operations on Active record models
12
+ * Requests on Rails controllers actions
13
+
14
+ ## Installation
15
+
16
+ Add the gem to your project's Gemfile:
17
+
18
+ ```ruby
19
+ gem "flu-rails", git: "https://github.com/crepesourcing/flu-rails.git"
20
+ ```
21
+
22
+ Then, create an initializer into your Rails app (`config/initializers/flu-rails.rb`)
23
+
24
+ ```ruby
25
+ Flu.configure do |config|
26
+ config.development_environments = []
27
+ config.rabbitmq_host = ENV["RABBITMQ_HOST"]
28
+ config.rabbitmq_port = ENV["RABBITMQ_PORT"]
29
+ config.rabbitmq_user = ENV["RABBITMQ_USER"]
30
+ config.rabbitmq_password = ENV["RABBITMQ_PASSWORD"]
31
+ config.rabbitmq_exchange_name = ENV["RABBITMQ_EXCHANGE_NAME"]
32
+ end
33
+ ```
34
+ Each configuration is detailed below.
35
+
36
+ `flu-rails` will start automatically with `railties`.
37
+
38
+ ## Requirements
39
+
40
+ * Ruby >= 3.2 (tested against 3.2, 3.3 and 3.4)
41
+ * Tested with Rails 8 (`activerecord` and `actionpack`)
42
+ * Tested with RabbitMQ 3.5.8
43
+
44
+ ## Usage
45
+
46
+ ### Start up
47
+
48
+ If your application does not include `Railties`, `flu-rails` can be started as followed:
49
+ ```
50
+ Flu.init
51
+ Flu.start
52
+ ```
53
+
54
+ `flu-rails` startup waits until its RabbitMQ exchange is connected.
55
+
56
+ ### Track changes on an ActiveRecord model
57
+
58
+ All subclasses of `ActiveRecord::Base` that call `track_entity_changes` are "tracked". _E.g._:
59
+
60
+ ```ruby
61
+ class Car < ActiveRecord::Base
62
+ track_entity_changes
63
+ end
64
+ ```
65
+
66
+ `track_entity_changes` has several options:
67
+
68
+ | Option | Default Value | Type | Required? | Description | Example |
69
+ | ---- | ----- | ------ | ----- | ------ | ----- |
70
+ | `user_metadata` | `{}`| Hash of lambdas | Optional | This hash can define two keys: `:create` and `:update`. Each value is a lambda that returns a hash. | `{create: lambda {{other_id: id}}}` |
71
+ | `ignored_model_changes` | `[]`| Array of strings | Optional | Same as the global parameter `default_ignored_model_changes` for a dedicated class. This option does not override `default_ignored_model_changes`. | `[:rsa_key, :salt]` |
72
+ | `emitter` | Emitter from configuration | Lambda that returns a string | Optional | This lambda is executed in each controller action to override the configuration's `emitter`. The result of this lambda cannot contain a dot ("`.`"). | `lambda { "overriden_emitter" }` |
73
+
74
+ For instance:
75
+
76
+ ```ruby
77
+ class Invoice < ActiveRecord::Base
78
+ track_entity_changes user_metadata: {
79
+ create: lambda {
80
+ {
81
+ user_full_name: user.full_name,
82
+ user_birthdate: user.birthdate
83
+ }
84
+ }
85
+ }
86
+
87
+ belongs_to :user
88
+ end
89
+ ```
90
+
91
+ ### Track requests to a Rails Controller action
92
+
93
+ All subclasses of `ActionController::Base` or `ActionController::API` (Rails 5) that call `track_requests` are "tracked". _E.g._:
94
+
95
+ ```ruby
96
+ class ApplicationController < ActionController::Base
97
+ track_requests
98
+ end
99
+ ```
100
+
101
+ `track_requests` has several options:
102
+
103
+ | Option | Default Value | Type | Required? | Description | Example |
104
+ | ---- | ----- | ------ | ----- | ------ | ----- |
105
+ | `user_metadata` | `nil`| Lambda that returns a hash | Optional | This lambda is executed in each controller action to create an additional event attribute: `user_metadata`. | `lambda { {id: 4}}` |
106
+ | `entity` | `nil`| Lambda that returns a hash | Optional | This lambda is executed in each controller action to attach a hash to every `entity_change` event related to it as an attribute named: `request_metadata`. | `lambda { {path: request.path}}` |
107
+ | `emitter` | Emitter from configuration | Lambda that returns a string | Optional | This lambda is executed in each controller action to override the configuration's `emitter`. The result of this lambda cannot contain a dot ("`.`"). | `lambda { "overriden_emitter" }` |
108
+
109
+ For instance,
110
+
111
+ ```ruby
112
+ class ApplicationController < ActionController::Base
113
+ track_requests user_metadata: lambda {
114
+ {
115
+ constant_pouet: "Pouet",
116
+ user_agent: request.headers["User-Agent"]
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### Create and emit an event manually
122
+
123
+ Flu's components can be used programmatically to create and emit events manually
124
+
125
+ ```ruby
126
+ data = {
127
+ recipient_name: "Lazar",
128
+ recipient_email: "lazar@fourtytwo.net",
129
+ message: "Hi!"
130
+ }
131
+ kind = :manual
132
+ name = "send email"
133
+ event = Flu.event_factory.build_event(name, kind, data)
134
+ Flu.event_publisher.publish(event)
135
+ ```
136
+
137
+ *Warning!* When calling Flu's `publisher` programmatically in a transaction, these events won't be published according to the transaction's commit. They will be published instantly, contrary to the `ActiveRecord` events that will be published when the transaction commits.
138
+ If you want to publish events manually taking the transaction into account, please use the dedicated method `flu_add_manual_event(name: string, data: Hash)` (available on every `ActiveRecord` extended with `track_entity_changes`). For instance:
139
+ ```ruby
140
+ Invoice.transaction do
141
+ invoice = Invoice.new
142
+ invoice.save!
143
+ invoice.flu_add_manual_event("a custom name", { some_data: true }) ## kind is :manual
144
+ Invoice.new.save!
145
+ end
146
+ ```
147
+ 3 events will be published according to the following order: `entity_change.create invoice`, `manual.a custom name`, `entity_change.create_invoice`.
148
+
149
+
150
+ ## Overall configuration options
151
+
152
+ All options have a default value. However, all of them can be changed in your initializer file.
153
+
154
+ | Option | Default Value | Type | Required? | Description | Example |
155
+ | ---- | ----- | ------ | ----- | ------ | ----- |
156
+ | `development_environments` | `[]`| Array of strings | Optional | If `Rails.env` matches one of these values then no connection is attempted to the exchange (messages are created by not published to any exchange). | `["test", "development"]` |
157
+ | `rejected_user_agents` | `[]`| Array of regexp | Optional | When calling a controller action, an event can be prevented from being emitted if the request's `user_agent` matches a regular expression. This option is a list of regular expressions.| `[/[^\(]*[^\)]Chrome\//]`|
158
+ | `logger` | `Logger.new(STDOUT)`| Logger | Optional | The logger used by `flu-rails` | `Rails.logger` |
159
+ | `rabbitmq_host` | `"localhost"` | String | Required | RabbitMQ exchange's host. | `"192.168.42.42"` |
160
+ | `rabbitmq_port` | `"5672"` | Integer | Required | RabbitMQ exchange's port. | `"1234"` |
161
+ | `rabbitmq_user` | `""` | String | Required | RabbitMQ exchange's username. | `"root"` |
162
+ | `rabbitmq_password` | `""` | String | Required | RabbitMQ exchange's password. | `"pouet"` |
163
+ | `rabbitmq_exchange_name` | `"events"` | String | Required | RabbitMQ exchange's name. | `"myproject"` |
164
+ | `rabbitmq_management_scheme` | `"http"` | String | Required | RabbitMQ exchange's management scheme. This scheme is used when `flu-rails` must access metadata information about queues, messages, etc. This port is used to create/delete bindings between the queue and its exchange. | `"https"` |
165
+ | `rabbitmq_management_port` | `"15672"` | Integer | Optional | RabbitMQ exchange's management port. This port is used when `flu-rails` must access metadata information about queues, messages, etc. This port is important if you want to use an instance of `QueueRepository`. Not required for simple use cases. | `"4242"` |
166
+ | `rabbitmq_exchange_durable` | `true` | Boolean | Optional | Make the RabbitMQ's exchange durable or not. From RabbitMQ's [documentation](https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges): _"Durable exchanges survive broker restart whereas transient exchanges do not (they have to be redeclared when broker comes back online)."_ | `false` |
167
+ | `auto_connect_to_exchange` | `true`| Boolean | Optional | Thanks to `Railties`, `flu-rails` starts automatically when the Rails app boots. However, this can be useful to not connect RabbitMQ at start up. To do so, set `auto_connect_to_exchange` to `false`. | `false` |
168
+ | `default_ignored_model_changes` | `[:password, :password_confirmation, :created_at, :updated_at]` | Boolean | Optional | By default, all these attributes will be ignored from model changes when creating an event. For instance, this means that timestamp fields (`created_at` and `updated_at`) are not monitored when they change. | `[]` |
169
+ | `default_ignored_request_params` | `[:password, :password_confirmation, :controller, :action]` | Boolean | Optional | By default, all these parameters will be ignored from controller request's `params` when creating an event. | `false` |
170
+ | `application_name` | `Rails.application.class.parent_name.to_s.camelize` | String | Required | Is used as `emitter` for each event created by `flu-rails`, if not overriden by the `track_met`. | `my_app` |
171
+ | `bunny_options` | `{}` | Hash of symbols | Optional | Additional options to add when connecting the RabbitMQ broker. This overrides the existing options with the same name. | `{ verify_peer: true }` |
172
+
173
+ ## How to execute tests
174
+
175
+ From the `flu-rails` directory:
176
+
177
+ ```
178
+ $ docker build . -t flu:test
179
+ $ docker run -v `pwd`:/usr/src/app/ flu:test rspec spec
180
+ ```
181
+
182
+ ## Example of Events
183
+
184
+ ### Structure of an Active Record change
185
+
186
+ An event is created and published to the message broker every time a CRUD operation is executed on an ActiveRecord model. Events are published only once the current transaction is commited. Events are not published if a transaction rollbacks.
187
+
188
+ For example, this code will produces an event _"create country"_:
189
+
190
+ ```ruby
191
+ class Country < ActiveRecord::Base
192
+ track_entity_changes user_metadata: {
193
+ create: lambda {
194
+ {
195
+ continent_name: continent.name
196
+ }
197
+ }
198
+ }
199
+ belongs_to :continent
200
+ end
201
+
202
+ country = Country.new(name: "Belgium", short_name: "BE", continent: Continent.find(12))
203
+ country.save!
204
+ ```
205
+
206
+ The generated event looks like:
207
+
208
+ ```json
209
+ {
210
+ "meta": {
211
+ "id": "540e4f55-9afa-48ac-80d9-7b0ae3654682",
212
+ "name": "create country",
213
+ "emitter": "my_application",
214
+ "timestamp": "2017-02-16T14:13:42.509Z",
215
+ "kind": "entity_change",
216
+ "status": "new"
217
+ },
218
+ "data": {
219
+ "changes": {
220
+ "id": [null, 42],
221
+ "name": [null, "Belgium"],
222
+ "shortName": [null, "BE"],
223
+ "continentId": [null, 42]
224
+ },
225
+ "entity_id": 42,
226
+ "request_id": null,
227
+ "action_name": "create",
228
+ "entity_name": "country",
229
+ "associations": {
230
+ "continent_id": 12
231
+ },
232
+ "user_metadata": {
233
+ "continent_name": "Europe"
234
+ }
235
+ }
236
+ }
237
+ ```
238
+ (`request_id` is not `nil` if this code is executed through a controller action)
239
+
240
+ ### Structure of an Rails controller request
241
+
242
+ For instance, calling the action `destroy` of `CountryController` will emit this event:
243
+
244
+ ```json
245
+ {
246
+ "meta": {
247
+ "id": "a79d411d-7a34-4d31-870b-e09dcd7e5127",
248
+ "name": "request to destroy countries",
249
+ "emitter": "my_application",
250
+ "timestamp": "2017-02-16T14:20:03.463Z",
251
+ "kind": "request",
252
+ "status": "new"
253
+ },
254
+ "data": {
255
+ "requestId": "ec01b901-db42-4c60-9da1-e3ec96a017fc",
256
+ "controllerName": "countries",
257
+ "actionName": "destroy",
258
+ "path": "/countries",
259
+ "responseCode": 200,
260
+ "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
261
+ "duration": 0.065510717,
262
+ "params": {
263
+ "format": "json"
264
+ },
265
+ "userMetadata": {}
266
+ }
267
+ }
268
+ ```
269
+
270
+ ## Side-notes
271
+
272
+ * `assocations` node contains `belongs_to` associations only.
@@ -0,0 +1,89 @@
1
+ module Flu
2
+ class ActionControllerExtender
3
+ def self.extend_controllers(event_factory, event_publisher, logger)
4
+ all_controller_types.each do |controller_type|
5
+ controller_type.class_eval do
6
+ def self.flu_is_tracked=(is_tracked)
7
+ @flu_is_tracked = is_tracked
8
+ end
9
+
10
+ def self.flu_is_tracked
11
+ @flu_is_tracked || false
12
+ end
13
+
14
+ define_singleton_method(:track_requests) do |options = {}|
15
+ self.flu_is_tracked = true
16
+ user_metadata_lambda = options[:user_metadata]
17
+ entity_metadata_lambda = options[:entity_metadata]
18
+ ignored_request_params = options.fetch(:ignored_request_params, []).map(&:to_sym)
19
+ emitter_lambda = options[:emitter]
20
+
21
+ before_action do
22
+ define_request_id
23
+ @request_start_time = Time.zone.now
24
+ end
25
+ prepend_before_action do
26
+ define_request_entity_metadata_lambda(entity_metadata_lambda)
27
+ end
28
+ prepend_after_action do
29
+ track_requests(event_factory, event_publisher, user_metadata_lambda, ignored_request_params, emitter_lambda)
30
+ remove_request_entity_metadata_lambda
31
+ end
32
+ after_action do
33
+ remove_request_id
34
+ end
35
+
36
+ def define_request_id
37
+ request_id = SecureRandom.uuid
38
+ @flu_request_id = request_id
39
+ Flu::CoreExt.flu_tracker_request_id = request_id
40
+ end
41
+
42
+ def remove_request_id
43
+ Flu::CoreExt.flu_tracker_request_id = nil
44
+ end
45
+
46
+ def define_request_entity_metadata_lambda(entity_metadata_lambda)
47
+ Flu::CoreExt.flu_tracker_request_entity_metadata = instance_exec(&entity_metadata_lambda) if entity_metadata_lambda
48
+ end
49
+
50
+ def remove_request_entity_metadata_lambda
51
+ Flu::CoreExt.flu_tracker_request_entity_metadata = nil
52
+ end
53
+
54
+ def rejected_origin?(request)
55
+ rejected_user_agents = Regexp.union(Flu.config.rejected_user_agents)
56
+ user_agent = request.user_agent
57
+ matching_user_agents = user_agent&.match(rejected_user_agents)
58
+ !matching_user_agents.nil?
59
+ end
60
+
61
+ def track_requests(event_factory, event_publisher, user_metadata_lambda, ignored_request_params, emitter_lambda)
62
+ if rejected_origin?(request)
63
+ logger.warn "Origin user agent rejected: #{request.user_agent}"
64
+ else
65
+ tracked_request = event_factory.create_data_from_request(@flu_request_id,
66
+ params,
67
+ request,
68
+ response,
69
+ @request_start_time,
70
+ ignored_request_params)
71
+ tracked_request[:user_metadata] = instance_exec(&user_metadata_lambda) if user_metadata_lambda
72
+ tracked_request[:overriden_emitter] = instance_exec(&emitter_lambda) if emitter_lambda
73
+ event = event_factory.build_request_event(tracked_request)
74
+ event_publisher.publish(event)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def self.all_controller_types
85
+ class_names = ["ActionController::Base", "ActionController::API"]
86
+ class_names.select { |class_name| Object.const_defined?(class_name) }.map(&:constantize)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,125 @@
1
+ module Flu
2
+ class ActiveRecordExtender
3
+ def self.extend_models(event_factory, event_publisher)
4
+ ActiveRecord::Base.class_eval do
5
+ define_singleton_method(:track_entity_changes) do |options = {}|
6
+ self.flu_is_tracked = true
7
+ self.flu_user_metadata_lambdas = options.fetch(:user_metadata, {})
8
+ self.flu_ignored_model_changes = options.fetch(:ignored_model_changes, []).map(&:to_s)
9
+ self.flu_overriden_emitter_lambda = options.fetch(:emitter, nil)
10
+
11
+ after_create { flu_track_entity_change(:create, flu_changes_depending_on_active_record_version, event_factory) }
12
+ after_update { flu_track_entity_change(:update, flu_changes_depending_on_active_record_version, event_factory) }
13
+ after_destroy { flu_track_entity_change(:destroy, { "id" => [id, nil] }, event_factory) }
14
+ after_commit { flu_commit_changes(event_factory, event_publisher) }
15
+ after_rollback { flu_rollback_changes }
16
+ end
17
+
18
+ def self.flu_ignored_model_changes=(ignored_model_changes)
19
+ @flu_ignored_model_changes = ignored_model_changes
20
+ end
21
+
22
+ def self.flu_ignored_model_changes
23
+ @flu_ignored_model_changes
24
+ end
25
+
26
+ def self.flu_overriden_emitter_lambda=(overriden_emitter_lambda)
27
+ @flu_overriden_emitter_lambda = overriden_emitter_lambda
28
+ end
29
+
30
+ def self.flu_overriden_emitter_lambda
31
+ @flu_overriden_emitter_lambda
32
+ end
33
+
34
+ def self.flu_user_metadata_lambdas=(user_metadata_lambdas)
35
+ @flu_user_metadata_lambdas = user_metadata_lambdas
36
+ end
37
+
38
+ def self.flu_user_metadata_lambdas
39
+ @flu_user_metadata_lambdas
40
+ end
41
+
42
+ def self.flu_is_tracked=(is_tracked)
43
+ @flu_is_tracked = is_tracked
44
+ end
45
+
46
+ def self.flu_is_tracked
47
+ @flu_is_tracked || false
48
+ end
49
+
50
+ def self.flu_association_columns
51
+ @flu_association_columns ||= reflect_on_all_associations(:belongs_to).flat_map do |association|
52
+ column_names = [association.foreign_key]
53
+ column_names.push(association.foreign_type) if association.polymorphic?
54
+ column_names
55
+ end
56
+ end
57
+
58
+ def flu_changes_depending_on_active_record_version
59
+ respond_to?(:saved_changes) ? saved_changes : changes
60
+ end
61
+
62
+ def flu_changes
63
+ @flu_changes ||= []
64
+ end
65
+
66
+ def flu_publish_events!
67
+ run_callbacks(:commit)
68
+ end
69
+
70
+ def flu_add_manual_event(name, data)
71
+ raise "data must be a hash" if data.nil? || !data.is_a?(Hash)
72
+ flu_changes.push({
73
+ name: name,
74
+ data: data,
75
+ flu_is_a_manual_event: true
76
+ })
77
+ end
78
+
79
+ def flu_changes_as_events(event_factory)
80
+ flu_changes.select do |data|
81
+ !data[:changes].try(:empty?) || data[:flu_is_a_manual_event]
82
+ end.map do |data|
83
+ if data[:flu_is_a_manual_event]
84
+ event_factory.build_manual_event(data[:name], data[:data])
85
+ else
86
+ event_factory.build_entity_change_event(data)
87
+ end
88
+ end
89
+ end
90
+
91
+ def flu_commit_changes(event_factory, event_publisher)
92
+ flu_changes_as_events(event_factory).each do |event|
93
+ event_publisher.publish(event)
94
+ end
95
+ flu_flush_changes
96
+ end
97
+
98
+ def flu_rollback_changes
99
+ flu_flush_changes
100
+ end
101
+
102
+ def flu_flush_changes
103
+ flu_changes.clear
104
+ end
105
+
106
+ def flu_track_entity_change(action_name, changes, event_factory)
107
+ unless changes.empty?
108
+ request_id = Flu::CoreExt.flu_tracker_request_id
109
+ request_entity_metadata = Flu::CoreExt.flu_tracker_request_entity_metadata
110
+ data = event_factory.create_data_from_entity_changes(action_name,
111
+ self,
112
+ request_id,
113
+ request_entity_metadata,
114
+ changes,
115
+ self.class.flu_user_metadata_lambdas[action_name],
116
+ self.class.flu_association_columns,
117
+ self.class.flu_ignored_model_changes,
118
+ self.class.flu_overriden_emitter_lambda)
119
+ flu_changes.push(data) unless data.nil?
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,20 @@
1
+ module Flu
2
+ class Configuration
3
+ attr_accessor :development_environments,
4
+ :rejected_user_agents,
5
+ :logger,
6
+ :rabbitmq_host,
7
+ :rabbitmq_management_scheme,
8
+ :rabbitmq_management_port,
9
+ :rabbitmq_port,
10
+ :rabbitmq_user,
11
+ :rabbitmq_password,
12
+ :rabbitmq_exchange_name,
13
+ :rabbitmq_exchange_durable,
14
+ :auto_connect_to_exchange,
15
+ :default_ignored_model_changes,
16
+ :default_ignored_request_params,
17
+ :application_name,
18
+ :bunny_options
19
+ end
20
+ end