diff --git a/.reek.yml b/.reek.yml index 487e74e..678bca1 100644 --- a/.reek.yml +++ b/.reek.yml @@ -15,7 +15,6 @@ detectors: - Truemail::Validate::Smtp#run - Truemail::Validate::Smtp#not_includes_user_not_found_errors? - Truemail::Validate::Mx#hosts_from_cname_records - - Truemail::Configuration#logger= - Truemail::Validate::Smtp::Request#initialize - Truemail::Validate::Smtp::Request::Session#initialize - Truemail::RSpec::ValidatorHelper::ValidatorFactory#process_validator_params @@ -40,7 +39,6 @@ detectors: UtilityFunction: exclude: - Truemail::Audit::Base#verifier_domain - - Truemail::Configuration#logger_options - Truemail::Configuration#match_regex? - Truemail::Configuration#regex_by_method - Truemail::Dns::Worker#nameserver_port @@ -61,6 +59,7 @@ detectors: - Truemail#raise_unless - Truemail::Configuration#raise_unless - Truemail#determine_configuration + - Truemail::Logger::Builder#raise_unless FeatureEnvy: exclude: @@ -75,12 +74,12 @@ detectors: NilCheck: exclude: - Truemail::Validator#result_not_changed? - - Truemail::Configuration#logger= + - Truemail::Logger::Builder#validate_logger_output - Truemail::Validator#run - BooleanParameter: + ManualDispatch: exclude: - - Truemail::Configuration#logger= + - Truemail::Logger::Builder#validate_logger_instance TooManyConstants: exclude: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1905220..a4b635a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.3.0] - 2024.02.26 + +### Added + +- Added ability to use custom logger instance in event logger. Thanks [@n-rodriguez](https://github.com/n-rodriguez) for report +- Added `Truemail::Logger::Builder`, tests + +### Updated + +- Updated `Truemail::Configuration#logger=`, tests +- Updated `Truemail::Logger`, tests +- Updated `circleci`/`lefthook` configs +- Updated gem version + +### Removed + +- Removed `Truemail::Configuration#logger_options` + ## [3.2.0] - 2024.02.24 ### Added diff --git a/README.md b/README.md index 15408c5..ddc34ef 100644 --- a/README.md +++ b/README.md @@ -251,11 +251,11 @@ Truemail.configure do |config| config.smtp_safe_check = true # Optional parameter. This option will enable tracking events. You can print tracking - # events to stdout, write to file or both of these. Logger class by default is Logger - # from Ruby stdlib. Tracking event by default is :error + # events to stdout, write to file or both of these. Logger instance by default is Logger + # instance from Ruby stdlib. You can override this behavior. See how to do it in the + # examples below. Tracking event by default is :error # Available tracking event: :all, :unrecognized_error, :recognized_error, :error config.logger = { - logger_class: MyCustomLogger, tracking_event: :all, stdout: true, log_absolute_path: '/home/app/log/truemail.log' @@ -292,7 +292,7 @@ Truemail.configuration @smtp_fail_fast=true, @smtp_safe_check=true, @logger=#> + @event=:all, @file="/home/app/log/truemail.log", @custom_logger=nil @stdout=true>> ``` ##### Update global configuration @@ -328,7 +328,7 @@ Truemail.configuration @smtp_fail_fast=true, @smtp_safe_check=true, @logger=#> + @event=:all, @file="/home/app/log/truemail.log", @custom_logger=nil @stdout=true>> ``` ##### Reset global configuration @@ -1263,16 +1263,11 @@ end #### Using custom logger -By default Truemail uses `Logger`, default logger class from Ruby stdlib. But you can override this behavior passing your own class in logger configuration. Please note, your own logger class should have the same interface as builtin stdlib `Logger` class. +By default Truemail uses `Logger`, default logger instance from Ruby stdlib. But you can override this behavior passing your logger instance in event logger configuration. Please note, your own logger instance should have the same interface as builtin stdlib `Logger` instance. In this case `custom_logger` is only one required field for logger configuration (you don't have to use `stdout` and `log_absolute_path`). ```ruby Truemail.configure do |config| - config.logger = { - logger_class: MyCustomLogger, - tracking_event: :all, - stdout: true, - log_absolute_path: '/home/app/log/truemail.log' - } + config.logger = { custom_logger: MyCustomLogger.new } end ``` diff --git a/lib/truemail/configuration.rb b/lib/truemail/configuration.rb index a88dfc8..9e8a385 100644 --- a/lib/truemail/configuration.rb +++ b/lib/truemail/configuration.rb @@ -2,15 +2,12 @@ module Truemail class Configuration - require 'logger' - DEFAULT_CONNECTION_TIMEOUT = 2 DEFAULT_RESPONSE_TIMEOUT = 2 DEFAULT_CONNECTION_ATTEMPTS = 2 DEFAULT_VALIDATION_TYPE = :smtp DEFAULT_SMTP_PORT = 25 DEFAULT_LOGGER_OPTIONS = { - logger_class: ::Logger, tracking_event: :error, stdout: false, log_absolute_path: nil @@ -82,17 +79,9 @@ def argument_consistent?(method, argument) end end - def logger=(options) # rubocop:disable Metrics/AbcSize + def logger=(options) raise_unless(options, __method__, options.is_a?(::Hash)) - logger_class, tracking_event, stdout, log_absolute_path = logger_options(options) - raise_unless(logger_class, __method__, logger_class.is_a?(::Class)) - valid_event = Truemail::Log::Event::TRACKING_EVENTS.key?(tracking_event) - stdout_only = stdout && log_absolute_path.nil? - file_only = log_absolute_path.is_a?(::String) - both_types = stdout && file_only - argument_info = valid_event ? log_absolute_path : tracking_event - raise_unless(argument_info, __method__, valid_event && (stdout_only || file_only || both_types)) - @logger = Truemail::Logger.new(logger_class, tracking_event, stdout, log_absolute_path) + @logger = Truemail::Logger::Builder.call(Truemail::Configuration::DEFAULT_LOGGER_OPTIONS, **options) end def complete? @@ -163,9 +152,5 @@ def validate_validation_type(settings) check_validation_type(validation_type) end end - - def logger_options(current_options) - Truemail::Configuration::DEFAULT_LOGGER_OPTIONS.merge(current_options).values - end end end diff --git a/lib/truemail/logger.rb b/lib/truemail/logger.rb index 8752ce7..686b4e7 100644 --- a/lib/truemail/logger.rb +++ b/lib/truemail/logger.rb @@ -2,13 +2,67 @@ module Truemail class Logger - attr_reader :logger_class, :event, :stdout, :file + require 'logger' - def initialize(logger_class, event, error_stdout, log_absolute_path) - @logger_class = logger_class + BUILDER_ATTRS = %i[tracking_event stdout log_absolute_path custom_logger].freeze + + Builder = ::Struct.new(*BUILDER_ATTRS, keyword_init: true) do + private_class_method :new + + def self.call(default_settings, **logger_attributes) + new(default_settings, **logger_attributes).validate_attributes.build_instance + end + + def initialize(default_settings, **args) + super(**default_settings.merge(**args)) + end + + def validate_attributes + validate_logger_event + return validate_logger_instance if custom_logger + validate_logger_output + self + end + + def build_instance + Truemail::Logger.new(tracking_event, stdout, log_absolute_path, custom_logger) + end + + private + + def raise_unless(argument_context, argument_name, condition) + raise Truemail::ArgumentError.new(argument_context, argument_name) unless condition + end + + def validate_logger_instance + raise_unless(custom_logger, :custom_logger=, custom_logger && custom_logger.respond_to?(:add)) + self + end + + def validate_logger_event + raise_unless(tracking_event, :tracking_event=, Truemail::Log::Event::TRACKING_EVENTS.key?(tracking_event)) + end + + def validate_logger_output + stdout_only = stdout && log_absolute_path.nil? + file_only = log_absolute_path.is_a?(::String) + both_types = stdout && file_only + raise_unless( + { stdout: stdout, log_absolute_path: log_absolute_path }, + 'stdout=, log_absolute_path=', + both_types || stdout_only || file_only + ) + end + end + + attr_reader :event, :stdout, :file, :custom_logger, :stdout_logger, :file_logger + + def initialize(event, error_stdout, log_absolute_path, custom_logger) @event = event @stdout = error_stdout @file = log_absolute_path + @custom_logger = custom_logger + init_builtin_loggers end def push(validator_instance) @@ -26,10 +80,22 @@ def init_log_file output_file end - def create_logs(log_level, serialized_object) + def init_builtin_loggers + return if custom_logger %i[stdout file].each do |output_type| next unless public_send(output_type) - logger_class.new(output_type.eql?(:stdout) ? $stdout : init_log_file).add(log_level) { serialized_object } + instance_variable_set( + :"@#{output_type}_logger", + ::Logger.new(output_type.eql?(:stdout) ? $stdout : init_log_file) + ) + end + end + + def create_logs(log_level, serialized_object) + %i[custom_logger stdout_logger file_logger].each do |getter| + logger_instance = public_send(getter) + next unless logger_instance + logger_instance.add(log_level) { serialized_object } end end end diff --git a/lib/truemail/version.rb b/lib/truemail/version.rb index a38c216..c793413 100644 --- a/lib/truemail/version.rb +++ b/lib/truemail/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Truemail - VERSION = '3.2.0' + VERSION = '3.3.0' end diff --git a/spec/truemail/configuration_spec.rb b/spec/truemail/configuration_spec.rb index 4c4727a..5215830 100644 --- a/spec/truemail/configuration_spec.rb +++ b/spec/truemail/configuration_spec.rb @@ -36,7 +36,6 @@ specify do expect(described_class::DEFAULT_LOGGER_OPTIONS).to eq( - logger_class: ::Logger, tracking_event: :error, stdout: false, log_absolute_path: nil @@ -645,6 +644,7 @@ end let(:default_tracking_events_expectation) { expect(configuration_instance.logger.event).to eq(:error) } + let(:file) { Pathname(::File.expand_path('../support/tmp/log', ::File.dirname(__FILE__))) } context 'when stdout only' do let(:logger_params) { { stdout: true } } @@ -659,7 +659,9 @@ end context 'when file only' do - let(:logger_params) { { log_absolute_path: 'some_absolute_path' } } + let(:logger_params) { { log_absolute_path: file.to_s } } + + after { ::FileUtils.rm_rf(file.dirname) } include_examples 'sets logger instance' @@ -671,7 +673,9 @@ end context 'when stdout and file outputs' do - let(:logger_params) { { stdout: true, log_absolute_path: 'some_absolute_path' } } + let(:logger_params) { { stdout: true, log_absolute_path: file.to_s } } + + after { ::FileUtils.rm_rf(file.dirname) } include_examples 'sets logger instance' @@ -702,12 +706,12 @@ end context 'with valid logger class' do - let(:logger_class) { ::Class } - let(:logger_params) { { logger_class: logger_class, stdout: true } } + let(:custom_logger) { instance_double('LoggerClassInstance', add: 42) } + let(:logger_params) { { custom_logger: custom_logger, stdout: true } } it 'sets logger class' do set_logger - expect(configuration_instance.logger.logger_class).to eq(logger_class) + expect(configuration_instance.logger.custom_logger).to eq(custom_logger) end end end @@ -717,6 +721,10 @@ specify { expect { set_logger }.to raise_error(Truemail::ArgumentError, error_message) } end + let(:error_message_wrong_logger_output) do + '{:stdout=>false, :log_absolute_path=>nil} is not a valid stdout=, log_absolute_path=' + end + context 'with wrong params type' do let(:logger_params) { 42 } let(:error_message) { "#{logger_params} is not a valid logger=" } @@ -726,7 +734,7 @@ context 'with empty params' do let(:logger_params) { {} } - let(:error_message) { ' is not a valid logger=' } + let(:error_message) { error_message_wrong_logger_output } include_examples 'raises logger argument error' end @@ -734,7 +742,7 @@ context 'with log_absolute_path wrong type' do let(:log_absolute_path) { true } let(:logger_params) { { log_absolute_path: log_absolute_path } } - let(:error_message) { "#{log_absolute_path} is not a valid logger=" } + let(:error_message) { '{:stdout=>false, :log_absolute_path=>true} is not a valid stdout=, log_absolute_path=' } include_examples 'raises logger argument error' end @@ -742,7 +750,7 @@ context 'when attempt configure logger without output' do let(:stdout) { false } let(:logger_params) { { stdout: stdout } } - let(:error_message) { ' is not a valid logger=' } + let(:error_message) { error_message_wrong_logger_output } include_examples 'raises logger argument error' end @@ -750,15 +758,15 @@ context 'with not existing tracking event' do let(:tracking_event) { :not_existing_tracking_event } let(:logger_params) { { tracking_event: tracking_event } } - let(:error_message) { "#{tracking_event} is not a valid logger=" } + let(:error_message) { "#{tracking_event} is not a valid tracking_event=" } include_examples 'raises logger argument error' end - context 'with valid logger class' do - let(:logger_class) { 42 } - let(:logger_params) { { logger_class: logger_class, stdout: true } } - let(:error_message) { "#{logger_class} is not a valid logger=" } + context 'with invalid logger class' do + let(:custom_logger_instance) { 42 } + let(:logger_params) { { custom_logger: custom_logger_instance, stdout: true } } + let(:error_message) { "#{custom_logger_instance} is not a valid custom_logger=" } include_examples 'raises logger argument error' end diff --git a/spec/truemail/logger_spec.rb b/spec/truemail/logger_spec.rb index b2ea92e..14fad7a 100644 --- a/spec/truemail/logger_spec.rb +++ b/spec/truemail/logger_spec.rb @@ -1,19 +1,19 @@ # frozen_string_literal: true RSpec.describe Truemail::Logger do - subject(:logger_instance) { described_class.new(logger_class, event, stdout, file) } + subject(:logger_instance) { described_class.new(event, stdout, file, custom_logger) } - let(:logger_class) { ::Logger } let(:event) { :event } let(:stdout) { true } let(:file) { nil } + let(:custom_logger) { nil } describe '.new' do it 'creates event logger with settings' do - expect(logger_instance.logger_class).to eq(logger_class) expect(logger_instance.event).to eq(event) expect(logger_instance.stdout).to eq(stdout) expect(logger_instance.file).to eq(file) + expect(logger_instance.custom_logger).to be_nil expect(logger_instance).to be_an_instance_of(described_class) end end @@ -45,47 +45,179 @@ push_log end end + end + + describe '#create_logs' do + subject(:create_logs) { logger_instance.send(:create_logs, log_level, serialized_object) } + + let(:log_level) { 1 } + let(:serialized_object) { 'serialized_object' } + let(:output_expectation) { "INFO -- : #{serialized_object}" } + let(:custom_logger_instnace) { instance_double('SomeCustomLoggerInstance') } + + context 'when stdout configured' do + it 'print to stdout logger info' do + expect(custom_logger_instnace) + .not_to receive(:add) + .with(log_level) { |&block| expect(block.call).to eq(serialized_object) } + expect { create_logs }.to output(/#{output_expectation}/).to_stdout + end + end - describe '#create_logs' do - subject(:create_logs) { logger_instance.send(:create_logs, log_level, serialized_object) } + context 'when output file path configured' do + let(:stdout) { false } + let(:file) { Pathname(::File.expand_path('../support/tmp/log', ::File.dirname(__FILE__))) } - let(:log_level) { 1 } - let(:serialized_object) { 'serialized_object' } - let(:output_expectation) { "INFO -- : #{serialized_object}" } + after { ::FileUtils.rm_rf(file.dirname) } - context 'when stdout configured' do - it 'print to stdout logger info' do - expect { create_logs }.to output(/#{output_expectation}/).to_stdout + context 'when log file not exists' do + it 'creates file, add log' do + expect(custom_logger_instnace) + .not_to receive(:add) + .with(log_level) { |&block| expect(block.call).to eq(serialized_object) } + expect { create_logs }.to change(file, :exist?).from(false).to(true) + expect(file.read).to include(output_expectation) end end - context 'when output file path configured' do - let(:stdout) { false } - let(:file) { Pathname(::File.expand_path('../support/tmp/log', ::File.dirname(__FILE__))) } + context 'when log file exists' do + let(:file_context) { 'file_context' } + + before do + file.parent.mkpath + ::File.open(file, 'a+') { |data| data.puts file_context } + end + + it 'add log to exsiting file context' do + expect { create_logs }.not_to output(/#{output_expectation}/).to_stdout + expect(file.read).to include(file_context, output_expectation) + end + end + end + + context 'when custom logger instance configured' do + let(:custom_logger) { custom_logger_instnace } + + it 'sends truemail log context to custom logger instance' do + expect(custom_logger_instnace) + .to receive(:add) + .with(log_level) { |&block| expect(block.call).to eq(serialized_object) } + create_logs + end + end + end +end + +RSpec.describe Truemail::Logger::Builder do + specify { expect(described_class).to be < ::Struct } + + describe '.call' do + subject(:build_logger_instance) { described_class.call(default_settings, **logger_attributes) } + + let(:default_settings) { {} } + let(:custom_logger) { nil } + let(:logger_attributes) do + { + tracking_event: tracking_event, + stdout: stdout, + log_absolute_path: log_absolute_path, + custom_logger: custom_logger + } + end + + context 'when valid logger attributes' do + let(:tracking_event) { Truemail::Log::Event::TRACKING_EVENTS.keys.sample } - after { ::FileUtils.rm_rf(file.dirname) } + context 'when custom logger instance' do + let(:custom_logger) { instance_double('SomeCustomLoggerInstance', add: 42) } + let(:stdout) { nil } + let(:log_absolute_path) { nil } - context 'when log file not exists' do - it 'creates file, add log' do - expect { create_logs }.to change(file, :exist?).from(false).to(true) - expect(file.read).to include(output_expectation) - end + it 'builds truemail logger instance using custom logger instance' do + expect(Truemail::Logger) + .to receive(:new) + .with(tracking_event, stdout, log_absolute_path, custom_logger) + build_logger_instance end + end + + context 'when stdout output passed' do + let(:stdout) { true } + let(:log_absolute_path) { nil } + + it 'builds truemail logger instance with default logger targeted to stdout' do + expect(Truemail::Logger) + .to receive(:new) + .with(tracking_event, stdout, log_absolute_path, custom_logger) + build_logger_instance + end + end - context 'when log file exists' do - let(:file_context) { 'file_context' } + context 'when file output passed' do + let(:stdout) { false } + let(:log_absolute_path) { 'some_absolute_path' } - before do - file.parent.mkpath - ::File.open(file, 'a+') { |data| data.puts file_context } - end + it 'builds truemail logger instance with default logger targeted to file' do + expect(Truemail::Logger) + .to receive(:new) + .with(tracking_event, stdout, log_absolute_path, custom_logger) + build_logger_instance + end + end + + context 'when both stdout and file output passed' do + let(:stdout) { true } + let(:log_absolute_path) { 'some_absolute_path' } + + it 'builds truemail logger instance with default logger targeted to stdout and file' do + expect(Truemail::Logger) + .to receive(:new) + .with(tracking_event, stdout, log_absolute_path, custom_logger) + build_logger_instance + end + end + end - it 'add log to exsiting file context' do - expect { create_logs }.not_to output(/#{output_expectation}/).to_stdout - expect(file.read).to include(file_context, output_expectation) - end + context 'when invalid logger attributes' do + let(:tracking_event) { Truemail::Log::Event::TRACKING_EVENTS.keys.sample } + + shared_examples 'raises truemail argument error' do + specify 'raises truemail argument error' do + expect(Truemail::Logger) + .not_to receive(:new) + .with(tracking_event, stdout, log_absolute_path, custom_logger) + expect { build_logger_instance }.to raise_error( + Truemail::ArgumentError, + error_context + ) end end + + context 'when wrong tracking event name' do + let(:tracking_event) { 'wrong_tracking_event' } + let(:stdout) { nil } + let(:log_absolute_path) { nil } + let(:error_context) { "#{tracking_event} is not a valid tracking_event=" } + + include_examples 'raises truemail argument error' + end + + context 'when builtin logger output did not specified' do + let(:stdout) { nil } + let(:log_absolute_path) { nil } + let(:error_context) { '{:stdout=>nil, :log_absolute_path=>nil} is not a valid stdout=, log_absolute_path=' } + + include_examples 'raises truemail argument error' + end + + context 'when the custom logger instance has a different than stdlib logger interface' do + let(:custom_logger) { instance_double('SomeCustomLoggerInstance') } + let(:stdout) { nil } + let(:log_absolute_path) { nil } + let(:error_context) { "#{custom_logger} is not a valid custom_logger=" } + + include_examples 'raises truemail argument error' + end end end end