diff --git a/app/controllers/concerns/event_logger_rails/loggable_controller.rb b/app/controllers/concerns/event_logger_rails/loggable_controller.rb index 976e039..c7168d2 100644 --- a/app/controllers/concerns/event_logger_rails/loggable_controller.rb +++ b/app/controllers/concerns/event_logger_rails/loggable_controller.rb @@ -7,6 +7,9 @@ module LoggableController extend ActiveSupport::Concern include EventLoggerRails::Extensions::Loggable + # Includes the controller name and action in the log output. + # + # @return [Hash] The data to include in log output. def optional_data { action: action_name, diff --git a/app/models/concerns/event_logger_rails/loggable_model.rb b/app/models/concerns/event_logger_rails/loggable_model.rb index a082f40..782fdf9 100644 --- a/app/models/concerns/event_logger_rails/loggable_model.rb +++ b/app/models/concerns/event_logger_rails/loggable_model.rb @@ -7,6 +7,9 @@ module LoggableModel extend ActiveSupport::Concern include EventLoggerRails::Extensions::Loggable + # Includes the model name and instance ID in the log output. + # + # @return [Hash] The data to include in log output. def optional_data { model: self.class.name, diff --git a/lib/event_logger_rails.rb b/lib/event_logger_rails.rb index 76788eb..ee8ccc0 100644 --- a/lib/event_logger_rails.rb +++ b/lib/event_logger_rails.rb @@ -15,26 +15,58 @@ require 'event_logger_rails/output' require 'event_logger_rails/version' -## -# Namespace for EventLoggerRails gem +# Provides configurable state and public API for EventLoggerRails. +# Also serves as the namespace for the gem. module EventLoggerRails + # @!attribute [r] default_level + # @return [Symbol] The default level of the events logged by EventLoggerRails. mattr_accessor :default_level + + # @!attribute [r] logdev + # @return [IO, #write] The log device used by EventLoggerRails. mattr_accessor :logdev + + # @!attribute [r] registered_events + # @return [Array] The events registry defined in the config/event_logger_rails.yml file. mattr_accessor :registered_events + + # @!attribute [r] sensitive_fields + # @return [Array] The fields which may contain sensitive data that EventLoggerRails should filter. mattr_accessor :sensitive_fields + # Provides a method for configuring EventLoggerRails. + # + # @yield [self] Gives the class itself to the block for configuration. + # @example + # EventLoggerRails.setup do |config| + # config.default_level = :info + # end + # @return [void] def self.setup yield self end + # Returns or initializes the Emitter instance for EventLoggerRails. + # + # @note The emitter is initialized with the configured log device. + # @return [Emitter] The Emitter instance used for logging events. def self.emitter @emitter ||= Emitter.new(logdev:) end + # Forwards the arguments to the Emitter's log method. + # + # @example + # EventLoggerRails.log('foo.bar.baz', level: :info, data: { foo: 'bar' }) + # @param (see Emitter#log) + # @return [void] def self.log(...) emitter.log(...) end + # Resets the Emitter instance. + # + # @return [void] def self.reset @emitter = nil end diff --git a/lib/event_logger_rails/current_request.rb b/lib/event_logger_rails/current_request.rb index 3e39364..ae67ff4 100644 --- a/lib/event_logger_rails/current_request.rb +++ b/lib/event_logger_rails/current_request.rb @@ -1,9 +1,21 @@ # frozen_string_literal: true module EventLoggerRails - ## # Provides global state with request details class CurrentRequest < ActiveSupport::CurrentAttributes + # @note Defines the attributes for the current request object. + # @!attribute [rw] id + # @return [String] The ID of the request. + # @!attribute [rw] format + # @return [Symbol] The format of the request. + # @!attribute [rw] method + # @return [String] The HTTP method of the request. + # @!attribute [rw] parameters + # @return [Hash] The parameters of the request. + # @!attribute [rw] path + # @return [String] The path of the request. + # @!attribute [rw] remote_ip + # @return [String] The remote IP of the request. attribute :id, :format, :method, :parameters, :path, :remote_ip end end diff --git a/lib/event_logger_rails/emitter.rb b/lib/event_logger_rails/emitter.rb index c0cd91e..196b408 100644 --- a/lib/event_logger_rails/emitter.rb +++ b/lib/event_logger_rails/emitter.rb @@ -4,10 +4,24 @@ module EventLoggerRails ## # Processes events, sending data to logger. class Emitter + ## Initializes the emitter using the given log device for log output. + # + # @param logdev [IO, #write] The log device for log output. def initialize(logdev:) @logger = JsonLogger.new(logdev) end + # Validates and logs an event with the given level and data. + # If an error is raised, it recursively calls itself with the error's event. + # + # @note Prefer to use the public API provided by `EventLoggerRails.log()`. + # @param event [EventLoggerRails::Event, String] The event to log. Can be a string or an Event object. + # @param level [Symbol] The level of the event. + # @param data [Hash] Additional data to log. + # @return [Integer] The number of bytes written to the log. + # @example + # emitter = EventLoggerRails::Emitter.new(logdev: $stdout) + # emitter.log('foo.bar.baz', level: :info, data: { foo: 'bar' }) def log(event, level:, data: {}) Event.new(event).validate! do |validated_event| message = Message.new(event: validated_event, data:) @@ -20,8 +34,16 @@ def log(event, level:, data: {}) private + # @!attribute [r] logger + # @return [JsonLogger] The logger instance used for log output. attr_reader :logger + # Logs a message with the given level. + # + # @param message [String] The message to log. + # @param level [Symbol] The level of the message. + # @return [Integer] The number of bytes written to the log. + # @raise [EventLoggerRails::Exceptions::InvalidLoggerLevel] If the level is invalid. def log_message(message, level) logger.send(level) { message } rescue NoMethodError diff --git a/lib/event_logger_rails/engine.rb b/lib/event_logger_rails/engine.rb index 1c5ae87..377cb57 100644 --- a/lib/event_logger_rails/engine.rb +++ b/lib/event_logger_rails/engine.rb @@ -1,28 +1,40 @@ # frozen_string_literal: true module EventLoggerRails - ## - # Engine for plugging into Rails + # Engine for plugging into Rails. class Engine < ::Rails::Engine + # Use the EventLoggerRails namespace. isolate_namespace EventLoggerRails + # Use the rspec test framework. config.generators do |generator| generator.test_framework :rspec end + # Initialize the EventLoggerRails configuration. config.event_logger_rails = ActiveSupport::OrderedOptions.new config.event_logger_rails.logdev = "log/event_logger_rails.#{Rails.env}.log" + config.event_logger_rails.logger_class = 'EventLoggerRails::JsonLogger' config.event_logger_rails.default_level = :warn + # Add the EventLoggerRails middleware. initializer 'event_logger_rails.add_middleware' do |app| + # Use middleware to capture the request details. app.middleware.use Middleware::CaptureRequestDetails end + # Configure EventLoggerRails config.after_initialize do |app| EventLoggerRails.setup do |engine| + # Set the default logging level from the registration. engine.default_level = app.config.event_logger_rails.default_level + # Set the log device from the registration. engine.logdev = app.config.event_logger_rails.logdev + # Set the logger class from the registration. + engine.logger_class = app.config.event_logger_rails.logger_class + # Set the registered events from the registration. engine.registered_events = Rails.application.config_for(:event_logger_rails) + # Set the sensitive fields from the registration. engine.sensitive_fields = app.config.filter_parameters end end diff --git a/lib/event_logger_rails/event.rb b/lib/event_logger_rails/event.rb index c70b859..13ca4dc 100644 --- a/lib/event_logger_rails/event.rb +++ b/lib/event_logger_rails/event.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true module EventLoggerRails - ## # Models an event for logging. class Event + # Contains the default event registration. DEFAULT_EVENTS = { 'event_logger_rails.logger_level.invalid' => { description: 'Indicates provided level was invalid.', @@ -20,32 +20,89 @@ class Event }.freeze private_constant :DEFAULT_EVENTS - attr_reader :identifier, :description, :level + # @!attribute [r] identifier + # @return [String] The identifier of the event. + attr_reader :identifier + # @!attribute [r] description + # @return [String] The description of the event. + attr_reader :description + + # @!attribute [r] level + # @return [Symbol] The configured logging level of the event. + attr_reader :level + + # Initializes the event using the provided identifier to determine its properties from + # either the default registration (for default events) or the user-defined registry. + # + # @param provided_identifier [EventLoggerRails::Event, String] The event or its identifier. def initialize(provided_identifier) @provided_identifier = provided_identifier.to_s + # Attempt to find default registration for event if (default_event = DEFAULT_EVENTS[@provided_identifier]) default_registration = [@provided_identifier, *default_event&.values] end + # Fallback to user-defined registration if default not found. + # Deconstruct registration to set identifier, description, and level attributes. @identifier, @description, @level = default_registration || config_registration end + # Converts the event into a hash and merges the given hash into it. + # + # @param kwargs [Hash] The hash to merge into the event. + # @return [Hash] The merged hash. + # @example + # event = EventLoggerRails::Event.new('event_logger_rails.event.testing') + # event.merge(foo: 'bar') + # # { + # # event_identifier: 'event_logger_rails.event.testing', + # # event_description: 'Event reserved for testing', + # # foo: 'bar' + # # } def merge(...) to_hash.merge(...) end + # Determines if the event is valid. + # + # @return [Boolean] true if the event is valid, false otherwise. + # @example + # valid_event = EventLoggerRails::Event.new('event_logger_rails.event.testing') + # valid_event.valid? # => true + # invalid_event = EventLoggerRails::Event.new('foo.bar.baz') + # invalid_event.valid? # => false def valid? identifier.present? end + # Validates the event and yields it to the given block. + # + # @note This only validates the event registration. Logger level is validated at the time of logging. + # @yield [self] Yields the event to the given block. + # @raise [EventLoggerRails::Exceptions::UnregisteredEvent] If the event is not registered. + # @example + # event = EventLoggerRails::Event.new('event_logger_rails.event.testing') + # event.validate! do |validated_event| + # puts "Event: #{validated_event}" + # end def validate! raise Exceptions::UnregisteredEvent.new(unregistered_event: self) unless valid? yield(self) end + # Returns a hash representation of the event. + # + # @return [Hash] The event as a hash. + # @example + # event = EventLoggerRails::Event.new('event_logger_rails.event.testing') + # event.to_hash + # # { + # # event_identifier: 'event_logger_rails.event.testing', + # # event_description: 'Event reserved for testing' + # # } def to_hash { event_identifier: identifier, @@ -53,18 +110,38 @@ def to_hash } end + # Returns a string representation of the event. + # The provided identifier is returned if the event is not registered. + # + # @return [String] The event as a string. + # @example + # event = EventLoggerRails::Event.new('event_logger_rails.event.testing') + # event.to_s # => 'event_logger_rails.event.testing' def to_s identifier&.to_s || provided_identifier.to_s end + # Determines if the event is equivalent to the given object through string comparison. + # + # @param other [EventLoggerRails::Event] The event to compare. + # @return [Boolean] true if the event is equal to the given object, false otherwise. + # @example + # event = EventLoggerRails::Event.new('event_logger_rails.event.testing') + # event == 'event_logger_rails.event.testing' # => true def ==(other) to_s == other.to_s end private + # @!attribute [r] provided_identifier + # @return [String] The identifier provided when the event was initialized. attr_reader :provided_identifier + # Parses the event identifier and looks up the details from the user-defined registry. + # + # @return [Array] The identifier, description, and level of the event. + # If the event is not registered, each array element will be nil. def config_registration parsed_event = provided_identifier.split('.').map(&:to_sym) config = EventLoggerRails.registered_events.dig(*parsed_event) diff --git a/lib/event_logger_rails/exceptions/invalid_logger_level.rb b/lib/event_logger_rails/exceptions/invalid_logger_level.rb index ed9dca3..766e25a 100644 --- a/lib/event_logger_rails/exceptions/invalid_logger_level.rb +++ b/lib/event_logger_rails/exceptions/invalid_logger_level.rb @@ -2,17 +2,22 @@ module EventLoggerRails module Exceptions - ## # Indicates invalid log level provided. class InvalidLoggerLevel < StandardError + # @!attribute [r] event + # @return [EventLoggerRails::Event] The default invalid logging level event. attr_reader :event + # Initializes the exception with the given logger level. def initialize(logger_level:) super @event = Event.new('event_logger_rails.logger_level.invalid') @logger_level = logger_level end + # Provides an informative error message. + # + # @return [String] The error message. def message "Invalid logger level provided: '#{logger_level.to_sym}'. " \ 'Valid levels: :debug, :info, :warn, :error, :unknown.' @@ -20,6 +25,8 @@ def message private + # @!attribute [r] logger_level + # @return [Symbol] The invalid logger level. attr_reader :logger_level end end diff --git a/lib/event_logger_rails/exceptions/unregistered_event.rb b/lib/event_logger_rails/exceptions/unregistered_event.rb index 1dd9cce..11ebb8f 100644 --- a/lib/event_logger_rails/exceptions/unregistered_event.rb +++ b/lib/event_logger_rails/exceptions/unregistered_event.rb @@ -2,23 +2,32 @@ module EventLoggerRails module Exceptions - ## # Indicates event provided not registered. class UnregisteredEvent < StandardError + # @!attribute [r] event + # @return [EventLoggerRails::Event] The default event for unregistered events. attr_reader :event + # Initializes the exception with the given unregistered event. + # + # @param unregistered_event [EventLoggerRails::Event] The unregistered event. def initialize(unregistered_event:) super() @event = Event.new('event_logger_rails.event.unregistered') @unregistered_event = unregistered_event end + # Provides an informative error message. + # + # @return [String] The error message. def message "Event provided not registered: #{unregistered_event}" end private + # @!attribute [r] unregistered_event + # @return [EventLoggerRails::Event] The unregistered event. attr_reader :unregistered_event end end diff --git a/lib/event_logger_rails/extensions/loggable.rb b/lib/event_logger_rails/extensions/loggable.rb index aaea0ff..ab0e428 100644 --- a/lib/event_logger_rails/extensions/loggable.rb +++ b/lib/event_logger_rails/extensions/loggable.rb @@ -2,9 +2,13 @@ module EventLoggerRails module Extensions - ## - # Provides event logging with relevant model data. + # Provides event logging with optional data. module Loggable + # Logs an event with the given level and data. + # + # @param event [EventLoggerRails::Event] The event to log. + # @option kwargs [Symbol] :level The level of the event. + # @option kwargs [Hash] :data The data of the event. def log_event(event, **kwargs) EventLoggerRails.log( event, @@ -15,6 +19,10 @@ def log_event(event, **kwargs) private + # Optional data to include in log output. + # + # @return [Hash] The data to include in log output. + # @note This method can be overridden by classes that implement Loggable. def optional_data {} end diff --git a/lib/event_logger_rails/json_logger.rb b/lib/event_logger_rails/json_logger.rb index 89b8c23..aad2bb2 100644 --- a/lib/event_logger_rails/json_logger.rb +++ b/lib/event_logger_rails/json_logger.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true module EventLoggerRails - ## # Writes log entries in JSON format class JsonLogger < ::Logger include ActiveSupport::LoggerSilence + # Initializes the logger with a JSON formatter. + # + # @param logdev [IO, #write] The log device for log output. def initialize(...) super(...) @formatter = proc do |level, timestamp, _progname, message| diff --git a/lib/event_logger_rails/message.rb b/lib/event_logger_rails/message.rb index 31f9575..0162f19 100644 --- a/lib/event_logger_rails/message.rb +++ b/lib/event_logger_rails/message.rb @@ -1,20 +1,32 @@ # frozen_string_literal: true module EventLoggerRails - ## # Models a message sent to the logger containing event and optional data class Message + # Initializes the message with the given event and data. + # + # @param event [EventLoggerRails::Event] The event to log. + # @param data [Hash] Additional data to log. def initialize(event:, data: {}) @event = event @data = data end + # Converts the message to a hash containing the event and data details. + # + # @return [Hash] The hash representation of the message. def to_hash event.merge(data) end private - attr_reader :event, :data + # @!attribute [r] event + # @return [EventLoggerRails::Event] The event to log. + attr_reader :event + + # @!attribute [r] data + # @return [Hash] Additional data to log. + attr_reader :data end end diff --git a/lib/event_logger_rails/middleware/capture_request_details.rb b/lib/event_logger_rails/middleware/capture_request_details.rb index 6624318..177dc53 100644 --- a/lib/event_logger_rails/middleware/capture_request_details.rb +++ b/lib/event_logger_rails/middleware/capture_request_details.rb @@ -4,14 +4,21 @@ module EventLoggerRails module Middleware - ## # Middleware to capture request details and store in global state class CaptureRequestDetails + # Initializes the middleware with the given app. + # + # @param app [Proc] The Rack app. def initialize(app) @app = app end # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + + # Captures request details and stores in global state + # + # @param env [Hash] The Rack environment. + # @note The CurrentRequest is reset at the end of the request. def call(env) begin request = ActionDispatch::Request.new(env) diff --git a/lib/event_logger_rails/output.rb b/lib/event_logger_rails/output.rb index 503a4d6..0fdef45 100644 --- a/lib/event_logger_rails/output.rb +++ b/lib/event_logger_rails/output.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true module EventLoggerRails - ## # Merges data from application, request, and logger message for structured output class Output + # Initializes the output with the given level, timestamp, and message. + # + # @param level [Symbol] The level of the event. + # @param timestamp [Time] The timestamp of the event. + # @param message [EventLoggerRails::Message] The message of the event. def initialize(level:, timestamp:, message:) @current_request = EventLoggerRails::CurrentRequest @level = level @@ -11,22 +15,48 @@ def initialize(level:, timestamp:, message:) @message = message.respond_to?(:to_hash) ? sanitizer.filter(**message) : { message: } end + # Converts the output to a JSON string. + # + # @return [String] The JSON representation of the output. def to_json(*args) JSON.generate(to_hash, *args) end + # Converts the output to a hash containing the application, request, and logger details. + # + # @return [Hash] The hash representation of the output. def to_hash application_data.merge(**current_request_data, **logger_data) end private - attr_reader :level, :timestamp, :message, :current_request + # @!attribute [r] level + # @return [Symbol] The level of the event. + attr_reader :level + # @!attribute [r] timestamp + # @return [Time] The timestamp of the event. + attr_reader :timestamp + + # @!attribute [r] message + # @return [EventLoggerRails::Message] The message to log. + attr_reader :message + + # @!attribute [r] current_request + # @return [EventLoggerRails::CurrentRequest] The object storing the current request data. + attr_reader :current_request + + # Finds or initializes a parameter filter for sensitive data. + # + # @return [ActiveSupport::ParameterFilter] The parameter filter for sensitive data. def sanitizer @sanitizer ||= ActiveSupport::ParameterFilter.new(EventLoggerRails.sensitive_fields) end + # Structures the application data to include in the output. + # + # @return [Hash] The application data to include in the output. def application_data { environment: Rails.env, @@ -36,6 +66,10 @@ def application_data end # rubocop:disable Metrics/AbcSize + + # Structures the request data to include in the output. + # + # @return [Hash] The request data to include in the output. def current_request_data return {} if CurrentRequest.instance.attributes.blank? @@ -50,6 +84,10 @@ def current_request_data end # rubocop:enable Metrics/AbcSize + # Structures the logger data to include in the output. + # + # @return [Hash] The logger data to include in the output. + # @note The logger data includes the level, timestamp, and message. def logger_data { level:, diff --git a/lib/event_logger_rails/version.rb b/lib/event_logger_rails/version.rb index 0edbb4b..32facc8 100644 --- a/lib/event_logger_rails/version.rb +++ b/lib/event_logger_rails/version.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true module EventLoggerRails + # The version of the gem. + # + # @return [String] The version of the gem. VERSION = '0.3.1' end