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 +7 -0
- data/CHANGELOG.md +128 -0
- data/MIT-LICENSE +20 -0
- data/README.md +272 -0
- data/lib/flu-rails/action_controller_extender.rb +89 -0
- data/lib/flu-rails/active_record_extender.rb +125 -0
- data/lib/flu-rails/configuration.rb +20 -0
- data/lib/flu-rails/core_ext.rb +30 -0
- data/lib/flu-rails/dummy/in_memory_event_publisher.rb +40 -0
- data/lib/flu-rails/event.rb +106 -0
- data/lib/flu-rails/event_factory.rb +117 -0
- data/lib/flu-rails/event_publisher.rb +51 -0
- data/lib/flu-rails/queue_repository.rb +40 -0
- data/lib/flu-rails/railtie.rb +14 -0
- data/lib/flu-rails/util.rb +71 -0
- data/lib/flu-rails/version.rb +3 -0
- data/lib/flu-rails.rb +88 -0
- metadata +192 -0
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
|
+
[](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
|