event_logger_rails 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95ba9a01dfc949b290f6142ac959cc206e705f5b286e2768381a82d6477f424d
4
- data.tar.gz: aa992346f7623bb36cd0d12fd83aed155a92a8645c0cfbe8b47ec8c7d22fcc3e
3
+ metadata.gz: 110cf01af709d86d2e848b9d11363ca58a0ab433eb444c05b9e9044d22ed35a3
4
+ data.tar.gz: 4b338f9e6f937e16195fc6f184bbad7e376409dcf44e545c50c3cec101b58123
5
5
  SHA512:
6
- metadata.gz: 7783f0fae629d0bf417b922a5b93257f22946b7b6b5f5304420341cf9e55f3a6471071fcd3bb36f1b00ed3862f181b6f222306f4975239c1353d24b57a3c94f8
7
- data.tar.gz: 6abc431c12d4395ae0b119fb62b26731a8509d22f4b98ac3eeb868a8de27a8255b6ff6b81fc83a8ebb4764643eda90d946a36cf3c1ba81db63b34cf1c26d6e8b
6
+ metadata.gz: 46be7cf18342e29d39b60f7cc4858858b5b880f16f049d65d89f69dccc0f0add12053be5020d6856a8db059f75f7f7b86f05e7733a55cadda3b3ed83dfe1e9c5
7
+ data.tar.gz: e69a33c9979e4bea40ec4ed0c723821c574586ab6a6ac29cea0f8e181fc0c545acf763b985d69b98899c5b2336262add45055479c3411673fc87ce20f760bc6f
data/README.md CHANGED
@@ -1,55 +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 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 }
143
+ ```
144
+
145
+ This will output an event with the corresponding severity level. You must provide a valid logger level (`:debug, :info, :warn, :error, or :unknown`).
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
+ }
49
176
  ```
50
177
 
51
- The log entry indicates the logger level (useful for filtering results), the registered event, and more useful information from the controller and request.
52
- This makes it simple use a tool like `awk` or another monitoring solution to parse the logs for emitted events, facilitating troubleshooting and analytics.
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
194
+ ```
195
+
196
+ By default, `EventLoggerRails` will include the model name, instance ID, and whatever data is passed.
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
+ }
223
+ ```
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
+ }
264
+ ```
265
+
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
+ ```
53
292
 
54
293
  ## Installation
55
294
 
@@ -62,27 +301,54 @@ gem 'event_logger_rails'
62
301
  And then execute:
63
302
 
64
303
  ```bash
65
- $ bundle
304
+ bundle
66
305
  ```
67
306
 
68
307
  Or install it yourself as:
69
308
 
70
309
  ```bash
71
- $ gem install event_logger_rails
310
+ gem install event_logger_rails
72
311
  ```
73
312
 
74
313
  Run the install generator to create a config file (`config/event_logger_rails.yml`):
75
314
 
76
315
  ```bash
77
- $ bin/rails generate event_logger_rails:install
316
+ bin/rails generate event_logger_rails:install
78
317
  ```
79
318
 
80
319
  Add your events to the generated config file following the structure of the examples.
81
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
+
82
347
  ## Contributing
83
348
 
84
- 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.
85
350
 
86
351
  ## License
87
352
 
88
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.0'
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.0
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-27 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