event_logger_rails 0.2.1 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7bfe805ac5781f56655df8ee5b2cd31f4e58ef9d489cea5ecaa08a2d6866ea1
4
- data.tar.gz: 5b07e0e885cd7593e1647bc267f326b1d0658df5a6dee0dacfd22a6f3203a1dc
3
+ metadata.gz: 110cf01af709d86d2e848b9d11363ca58a0ab433eb444c05b9e9044d22ed35a3
4
+ data.tar.gz: 4b338f9e6f937e16195fc6f184bbad7e376409dcf44e545c50c3cec101b58123
5
5
  SHA512:
6
- metadata.gz: 1f37082b41da102a901a8457c19b7035a7dbf4d804a2eccc09a5271988ae3eb1fa481201cdb3102564867770b2984a590f2e0094a4f19b6d17d8cb6a45fa35d6
7
- data.tar.gz: 60f84d836021a037baa968e3dd1a80c9af7378397884d422e186f1cb63353f9fb11cbde70c3bed01f3d689d5d12d59e0efb5aa7f9121c8efc0a5c5dd760e3296
6
+ metadata.gz: 46be7cf18342e29d39b60f7cc4858858b5b880f16f049d65d89f69dccc0f0add12053be5020d6856a8db059f75f7f7b86f05e7733a55cadda3b3ed83dfe1e9c5
7
+ data.tar.gz: e69a33c9979e4bea40ec4ed0c723821c574586ab6a6ac29cea0f8e181fc0c545acf763b985d69b98899c5b2336262add45055479c3411673fc87ce20f760bc6f
data/README.md CHANGED
@@ -1,67 +1,294 @@
1
- # EventLoggerRails
1
+ ## 🔌 EventLoggerRails 💾
2
2
 
3
- Rails gem that facilitates logging events for analysis.
3
+ ![Elara](elara.png?raw=true)
4
+ *Elara, the mascot for `EventLoggerRails`*
5
+
6
+ Are you tired of navigating through logs as if you're lost in the labyrinth of the Wired, searching for that elusive piece of data? Say "Hello, World!" to `EventLoggerRails`, the Rails engine transmuting your logs into enlightened gems of understanding. 💎
7
+
8
+ ### Visualize This
9
+
10
+ In a single, centralized config file, decipher the events that pulse through the veins of your business. Once set, let `EventLoggerRails` weave them into intricate patterns of JSON logs that shimmer like a digital mirage. 🎇
11
+
12
+ ### Yet, The Nexus Expands
13
+
14
+ Channel these JSON enigmas directly into analytic realms like OpenSearch. There, witness the alchemy of data taking form through real-time visualizations and analysis. 📊✨
15
+
16
+ ### Why Choose `EventLoggerRails`?
17
+
18
+ - 🚀 **Fast Setup**: Get your logging up and running in minutes, not hours!
19
+ - 🌐 **Team-Friendly Event Registry**: Simplify how your team defines and logs business-critical events.
20
+ - 📚 **Readable**: Logs in a clean, JSON-formatted structure for easy parsing and analysis.
21
+ - 🔍 **In-Depth Insight**: Elevate your business process analysis with granular, structured logging.
22
+
23
+ Don't let crucial events get lost in the digital void. Make your app's logging as unforgettable as your first journey into the Wired with `EventLoggerRails`!
24
+
25
+ ### Huh?
26
+
27
+ Ok, so Elara might be a little zealous about our project, and she seems to be stuck in a 90's anime. Don't let that dissuade you from using this engine, though.
28
+
29
+ Our no-nonsense project description: **`EventLoggerRails` is a Rails engine for emitting structured events in logs during the execution of business processes for analysis and visualization.
30
+ It allows teams to define events in a simple, centralized configuration file, and then log those events in JSON format for further processing.**
4
31
 
5
32
  ## Usage
6
33
 
7
- You can define a registry of events that your application emits via the config file (`config/event_logger_rails.yml`).
8
- The events that you define are placed in the `registered_events` structure in the config file.
34
+ You can define a registry of events your application emits via the config file (`config/event_logger_rails.yml`).
35
+ The events you define are placed in the config file under the corresponding environment. Most events belong in `shared`, though you may want to define different
36
+ events or event characteristics per environment.
9
37
 
10
- For example, to register a user signup event, first define the event as a registered event:
38
+ For example, to register a user signup event, first define the event as a registered event. You must include a `description` for the event, and you may
39
+ optionally include a `level` to use for that specific event.
11
40
 
12
41
  ```yaml
13
- registered_events:
42
+ shared:
14
43
  user:
15
44
  signup:
16
- success: 'Indicates a user signup was successful.'
17
- failure: 'Indicates a user signup was not successful.'
45
+ success:
46
+ description: 'Indicates a successful user signup.'
47
+ failure:
48
+ description: 'Indicates a user signup was not successful.'
49
+ level: 'error'
18
50
  ```
19
51
 
20
- Then, from the controller action that processes user signup's, include the `Loggable` concern and use the `log_event` method to log details about the event:
52
+ ### Logging in Controllers
53
+
54
+ Continuing this example, we'll want to log the events we registered. To do so, include the `EventLoggerRails::LoggableController` concern in the controller that
55
+ processes user signup's and call the `log_event` method to log details about the event:
21
56
 
22
57
  ```ruby
23
58
  class UsersController < ApplicationController
24
- include EventLoggerRails::Loggable
59
+ include EventLoggerRails::LoggableController
25
60
 
26
61
  def create
27
62
  user = User.new(user_params)
28
63
  if user.save
29
- log_event :info, 'user.signup.success'
64
+ log_event 'user.signup.success'
30
65
  redirect_to dashboard_path
31
66
  else
32
- log_event :error, 'user.signup.failure', errors: user.errors
67
+ log_event 'user.signup.failure', data: { errors: user.errors.full_messages }
33
68
  render :new
34
- end
69
+ end
35
70
  end
36
71
  end
37
72
  ```
38
73
 
39
74
  In this example, a possible successful signup could be structured like this:
40
75
 
41
- ```
42
- [INFO | 2021-12-27T20:57:06+00:00 | user.signup.success] {"controller"=>"Users", "action"=>"create", "method"=>"POST", "path"=>"/users", "remote_ip"=>"::1", "parameters"=>"{ "user"=>{ "email"=>"validemail@example.com", "first_name"=>"Test", "last_name"=>"User" } }"}
76
+ ```json
77
+ {
78
+ "environment": "development",
79
+ "format": "application/x-www-form-urlencoded;charset=UTF-8",
80
+ "host": "d6aeb6b0516c",
81
+ "id": "2b8f44c1-0e42-4a5f-84b8-52659990d138",
82
+ "service_name": "DummyApp",
83
+ "level": "WARN",
84
+ "method": "POST",
85
+ "parameters": {
86
+ "authenticity_token": "[FILTERED]",
87
+ "user": {
88
+ "email": "princess@leia.com",
89
+ "password": "[FILTERED]"
90
+ }
91
+ },
92
+ "path": "/users",
93
+ "remote_ip": "172.20.0.1",
94
+ "timestamp": "2023-09-30T06:47:16.938+00:00",
95
+ "event_identifier": "user.signup.success",
96
+ "event_description": "Indicates a user signup was successful.",
97
+ "email": "princess@leia.com",
98
+ "action": "create",
99
+ "controller": "Registrations"
100
+ }
43
101
  ```
44
102
 
45
103
  ...while a failed signup might look like this:
46
104
 
105
+ ```json
106
+ {
107
+ "environment": "development",
108
+ "format": "application/x-www-form-urlencoded;charset=UTF-8",
109
+ "host": "d6aeb6b0516c",
110
+ "id": "2b8f44c1-0e42-4a5f-84b8-52656690d138",
111
+ "service_name": "DummyApp",
112
+ "level": "ERROR",
113
+ "method": "POST",
114
+ "parameters": {
115
+ "authenticity_token": "[FILTERED]",
116
+ "user": {
117
+ "email": "",
118
+ "password": "[FILTERED]"
119
+ },
120
+ },
121
+ "path": "/users",
122
+ "remote_ip": "172.20.0.1",
123
+ "timestamp": "2023-09-30T06:47:16.928+00:00",
124
+ "event_identifier": "user.signup.failure",
125
+ "event_description": "Indicates a user signup was not successful.",
126
+ "errors": [
127
+ "Email can't be blank",
128
+ "Password can't be blank"
129
+ ],
130
+ "email": "princess@leia.com",
131
+ "action": "create",
132
+ "controller": "Registrations"
133
+ }
47
134
  ```
48
- [ERROR | 2021-12-27T20:57:06+00:00 | user.signup.failure] {"controller"=>"Users", "action"=>"create", "method"=>"POST", "path"=>"/users", "remote_ip"=>"::1", "parameters"=>"{ "user"=>{ "first_name"=>"Test", "last_name"=>"User" } }", "errors"=>"{ "email"=>"is missing" }"}
135
+
136
+ Note how the log entry from the previous example contains the data passed in via the optional `data` argument.
137
+
138
+ You can also provide a logger level as an optional argument if you need to specify a logger level other than the default. If you provide a logger level, it
139
+ will override the configured event level and the default logger level.
140
+
141
+ ```ruby
142
+ log_event 'user.signup.failure', level: :info, data: { errors: user.errors }
49
143
  ```
50
144
 
51
- If you fail to register an event, the logger will emit an `event_logger_rails.event.unregistered` event:
145
+ This will output an event with the corresponding severity level. You must provide a valid logger level (`:debug, :info, :warn, :error, or :unknown`).
52
146
 
147
+ ```json
148
+ {
149
+ "environment": "development",
150
+ "format": "application/x-www-form-urlencoded;charset=UTF-8",
151
+ "host": "d6aeb6b0516c",
152
+ "id": "2b8f44c1-0e42-4a5f-84b8-52656690d138",
153
+ "service_name": "DummyApp",
154
+ "level": "INFO",
155
+ "method": "POST",
156
+ "parameters": {
157
+ "authenticity_token": "[FILTERED]",
158
+ "user": {
159
+ "email": "",
160
+ "password": "[FILTERED]"
161
+ },
162
+ },
163
+ "path": "/users",
164
+ "remote_ip": "172.20.0.1",
165
+ "timestamp": "2023-09-30T06:47:16.928+00:00",
166
+ "event_identifier": "user.signup.failure",
167
+ "event_description": "Indicates a user signup was not successful.",
168
+ "errors": [
169
+ "Email can't be blank",
170
+ "Password can't be blank"
171
+ ],
172
+ "email": "princess@leia.com",
173
+ "action": "create",
174
+ "controller": "Registrations"
175
+ }
53
176
  ```
54
- [ERROR | 2021-12-27T20:57:06+00:00 | event_logger_rails.event.unregistered] {"controller"=>"Users", "action"=>"create", "method"=>"POST", "path"=>"/users", "remote_ip"=>"::1", "parameters"=>"{ "user"=>{ "first_name"=>"Test", "last_name"=>"User" } }", "message"=>"Event provided not registered: user.signup.failure"}
177
+
178
+ ### Logging in Models
179
+
180
+ You can also log events from within models by including the `EventLoggerRails::LoggableModel` concern and calling `log_event`.
181
+
182
+ ```ruby
183
+ class User < ApplicationRecord
184
+ include EventLoggerRails::LoggableModel
185
+
186
+ after_create :log_signup
187
+
188
+ private
189
+
190
+ def log_signup
191
+ log_event 'user.signup.success', data: { email: }
192
+ end
193
+ end
55
194
  ```
56
195
 
57
- If you provide an invalid log level, the logger will emit an `event_logger_rails.logger_level.invalid` event:
196
+ By default, `EventLoggerRails` will include the model name, instance ID, and whatever data is passed.
58
197
 
198
+ ```json
199
+ {
200
+ "environment": "development",
201
+ "format": "application/x-www-form-urlencoded;charset=UTF-8",
202
+ "host": "d6aeb6b0516c",
203
+ "id": "2b8f44c1-0e42-4a5f-84b8-52652332d138",
204
+ "service_name": "DummyApp",
205
+ "level": "WARN",
206
+ "method": "POST",
207
+ "parameters": {
208
+ "authenticity_token": "[FILTERED]",
209
+ "user": {
210
+ "email": "princess@leia.com",
211
+ "password": "[FILTERED]"
212
+ }
213
+ },
214
+ "path": "/users",
215
+ "remote_ip": "172.20.0.1",
216
+ "timestamp": "2023-09-30T06:47:16.817+00:00",
217
+ "event_identifier": "user.signup.success",
218
+ "event_description": "Indicates a user signup was successful.",
219
+ "email": "princess@leia.com",
220
+ "model": "User",
221
+ "instance_id": 41
222
+ }
59
223
  ```
60
- [ERROR | 2021-12-27T20:57:06+00:00 | event_logger_rails.logger_level.invalid] {"controller"=>"Users", "action"=>"create", "method"=>"POST", "path"=>"/users", "remote_ip"=>"::1", "parameters"=>"{ "user"=>{ "first_name"=>"Test", "last_name"=>"User" } }", "message"=>"Invalid logger level provided: 'foo'. Valid levels: debug, info, warn, error, fatal."}
224
+
225
+ ### Logging Everywhere Else
226
+
227
+ You can log events from anywhere inside of your application by calling `EventLoggerRails.log` directly, though you won't get the additional context
228
+ from the controller or model.
229
+
230
+ ```ruby
231
+ EventLoggerRails.log 'user.signup.success', level: :info, data: { user_id: @user.id }
232
+ ```
233
+
234
+ ### Errors
235
+
236
+ There are two expected errors which are handled by `EventLoggerRails`: an unregistered event and an invalid logger level. Both will result
237
+ in a log entry with an event corresponding to the error, and the severity level will be set to `ERROR`.
238
+
239
+ If you fail to register an event, the logger will emit an `event_logger_rails.event.unregistered` event:
240
+
241
+ ```json
242
+ {
243
+ "environment": "development",
244
+ "format": "application/x-www-form-urlencoded;charset=UTF-8",
245
+ "host": "d6aeb6b0516c",
246
+ "id": "94c5ffe9-1bd8-4e04-88a3-478958e242b0",
247
+ "service_name": "DummyApp",
248
+ "level": "ERROR",
249
+ "method": "POST",
250
+ "parameters": {
251
+ "authenticity_token": "[FILTERED]",
252
+ "user": {
253
+ "email": "",
254
+ "password": "[FILTERED]"
255
+ }
256
+ },
257
+ "path": "/users",
258
+ "remote_ip": "172.20.0.1",
259
+ "timestamp": "2023-09-30T07:03:34.993+00:00",
260
+ "event_identifier": "event_logger_rails.event.unregistered",
261
+ "event_description": "Indicates provided event was unregistered.",
262
+ "message": "Event provided not registered: foo.bar"
263
+ }
61
264
  ```
62
265
 
63
- The log entry indicates the logger level (useful for filtering results), the registered event, and more useful information from the controller and request.
64
- This makes it simple use a tool like `awk` or another monitoring solution to parse the logs for emitted events, facilitating troubleshooting and analytics.
266
+ If you provide an invalid log level, the logger will emit an `event_logger_rails.logger_level.invalid` event:
267
+
268
+ ```json
269
+ {
270
+ "environment": "development",
271
+ "format": "application/x-www-form-urlencoded;charset=UTF-8",
272
+ "host": "d6aeb6b0516c",
273
+ "id": "11541423-0008-4cc7-aef7-1e4af9a801d7",
274
+ "service_name": "DummyApp",
275
+ "level": "ERROR",
276
+ "method": "POST",
277
+ "parameters": {
278
+ "authenticity_token": "[FILTERED]",
279
+ "user": {
280
+ "email": "",
281
+ "password": "[FILTERED]"
282
+ }
283
+ },
284
+ "path": "/users",
285
+ "remote_ip": "172.20.0.1",
286
+ "timestamp": "2023-09-30T07:04:52.623+00:00",
287
+ "event_identifier": "event_logger_rails.logger_level.invalid",
288
+ "event_description": "Indicates provided level was invalid.",
289
+ "message": "Invalid logger level provided: 'foobar'. Valid levels: :debug, :info, :warn, :error, :unknown."
290
+ }
291
+ ```
65
292
 
66
293
  ## Installation
67
294
 
@@ -74,27 +301,54 @@ gem 'event_logger_rails'
74
301
  And then execute:
75
302
 
76
303
  ```bash
77
- $ bundle
304
+ bundle
78
305
  ```
79
306
 
80
307
  Or install it yourself as:
81
308
 
82
309
  ```bash
83
- $ gem install event_logger_rails
310
+ gem install event_logger_rails
84
311
  ```
85
312
 
86
313
  Run the install generator to create a config file (`config/event_logger_rails.yml`):
87
314
 
88
315
  ```bash
89
- $ bin/rails generate event_logger_rails:install
316
+ bin/rails generate event_logger_rails:install
90
317
  ```
91
318
 
92
319
  Add your events to the generated config file following the structure of the examples.
93
320
 
321
+ You can specify a default level `EventLoggerRails` will use if a level is not included in the call to the logger or configured as a default for the provided event.
322
+ This default level is set to `:warn` unless otherwise specified.
323
+
324
+ ```ruby
325
+ Rails.application.configure do |config|
326
+ config.event_logger_rails.default_level = :info
327
+ end
328
+ ```
329
+
330
+ By default, `EventLoggerRails` outputs to a separate log file (`log/event_logger_rails.#{Rails.env}.log`) from normal Rails log output, allowing
331
+ you to ingest these logs independently. If you wish to set an alternative log device to capture output, you can configure it in `config/application.rb`:
332
+
333
+ ```ruby
334
+ Rails.application.configure do |config|
335
+ config.event_logger_rails.logdev = 'path/to/log.file'
336
+ end
337
+ ```
338
+
339
+ Some platforms require logging output to be sent to $STDOUT. You can configure this as an output device easily enough.
340
+
341
+ ```ruby
342
+ Rails.application.configure do |config|
343
+ config.event_logger_rails.logdev = $stdout
344
+ end
345
+ ```
346
+
94
347
  ## Contributing
95
348
 
96
- Contributions are welcome. Feel free to open a PR.
349
+ Your inputs echo in this realm. Venture forth and materialize your thoughts through a PR.
97
350
 
98
351
  ## License
99
352
 
100
353
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
354
+
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventLoggerRails
4
+ ##
5
+ # Provides event logging with relevant controller/request data.
6
+ module LoggableController
7
+ extend ActiveSupport::Concern
8
+ include EventLoggerRails::Extensions::Loggable
9
+
10
+ def optional_data
11
+ {
12
+ action: action_name,
13
+ controller: controller_name.camelcase
14
+ }
15
+ end
16
+
17
+ private :optional_data
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventLoggerRails
4
+ ##
5
+ # Provides event logging with relevant model data.
6
+ module LoggableModel
7
+ extend ActiveSupport::Concern
8
+ include EventLoggerRails::Extensions::Loggable
9
+
10
+ def optional_data
11
+ {
12
+ model: self.class.name,
13
+ instance_id: id
14
+ }
15
+ end
16
+
17
+ private :optional_data
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventLoggerRails
4
+ ##
5
+ # Provides global state with request details
6
+ class CurrentRequest < ActiveSupport::CurrentAttributes
7
+ attribute :id, :format, :method, :parameters, :path, :remote_ip
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventLoggerRails
4
+ ##
5
+ # Processes events, sending data to logger.
6
+ class Emitter
7
+ def initialize(logdev:)
8
+ @logger = JsonLogger.new(logdev)
9
+ end
10
+
11
+ def log(event, level:, data: {})
12
+ Event.new(event).validate! do |validated_event|
13
+ message = Message.new(event: validated_event, data:)
14
+ level = level || validated_event.level || EventLoggerRails.default_level
15
+ log_message(message, level.to_sym)
16
+ end
17
+ rescue Exceptions::UnregisteredEvent, Exceptions::InvalidLoggerLevel => error
18
+ log(error.event, level: :error, data: { message: error.message })
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :logger
24
+
25
+ def log_message(message, level)
26
+ logger.send(level) { message }
27
+ rescue NoMethodError
28
+ raise Exceptions::InvalidLoggerLevel.new(logger_level: level)
29
+ end
30
+ end
31
+ end
@@ -1,7 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EventLoggerRails
4
+ ##
5
+ # Engine for plugging into Rails
4
6
  class Engine < ::Rails::Engine
5
7
  isolate_namespace EventLoggerRails
8
+
9
+ config.generators do |generator|
10
+ generator.test_framework :rspec
11
+ end
12
+
13
+ config.event_logger_rails = ActiveSupport::OrderedOptions.new
14
+ config.event_logger_rails.logdev = "log/event_logger_rails.#{Rails.env}.log"
15
+ config.event_logger_rails.default_level = :warn
16
+
17
+ initializer 'event_logger_rails.add_middleware' do |app|
18
+ app.middleware.use Middleware::CaptureRequestDetails
19
+ end
20
+
21
+ config.after_initialize do |app|
22
+ EventLoggerRails.setup do |engine|
23
+ engine.default_level = app.config.event_logger_rails.default_level
24
+ engine.logdev = app.config.event_logger_rails.logdev
25
+ engine.registered_events = Rails.application.config_for(:event_logger_rails)
26
+ engine.sensitive_fields = app.config.filter_parameters
27
+ end
28
+ end
6
29
  end
7
30
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventLoggerRails
4
+ ##
5
+ # Models an event for logging.
6
+ class Event
7
+ DEFAULT_EVENTS = {
8
+ 'event_logger_rails.logger_level.invalid' => {
9
+ description: 'Indicates provided level was invalid.',
10
+ level: :error
11
+ },
12
+ 'event_logger_rails.event.unregistered' => {
13
+ description: 'Indicates provided event was unregistered.',
14
+ level: :error
15
+ },
16
+ 'event_logger_rails.event.testing' => {
17
+ description: 'Event reserved for testing.',
18
+ level: :warn
19
+ }
20
+ }.freeze
21
+ private_constant :DEFAULT_EVENTS
22
+
23
+ attr_reader :identifier, :description, :level
24
+
25
+ def initialize(provided_identifier)
26
+ @provided_identifier = provided_identifier.to_s
27
+
28
+ if (default_event = DEFAULT_EVENTS[@provided_identifier])
29
+ default_registration = [@provided_identifier, *default_event&.values]
30
+ end
31
+
32
+ @identifier, @description, @level = default_registration || config_registration
33
+ end
34
+
35
+ def merge(...)
36
+ to_hash.merge(...)
37
+ end
38
+
39
+ def valid?
40
+ identifier.present?
41
+ end
42
+
43
+ def validate!
44
+ raise Exceptions::UnregisteredEvent.new(unregistered_event: self) unless valid?
45
+
46
+ yield(self)
47
+ end
48
+
49
+ def to_hash
50
+ {
51
+ event_identifier: identifier,
52
+ event_description: description
53
+ }
54
+ end
55
+
56
+ def to_s
57
+ identifier&.to_s || provided_identifier.to_s
58
+ end
59
+
60
+ def ==(other)
61
+ to_s == other.to_s
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :provided_identifier
67
+
68
+ def config_registration
69
+ parsed_event = provided_identifier.split('.').map(&:to_sym)
70
+ config = EventLoggerRails.registered_events.dig(*parsed_event)
71
+ case config
72
+ in { description:, level: }
73
+ [provided_identifier, description, level]
74
+ in { description: }
75
+ [provided_identifier, description, nil]
76
+ else
77
+ [nil, nil, nil]
78
+ end
79
+ end
80
+ end
81
+ end
@@ -5,11 +5,22 @@ module EventLoggerRails
5
5
  ##
6
6
  # Indicates invalid log level provided.
7
7
  class InvalidLoggerLevel < StandardError
8
- attr_reader :logger_level
8
+ attr_reader :event
9
9
 
10
10
  def initialize(logger_level:)
11
- super("Invalid logger level provided: '#{logger_level}'. Valid levels: debug, info, warn, error, fatal.")
11
+ super
12
+ @event = Event.new('event_logger_rails.logger_level.invalid')
13
+ @logger_level = logger_level
14
+ end
15
+
16
+ def message
17
+ "Invalid logger level provided: '#{logger_level.to_sym}'. " \
18
+ 'Valid levels: :debug, :info, :warn, :error, :unknown.'
12
19
  end
20
+
21
+ private
22
+
23
+ attr_reader :logger_level
13
24
  end
14
25
  end
15
26
  end
@@ -5,11 +5,21 @@ module EventLoggerRails
5
5
  ##
6
6
  # Indicates event provided not registered.
7
7
  class UnregisteredEvent < StandardError
8
- attr_reader :unregistered_event
8
+ attr_reader :event
9
9
 
10
10
  def initialize(unregistered_event:)
11
- super("Event provided not registered: #{unregistered_event}")
11
+ super()
12
+ @event = Event.new('event_logger_rails.event.unregistered')
13
+ @unregistered_event = unregistered_event
14
+ end
15
+
16
+ def message
17
+ "Event provided not registered: #{unregistered_event}"
12
18
  end
19
+
20
+ private
21
+
22
+ attr_reader :unregistered_event
13
23
  end
14
24
  end
15
25
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventLoggerRails
4
+ module Extensions
5
+ ##
6
+ # Provides event logging with relevant model data.
7
+ module Loggable
8
+ def log_event(event, **kwargs)
9
+ EventLoggerRails.log(
10
+ event,
11
+ level: kwargs[:level] || nil,
12
+ data: (kwargs[:data] || {}).merge(optional_data)
13
+ )
14
+ end
15
+
16
+ private
17
+
18
+ def optional_data
19
+ {}
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventLoggerRails
4
+ ##
5
+ # Writes log entries in JSON format
6
+ class JsonLogger < ::Logger
7
+ def initialize(...)
8
+ super(...)
9
+ @formatter = proc do |level, timestamp, _progname, message|
10
+ output = Output.new(level:, timestamp:, message:)
11
+ "#{output.to_json}\n"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventLoggerRails
4
+ ##
5
+ # Models a message sent to the logger containing event and optional data
6
+ class Message
7
+ def initialize(event:, data: {})
8
+ @event = event
9
+ @data = data
10
+ end
11
+
12
+ def to_hash
13
+ event.merge(data)
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :event, :data
19
+ end
20
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../current_request'
4
+
5
+ module EventLoggerRails
6
+ module Middleware
7
+ ##
8
+ # Middleware to capture request details and store in global state
9
+ class CaptureRequestDetails
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
15
+ def call(env)
16
+ begin
17
+ request = ActionDispatch::Request.new(env)
18
+
19
+ CurrentRequest.id = env['action_dispatch.request_id']
20
+ CurrentRequest.format = request.headers['Content-Type']
21
+ CurrentRequest.method = request.method
22
+ CurrentRequest.parameters = request.parameters.except(:controller, :action, :format)
23
+ CurrentRequest.path = request.path
24
+ CurrentRequest.remote_ip = request.remote_ip
25
+
26
+ status, headers, body = @app.call(env)
27
+ ensure
28
+ CurrentRequest.reset
29
+ end
30
+
31
+ [status, headers, body]
32
+ end
33
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventLoggerRails
4
+ ##
5
+ # Merges data from application, request, and logger message for structured output
6
+ class Output
7
+ def initialize(level:, timestamp:, message:)
8
+ @current_request = EventLoggerRails::CurrentRequest
9
+ @level = level
10
+ @timestamp = timestamp.iso8601(3)
11
+ @message = message.respond_to?(:to_hash) ? sanitizer.filter(**message) : { message: }
12
+ end
13
+
14
+ def to_json(*args)
15
+ JSON.generate(to_hash, *args)
16
+ end
17
+
18
+ def to_hash
19
+ application_data.merge(**current_request_data, **logger_data)
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :level, :timestamp, :message, :current_request
25
+
26
+ def sanitizer
27
+ @sanitizer ||= ActiveSupport::ParameterFilter.new(EventLoggerRails.sensitive_fields)
28
+ end
29
+
30
+ def application_data
31
+ {
32
+ environment: Rails.env,
33
+ host: Socket.gethostname,
34
+ service_name: Rails.application.class.module_parent_name
35
+ }
36
+ end
37
+
38
+ # rubocop:disable Metrics/AbcSize
39
+ def current_request_data
40
+ return {} if CurrentRequest.instance.attributes.blank?
41
+
42
+ {
43
+ format: current_request.format,
44
+ id: current_request.id,
45
+ method: current_request.method,
46
+ parameters: sanitizer.filter(current_request.parameters),
47
+ path: current_request.path,
48
+ remote_ip: current_request.remote_ip
49
+ }
50
+ end
51
+ # rubocop:enable Metrics/AbcSize
52
+
53
+ def logger_data
54
+ {
55
+ level:,
56
+ timestamp:,
57
+ **message
58
+ }
59
+ end
60
+ end
61
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EventLoggerRails
4
- VERSION = '0.2.1'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -2,24 +2,40 @@
2
2
 
3
3
  require 'rails'
4
4
  require 'active_support/dependencies'
5
- require 'event_logger_rails/version'
6
5
  require 'event_logger_rails/engine'
7
- require 'event_logger_rails/event_logger'
6
+ require 'event_logger_rails/current_request'
7
+ require 'event_logger_rails/event'
8
+ require 'event_logger_rails/emitter'
8
9
  require 'event_logger_rails/exceptions/invalid_logger_level'
9
10
  require 'event_logger_rails/exceptions/unregistered_event'
11
+ require 'event_logger_rails/extensions/loggable'
12
+ require 'event_logger_rails/json_logger'
13
+ require 'event_logger_rails/message'
14
+ require 'event_logger_rails/middleware/capture_request_details'
15
+ require 'event_logger_rails/output'
16
+ require 'event_logger_rails/version'
10
17
 
11
18
  ##
12
- # Namespace for UtilityClasses gem
19
+ # Namespace for EventLoggerRails gem
13
20
  module EventLoggerRails
14
- autoload :EventLogger, 'event_logger_rails/event_logger'
15
- autoload :InvalidLoggerLevel, 'event_logger_rails/exceptions/invalid_logger_level'
16
- autoload :UnregisteredEvent, 'event_logger_rails/exceptions/unregistered_event'
21
+ mattr_accessor :default_level
22
+ mattr_accessor :logdev
23
+ mattr_accessor :registered_events
24
+ mattr_accessor :sensitive_fields
25
+
26
+ def self.setup
27
+ yield self
28
+ end
29
+
30
+ def self.emitter
31
+ @emitter ||= Emitter.new(logdev:)
32
+ end
17
33
 
18
- def self.logger
19
- @logger ||= EventLogger.new
34
+ def self.log(...)
35
+ emitter.log(...)
20
36
  end
21
37
 
22
- def self.log(*tags, **params)
23
- logger.log(*tags, **params)
38
+ def self.reset
39
+ @emitter = nil
24
40
  end
25
41
  end
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rails/generators'
4
+
3
5
  module EventLoggerRails
4
6
  module Generators
5
7
  ##
6
- # Creates basic config file for EventLoggerRails
8
+ # Creates basic config file and initializer for EventLoggerRails
7
9
  class InstallGenerator < Rails::Generators::Base
8
- desc 'Create basic config file for EventLoggerRails'
10
+ desc 'Create event registry file'
9
11
  source_root File.expand_path('templates', __dir__)
10
12
 
11
13
  def copy_config_file
14
+ return if File.exist?(File.join(destination_root, 'config/event_logger_rails.yml'))
15
+
12
16
  copy_file 'event_logger_rails.yml', 'config/event_logger_rails.yml'
13
17
  end
14
18
  end
@@ -1,15 +1,3 @@
1
- ##
2
- # Allowed logger levels for use with EventLoggerRails
3
- # You can add/remove levels, if desired; however, an exception will occur
4
- # when using a logger level that is not enumerated here.
5
- #
6
- logger_levels:
7
- - 'debug'
8
- - 'info'
9
- - 'warn'
10
- - 'error'
11
- - 'fatal'
12
-
13
1
  ##
14
2
  # Registered events for use with EventLoggerRails
15
3
  # Add your custom event definitions to this section; attempting to log events
@@ -17,9 +5,23 @@ logger_levels:
17
5
  #
18
6
  # Example event:
19
7
  #
20
- # registered_events:
8
+ # shared:
21
9
  # user:
22
10
  # signup:
23
- # success: 'Indicates a successful user signup.'
11
+ # success:
12
+ # description: 'Indicates a successful user signup.'
13
+ # failure:
14
+ # description: 'Indicates a user signup was not successful.'
15
+ # level: 'error'
16
+ #
17
+ shared:
18
+
19
+ ##
20
+ # Add events that are unique to specific environments by uncommenting the appropriate
21
+ # environment and adding the event.
22
+ #
23
+ # development:
24
+ #
25
+ # test:
24
26
  #
25
- registered_events:
27
+ # production:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: event_logger_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dick Davis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-28 00:00:00.000000000 Z
11
+ date: 2023-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -24,7 +24,9 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 7.0.0
27
- description: Rails gem that facilitates logging events for analysis.
27
+ description: |
28
+ EventLoggerRails is a Rails engine for emitting structured events in logs during the execution of business processes for analysis and visualization.
29
+ It allows teams to define events in a simple, centralized configuration file, and then log those events in JSON format for further processing.
28
30
  email:
29
31
  - dick@hey.com
30
32
  executables: []
@@ -33,25 +35,33 @@ extra_rdoc_files: []
33
35
  files:
34
36
  - README.md
35
37
  - Rakefile
36
- - app/controllers/concerns/event_logger_rails/loggable.rb
38
+ - app/controllers/concerns/event_logger_rails/loggable_controller.rb
39
+ - app/models/concerns/event_logger_rails/loggable_model.rb
37
40
  - config/routes.rb
38
41
  - lib/event_logger_rails.rb
42
+ - lib/event_logger_rails/current_request.rb
43
+ - lib/event_logger_rails/emitter.rb
39
44
  - lib/event_logger_rails/engine.rb
40
- - lib/event_logger_rails/event_logger.rb
45
+ - lib/event_logger_rails/event.rb
41
46
  - lib/event_logger_rails/exceptions/invalid_logger_level.rb
42
47
  - lib/event_logger_rails/exceptions/unregistered_event.rb
48
+ - lib/event_logger_rails/extensions/loggable.rb
49
+ - lib/event_logger_rails/json_logger.rb
50
+ - lib/event_logger_rails/message.rb
51
+ - lib/event_logger_rails/middleware/capture_request_details.rb
52
+ - lib/event_logger_rails/output.rb
43
53
  - lib/event_logger_rails/version.rb
44
54
  - lib/generators/event_logger_rails/install_generator.rb
45
55
  - lib/generators/event_logger_rails/templates/event_logger_rails.yml
46
56
  - lib/tasks/event_logger_rails_tasks.rake
47
- homepage: https://github.com/d3d1rty/event_logger_rails
57
+ homepage: https://github.com/dickdavis/event_logger_rails
48
58
  licenses:
49
59
  - MIT
50
60
  metadata:
51
61
  rubygems_mfa_required: 'true'
52
- homepage_uri: https://github.com/d3d1rty/event_logger_rails
53
- source_code_uri: https://github.com/d3d1rty/event_logger_rails
54
- changelog_uri: https://github.com/d3d1rty/event_logger_rails/blob/main/CHANGELOG.md
62
+ homepage_uri: https://github.com/dickdavis/event_logger_rails
63
+ source_code_uri: https://github.com/dickdavis/event_logger_rails/tree/0.3.0
64
+ changelog_uri: https://github.com/dickdavis/event_logger_rails/blob/0.3.0/CHANGELOG.md
55
65
  post_install_message:
56
66
  rdoc_options: []
57
67
  require_paths:
@@ -60,15 +70,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
60
70
  requirements:
61
71
  - - ">="
62
72
  - !ruby/object:Gem::Version
63
- version: '2.7'
73
+ version: 3.1.4
64
74
  required_rubygems_version: !ruby/object:Gem::Requirement
65
75
  requirements:
66
76
  - - ">="
67
77
  - !ruby/object:Gem::Version
68
78
  version: '0'
69
79
  requirements: []
70
- rubygems_version: 3.1.2
80
+ rubygems_version: 3.3.26
71
81
  signing_key:
72
82
  specification_version: 4
73
- summary: Rails gem that facilitates logging events for analysis.
83
+ summary: Rails gem weaving the fabric of logged events into tapestries for analytic
84
+ reverie.
74
85
  test_files: []
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module EventLoggerRails
4
- ##
5
- # Provides event logging with relevant controller/request data.
6
- module Loggable
7
- extend ActiveSupport::Concern
8
-
9
- def log_event(level, event, **data)
10
- data_to_log = data_from_request.merge(data)
11
- EventLoggerRails.log(level, event, **data_to_log)
12
- rescue EventLoggerRails::Exceptions::UnregisteredEvent => e
13
- log_event :error, 'event_logger_rails.event.unregistered', message: e.message
14
- rescue EventLoggerRails::Exceptions::InvalidLoggerLevel => e
15
- log_event :error, 'event_logger_rails.logger_level.invalid', message: e.message
16
- end
17
-
18
- def data_from_request
19
- {
20
- controller: controller_name.camelcase,
21
- action: action_name,
22
- method: request.method,
23
- path: request.path,
24
- remote_ip: request.remote_ip,
25
- parameters: request.query_parameters.to_json
26
- }
27
- end
28
- end
29
- end
@@ -1,97 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'yaml'
4
-
5
- require_relative 'exceptions/invalid_logger_level'
6
- require_relative 'exceptions/unregistered_event'
7
-
8
- module EventLoggerRails
9
- ##
10
- # Outputs event and related data logs.
11
- class EventLogger
12
- DEFAULT_EVENTS = [
13
- 'event_logger_rails.logger_level.invalid',
14
- 'event_logger_rails.event.unregistered',
15
- 'event_logger_rails.event.testing'
16
- ].freeze
17
- private_constant :DEFAULT_EVENTS
18
-
19
- def initialize
20
- @logger_levels = logger_levels_from_config
21
- @registered_events = registered_events_from_config
22
- @last_updated = File.ctime(config_file)
23
- end
24
-
25
- def log(*tags, **params)
26
- reload_config if config_changed?
27
-
28
- level, event = *tags
29
- validate_tags(level, event)
30
- logger = ActiveSupport::TaggedLogging.new(Logger.new(output_device))
31
- logger.tagged("#{level.to_s.upcase} | #{DateTime.current} | #{event}") { logger.send(level, **params.as_json) }
32
- end
33
-
34
- private
35
-
36
- attr_reader :logger_levels, :registered_events, :last_updated
37
-
38
- def logger_levels_from_config
39
- data_from_config[:logger_levels].map(&:to_sym)
40
- end
41
-
42
- def registered_events_from_config
43
- data_from_config[:registered_events]
44
- end
45
-
46
- def data_from_config
47
- YAML.safe_load(File.read(config_file)).deep_symbolize_keys
48
- end
49
-
50
- def config_file
51
- Rails.root.join('config/event_logger_rails.yml')
52
- end
53
-
54
- def reload_config
55
- @logger_levels = logger_levels_from_config
56
- @registered_events = registered_events_from_config
57
- @last_updated = File.ctime(config_file)
58
- end
59
-
60
- def config_changed?
61
- return false unless Rails.env.development?
62
-
63
- last_updated != File.ctime(config_file)
64
- end
65
-
66
- def validate_tags(level, event)
67
- validate_logger_level(level) && validate_event(event)
68
- end
69
-
70
- def validate_logger_level(level)
71
- return true if logger_levels.include?(level)
72
-
73
- raise EventLoggerRails::Exceptions::InvalidLoggerLevel.new(logger_level: level)
74
- end
75
-
76
- def validate_event(event)
77
- return true if event_registered?(event) || default_event?(event)
78
-
79
- raise EventLoggerRails::Exceptions::UnregisteredEvent.new(unregistered_event: event)
80
- end
81
-
82
- def event_registered?(event)
83
- parsed_event = event.split('.').map(&:to_sym)
84
- registered_events.dig(*parsed_event)
85
- end
86
-
87
- def default_event?(event)
88
- DEFAULT_EVENTS.include?(event)
89
- end
90
-
91
- def output_device
92
- return $stdout unless Rails.env.test?
93
-
94
- File.open(File::NULL, 'w')
95
- end
96
- end
97
- end