Skip to content

Commit

Permalink
Implement configurable event levels (#42)
Browse files Browse the repository at this point in the history
* Allow events to have a configurable level

* Add config for default level

* Refactor event processing to use configurable and default levels

* Update config file template

* Update README
  • Loading branch information
dickdavis authored Oct 6, 2023
1 parent c9fa8d6 commit 1c9be4f
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 49 deletions.
45 changes: 34 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
![Elrika in the Wired](elrika-in-the-wired.png?raw=true)
*Elrika in the Wired, the mascot for `EventLoggerRails`*

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 cryptic gems of understanding. 💎
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. 💎

### Visualize This

Expand All @@ -25,16 +25,21 @@ Don't let crucial events get lost in the digital void. Make your app's logging a
## Usage

You can define a registry of events your application emits via the config file (`config/event_logger_rails.yml`).
The events you define are placed in the `registered_events` structure in the config file.
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
events or event characteristics per environment.

For example, to register a user signup event, first define the event as a registered event:
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
optionally include a `level` to use for that specific event.

```yaml
registered_events:
shared:
user:
signup:
success: 'Indicates a user signup was successful.'
failure: 'Indicates a user signup was not successful.'
success:
description: 'Indicates a successful user signup.'
failure:
description: 'Indicates a user signup was not successful.'
level: 'error'
```
### Logging in Controllers
Expand Down Expand Up @@ -97,7 +102,7 @@ In this example, a possible successful signup could be structured like this:
"host": "d6aeb6b0516c",
"id": "2b8f44c1-0e42-4a5f-84b8-52656690d138",
"service_name": "DummyApp",
"level": "WARN",
"level": "ERROR",
"method": "POST",
"parameters": {
"authenticity_token": "[FILTERED]",
Expand All @@ -123,10 +128,11 @@ In this example, a possible successful signup could be structured like this:

Note how the log entry from the previous example contains the data passed in via the optional `data` argument.

You can also provide a logger level as an optional argument if you need to specify a logger level other than the default:
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
will override the configured event level and the default logger level.

```ruby
log_event 'user.signup.failure', level: :error, data: { errors: user.errors }
log_event 'user.signup.failure', level: :info, data: { errors: user.errors }
```

This will output an event with the corresponding severity level. You must provide a valid logger level (`:debug, :info, :warn, :error, or :unknown`).
Expand All @@ -138,7 +144,7 @@ This will output an event with the corresponding severity level. You must provid
"host": "d6aeb6b0516c",
"id": "2b8f44c1-0e42-4a5f-84b8-52656690d138",
"service_name": "DummyApp",
"level": "ERROR",
"level": "INFO",
"method": "POST",
"parameters": {
"authenticity_token": "[FILTERED]",
Expand Down Expand Up @@ -215,7 +221,7 @@ You can log events from anywhere inside of your application by calling `EventLog
from the request.

```ruby
EventLoggerRails.log 'user.signup.success', :info, { user_id: @user.id }
EventLoggerRails.log 'user.signup.success', level: :info, data: { user_id: @user.id }
```

### Errors
Expand Down Expand Up @@ -305,6 +311,15 @@ bin/rails generate event_logger_rails:install

Add your events to the generated config file following the structure of the examples.

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.
This default level is set to `:warn` unless otherwise specified.

```ruby
Rails.application.configure do |config|
config.event_logger_rails.default_level = :info
end
```

By default, `EventLoggerRails` outputs to a separate log file (`log/event_logger_rails.#{Rails.env}.log`) from normal Rails log output, allowing
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`:

Expand All @@ -314,6 +329,14 @@ Rails.application.configure do |config|
end
```

Some platforms require logging output to be sent to $STDOUT. You can configure this as an output device easily enough.

```ruby
Rails.application.configure do |config|
config.event_logger_rails.logdev = $stdout
end
```

## Contributing

Your inputs echo in this realm. Venture forth and materialize your thoughts through a PR.
Expand Down
1 change: 1 addition & 0 deletions lib/event_logger_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
##
# Namespace for EventLoggerRails gem
module EventLoggerRails
mattr_accessor :default_level
mattr_accessor :logdev
mattr_accessor :registered_events
mattr_accessor :sensitive_fields
Expand Down
7 changes: 4 additions & 3 deletions lib/event_logger_rails/emitter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ def initialize(logdev:)
@logger = JsonLogger.new(logdev)
end

def log(event, level, data = {})
def log(event, level:, data: {})
Event.new(event).validate! do |validated_event|
message = Message.new(event: validated_event, data:)
log_message(message, level)
level = level || validated_event.level || EventLoggerRails.default_level
log_message(message, level.to_sym)
end
rescue Exceptions::UnregisteredEvent, Exceptions::InvalidLoggerLevel => error
log(error.event, :error, { message: error.message })
log(error.event, level: :error, data: { message: error.message })
end

private
Expand Down
4 changes: 3 additions & 1 deletion lib/event_logger_rails/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ class Engine < ::Rails::Engine

config.event_logger_rails = ActiveSupport::OrderedOptions.new
config.event_logger_rails.logdev = "log/event_logger_rails.#{Rails.env}.log"
config.event_logger_rails.default_level = :warn

initializer 'event_logger_rails.add_middleware' do |app|
app.middleware.use Middleware::CaptureRequestDetails
end

config.after_initialize do |app|
EventLoggerRails.setup do |engine|
engine.registered_events = Rails.application.config_for(:event_logger_rails)
engine.default_level = app.config.event_logger_rails.default_level
engine.logdev = app.config.event_logger_rails.logdev
engine.registered_events = Rails.application.config_for(:event_logger_rails)
engine.sensitive_fields = app.config.filter_parameters
end
end
Expand Down
38 changes: 25 additions & 13 deletions lib/event_logger_rails/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,31 @@ module EventLoggerRails
# Models an event for logging.
class Event
DEFAULT_EVENTS = {
'event_logger_rails.logger_level.invalid' => 'Indicates provided level was invalid.',
'event_logger_rails.event.unregistered' => 'Indicates provided event was unregistered.',
'event_logger_rails.event.testing' => 'Event reserved for testing.'
'event_logger_rails.logger_level.invalid' => {
description: 'Indicates provided level was invalid.',
level: :error
},
'event_logger_rails.event.unregistered' => {
description: 'Indicates provided event was unregistered.',
level: :error
},
'event_logger_rails.event.testing' => {
description: 'Event reserved for testing.',
level: :warn
}
}.freeze
private_constant :DEFAULT_EVENTS

attr_reader :identifier, :description
attr_reader :identifier, :description, :level

def initialize(provided_identifier)
@provided_identifier = provided_identifier.to_s

default_registration = DEFAULT_EVENTS.slice(@provided_identifier).to_a.flatten
@identifier, @description = if default_registration.empty?
config_registration
else
default_registration
end
if (default_event = DEFAULT_EVENTS[@provided_identifier])
default_registration = [@provided_identifier, *default_event&.values]
end

@identifier, @description, @level = default_registration || config_registration
end

def merge(...)
Expand Down Expand Up @@ -59,10 +67,14 @@ def ==(other)

def config_registration
parsed_event = provided_identifier.split('.').map(&:to_sym)
if (description = EventLoggerRails.registered_events.dig(*parsed_event))
[provided_identifier, description]
config = EventLoggerRails.registered_events.dig(*parsed_event)
case config
in { description:, level: }
[provided_identifier, description, level]
in { description: }
[provided_identifier, description, nil]
else
[nil, nil]
[nil, nil, nil]
end
end
end
Expand Down
8 changes: 6 additions & 2 deletions lib/event_logger_rails/extensions/loggable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ module Extensions
##
# Provides event logging with relevant model data.
module Loggable
def log_event(event, level: :warn, data: {})
EventLoggerRails.log(event, level, data.merge(optional_data))
def log_event(event, **kwargs)
EventLoggerRails.log(
event,
level: kwargs[:level] || nil,
data: (kwargs[:data] || {}).merge(optional_data)
)
end

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
# shared:
# user:
# signup:
# success: 'Indicates a successful user signup.'
# success:
# description: 'Indicates a successful user signup.'
# failure:
# description: 'Indicates a user signup was not successful.'
# level: 'error'
#
shared:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ def test_three
.to have_received(:log)
.with(
'event_logger_rails.event.testing',
:warn,
hash_including(data_from_controller)
level: nil,
data: hash_including(data_from_controller)
)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ def test
.to have_received(:log)
.with(
'event_logger_rails.event.testing',
:warn,
hash_including(data_from_model)
level: nil,
data: hash_including(data_from_model)
)
end
end
72 changes: 70 additions & 2 deletions spec/lib/event_logger_rails/emitter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
end

describe '#log' do
subject(:method_call) { emitter.log(event, level, **data) }
subject(:method_call) { emitter.log(event, level:, data:) }

let(:event) { EventLoggerRails::Event.new('event_logger_rails.event.testing') }
let(:level) { :warn }
Expand Down Expand Up @@ -156,6 +156,74 @@
# rubocop:enable RSpec/ExampleLength
end

context 'when no logger level is provided and a level is configured for event' do
let(:event) { 'foo.bar' }
let(:level) { nil }

before do
EventLoggerRails.setup do |config|
config.registered_events = { foo: { bar: { description: 'foobar', level: 'info' } } }
config.default_level = 'debug'
end
end

# rubocop:disable RSpec/ExampleLength
it 'logs the configured severity, timestamp, event identifier, description, and data' do
method_call
log_output = JSON.parse(buffer.string, symbolize_names: true)
expect(log_output).to include(
environment: 'test',
event_description: 'foobar',
event_identifier: 'foo.bar',
format: 'text/html',
host: anything,
id: '1234',
level: 'INFO',
method: 'GET',
parameters: { foo: 'bar' },
path: '/',
remote_ip: '10.1.1.1',
service_name: 'Dummy',
timestamp: match(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?([+-]\d{2}:\d{2}|Z)?\z/)
)
end
# rubocop:enable RSpec/ExampleLength
end

context 'when no logger level is provided and no level is configured for event' do
let(:event) { 'foo.bar' }
let(:level) { nil }

before do
EventLoggerRails.setup do |config|
config.registered_events = { foo: { bar: { description: 'foobar' } } }
config.default_level = 'debug'
end
end

# rubocop:disable RSpec/ExampleLength
it 'logs the default severity, timestamp, event identifier, description, and data' do
method_call
log_output = JSON.parse(buffer.string, symbolize_names: true)
expect(log_output).to include(
environment: 'test',
event_description: 'foobar',
event_identifier: 'foo.bar',
format: 'text/html',
host: anything,
id: '1234',
level: 'DEBUG',
method: 'GET',
parameters: { foo: 'bar' },
path: '/',
remote_ip: '10.1.1.1',
service_name: 'Dummy',
timestamp: match(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?([+-]\d{2}:\d{2}|Z)?\z/)
)
end
# rubocop:enable RSpec/ExampleLength
end

context 'when the logger level is not valid' do
let(:level) { :foo }

Expand Down Expand Up @@ -184,7 +252,7 @@
end

context 'when data is not provided' do
subject(:method_call) { emitter.log(event, level) }
subject(:method_call) { emitter.log(event, level:) }

# rubocop:disable RSpec/ExampleLength
it 'logs the default severity, timestamp, event identifier, and description' do
Expand Down
18 changes: 17 additions & 1 deletion spec/lib/event_logger_rails/event_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
RSpec.describe EventLoggerRails::Event do
subject(:event) { described_class.new(identifier) }

let(:event_config) { { description: 'This is an event.' } }

before do
EventLoggerRails.setup do |config|
config.registered_events = {
foo: { bar: 'This is an event.' }
foo: { bar: event_config }
}
end
end
Expand All @@ -24,6 +26,20 @@
it 'description contains the corresponding description' do
expect(event.description).to eq('This is an event.')
end

context 'when a level is not provided' do
it 'level is nil' do
expect(event.level).to be_nil
end
end

context 'when a level is provided' do
let(:event_config) { { description: 'This is an event.', level: :warn } }

it 'level contains the configured level' do
expect(event.level).to eq(:warn)
end
end
end

context 'when an unregistered event is provided' do
Expand Down
Loading

0 comments on commit 1c9be4f

Please sign in to comment.