rails_semantic_logger 4.6.1 → 4.12.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: '0816ddc6820dfc94fed6b737d023c2cfdc6e6df3019d34ff1e71c40a2a6ccb2f'
4
- data.tar.gz: b0c7f329c6e1ecf72687617155d994b6c37ef731d5166e30b76543a41e825efb
3
+ metadata.gz: 73a37ac27f2cf94d083cc75aa557a778f32e2de852b3be6447b072dc1da89047
4
+ data.tar.gz: 681f2145e71def6b336792fe3d581dfd89da6fd3ca471befcbd8ffa4f11abb50
5
5
  SHA512:
6
- metadata.gz: c42fadbc4b768c9f64f719e36cdf453bc80b88639fd8fe169704ae5b37375be8706b59d3ede6ef8a6eac090a0433be1fe514e90ea306f2f1288f026042e48b18
7
- data.tar.gz: f16dec6c5a4336c0d0a0a2862891ec2117cbf103257a759e8611376725167476bc090c2444a7011df7b4c4e323583f2d5392dd498dea72e03f23f3555c45bde8
6
+ metadata.gz: 8d299a14cb4c3eaf282e4bf502d8e60c2a36095b138c17969cf0a8f79b0431a731adbe8963ae2017bf429f12b7f42fe65d5c199a2c5f88e984a71d4605766bea
7
+ data.tar.gz: d837dc46c0dcd38b19dcf2a62c926c91ef1775542d55c8919d3a76db2e62764ab8675890238da78ab19b157f3b920e9d92257d5b2e347bb66d0c5a2bde60441f
data/README.md CHANGED
@@ -21,7 +21,7 @@ SemanticLogger::Processor.instance.instance_variable_set(:@queue, Queue.new)
21
21
 
22
22
  ## Supports
23
23
 
24
- For the complete list of supported Ruby and Rails versions, see the [Testing file](https://github.com/reidmorrison/rails_semantic_logger/blob/master/.travis.yml).
24
+ For the complete list of supported Ruby and Rails versions, see the [Testing file](https://github.com/reidmorrison/rails_semantic_logger/blob/master/.github/workflows/ci.yml).
25
25
 
26
26
  ## Author
27
27
 
@@ -14,12 +14,22 @@ module RailsSemanticLogger
14
14
 
15
15
  # Unused, but needed for Devise 401 status code monkey patch to still work.
16
16
  ::ActionController::Base.log_process_action(payload)
17
+
18
+ params = payload[:params]
17
19
 
18
- # According to PR https://github.com/reidmorrison/rails_semantic_logger/pull/37/files
19
- # payload[:params] is not always a Hash.
20
- payload[:params] = payload[:params].to_unsafe_h unless payload[:params].is_a?(Hash)
21
- payload[:params] = payload[:params].except(*INTERNAL_PARAMS)
22
- payload.delete(:params) if payload[:params].empty?
20
+ if params.kind_of?(Hash) || params.kind_of?(::ActionController::Parameters)
21
+ # According to PR https://github.com/reidmorrison/rails_semantic_logger/pull/37/files
22
+ # params is not always a Hash.
23
+ payload[:params] = params.to_unsafe_h unless params.is_a?(Hash)
24
+ payload[:params] = params.except(*INTERNAL_PARAMS)
25
+
26
+ if payload[:params].empty?
27
+ payload.delete(:params)
28
+ elsif params["file"]
29
+ # When logging to JSON the entire tempfile is logged, so convert it to a string.
30
+ payload[:params]["file"] = params["file"].inspect
31
+ end
32
+ end
23
33
 
24
34
  format = payload[:format]
25
35
  payload[:format] = format.to_s.upcase if format.is_a?(Symbol)
@@ -48,12 +58,6 @@ module RailsSemanticLogger
48
58
  payload.delete(:request)
49
59
  payload.delete(:response)
50
60
 
51
- params = payload[:params]
52
- if params
53
- # When logging to JSON the entire tempfile is logged, so convert it to a string.
54
- params["file"] = params["file"].inspect if params["file"]
55
- end
56
-
57
61
  {
58
62
  message: "Completed ##{payload[:action]}",
59
63
  duration: event.duration,
@@ -0,0 +1,135 @@
1
+ require "active_support/log_subscriber"
2
+ require "action_mailer"
3
+
4
+ module RailsSemanticLogger
5
+ module ActionMailer
6
+ class LogSubscriber < ::ActiveSupport::LogSubscriber
7
+ def deliver(event)
8
+ ex = event.payload[:exception_object]
9
+ message_id = event.payload[:message_id]
10
+ duration = event.duration.round(1)
11
+ if ex
12
+ log_with_formatter event: event, log_duration: true, level: :error do |fmt|
13
+ {
14
+ message: "Error delivering mail #{message_id} (#{duration}ms)",
15
+ exception: ex
16
+ }
17
+ end
18
+ else
19
+ message = begin
20
+ if event.payload[:perform_deliveries]
21
+ "Delivered mail #{message_id} (#{duration}ms)"
22
+ else
23
+ "Skipped delivery of mail #{message_id} as `perform_deliveries` is false"
24
+ end
25
+ end
26
+ log_with_formatter event: event, log_duration: true do |fmt|
27
+ { message: message }
28
+ end
29
+ end
30
+ end
31
+
32
+ # An email was generated.
33
+ def process(event)
34
+ mailer = event.payload[:mailer]
35
+ action = event.payload[:action]
36
+ duration = event.duration.round(1)
37
+ log_with_formatter event: event do |fmt|
38
+ { message: "#{mailer}##{action}: processed outbound mail in #{duration}ms" }
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ class EventFormatter
45
+ def initialize(event:, log_duration: false)
46
+ @event = event
47
+ @log_duration = log_duration
48
+ end
49
+
50
+ def mailer
51
+ event.payload[:mailer]
52
+ end
53
+
54
+ def payload
55
+ {}.tap do |h|
56
+ h[:event_name] = event.name
57
+ h[:mailer] = mailer
58
+ h[:action] = action
59
+ h[:message_id] = event.payload[:message_id]
60
+ h[:perform_deliveries] = event.payload[:perform_deliveries]
61
+ h[:subject] = event.payload[:subject]
62
+ h[:to] = event.payload[:to]
63
+ h[:from] = event.payload[:from]
64
+ h[:bcc] = event.payload[:bcc]
65
+ h[:cc] = event.payload[:cc]
66
+ h[:date] = date
67
+ h[:duration] = event.duration.round(2) if log_duration?
68
+ h[:args] = formatted_args
69
+ end
70
+ end
71
+
72
+ def date
73
+ if event.payload[:date].respond_to?(:to_time)
74
+ event.payload[:date].to_time.utc
75
+ elsif event.payload[:date].is_a?(String)
76
+ Time.parse(date).utc
77
+ else
78
+ nil
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ attr_reader :event
85
+
86
+ def mailer
87
+ event.payload[:mailer]
88
+ end
89
+
90
+ def action
91
+ event.payload[:action]
92
+ end
93
+
94
+ def formatted_args
95
+ if defined?(mailer.contantize.log_arguments?) && !mailer.contantize.log_arguments?
96
+ ""
97
+ else
98
+ JSON.pretty_generate(event.payload[:args].map { |arg| format(arg) }) if event.payload[:args].present?
99
+ end
100
+ end
101
+
102
+ def format(arg)
103
+ case arg
104
+ when Hash
105
+ arg.transform_values { |value| format(value) }
106
+ when Array
107
+ arg.map { |value| format(value) }
108
+ when GlobalID::Identification
109
+ begin
110
+ arg.to_global_id
111
+ rescue StandardError
112
+ arg
113
+ end
114
+ else
115
+ arg
116
+ end
117
+ end
118
+
119
+ def log_duration?
120
+ @log_duration
121
+ end
122
+ end
123
+
124
+ def log_with_formatter(level: :info, **kw_args)
125
+ fmt = EventFormatter.new(**kw_args)
126
+ msg = yield fmt
127
+ logger.public_send(level, **msg, payload: fmt.payload)
128
+ end
129
+
130
+ def logger
131
+ ::ActionMailer::Base.logger
132
+ end
133
+ end
134
+ end
135
+ end
@@ -198,7 +198,8 @@ module RailsSemanticLogger
198
198
  alias bind_values bind_values_v5_0_3
199
199
  alias render_bind render_bind_v5_0_3
200
200
  alias type_casted_binds type_casted_binds_v5_0_3
201
- elsif Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR > 0 # ~> 6.1.0
201
+ elsif (Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR > 0) || # ~> 6.1.0
202
+ Rails::VERSION::MAJOR == 7
202
203
  alias bind_values bind_values_v6_1
203
204
  alias render_bind render_bind_v6_1
204
205
  alias type_casted_binds type_casted_binds_v5_1_5
@@ -1,6 +1,4 @@
1
1
  require "rails"
2
- require "action_controller/log_subscriber"
3
- require "action_view/log_subscriber"
4
2
  require "rails_semantic_logger/options"
5
3
 
6
4
  module RailsSemanticLogger
@@ -55,16 +53,16 @@ module RailsSemanticLogger
55
53
  formatter = {color: {ap: ap_options}} if (formatter == :default) && (config.colorize_logging != false)
56
54
 
57
55
  # Set internal logger to log to file only, in case another appender experiences errors during writes
58
- appender = SemanticLogger::Appender::File.new(
59
- file_name: path,
60
- level: config.log_level,
61
- formatter: formatter
62
- )
56
+ appender = SemanticLogger::Appender::File.new(path, formatter: formatter)
63
57
  appender.name = "SemanticLogger"
64
58
  SemanticLogger::Processor.logger = appender
65
59
 
66
60
  # Check for previous file or stdout loggers
67
- SemanticLogger.appenders.each { |app| app.formatter = formatter if app.is_a?(SemanticLogger::Appender::File) }
61
+ SemanticLogger.appenders.each do |app|
62
+ next unless app.is_a?(SemanticLogger::Appender::File) || app.is_a?(SemanticLogger::Appender::IO)
63
+
64
+ app.formatter = formatter
65
+ end
68
66
  SemanticLogger.add_appender(file_name: path, formatter: formatter, filter: config.rails_semantic_logger.filter)
69
67
  end
70
68
 
@@ -73,8 +71,8 @@ module RailsSemanticLogger
73
71
  # If not able to log to file, log to standard error with warning level only
74
72
  SemanticLogger.default_level = :warn
75
73
 
76
- SemanticLogger::Processor.logger = SemanticLogger::Appender::File.new(io: STDERR)
77
- SemanticLogger.add_appender(io: STDERR)
74
+ SemanticLogger::Processor.logger = SemanticLogger::Appender::IO.new($stderr)
75
+ SemanticLogger.add_appender(io: $stderr)
78
76
 
79
77
  logger = SemanticLogger[Rails]
80
78
  logger.warn(
@@ -107,13 +105,20 @@ module RailsSemanticLogger
107
105
  Mongo::Logger.logger = SemanticLogger[Mongo] if defined?(Mongo::Logger)
108
106
 
109
107
  # Replace the Resque Logger
110
- Resque.logger = SemanticLogger[Resque] if defined?(Resque) && Resque.respond_to?(:logger)
108
+ Resque.logger = SemanticLogger[Resque] if defined?(Resque) && Resque.respond_to?(:logger=)
111
109
 
112
110
  # Replace the Sidekiq logger
113
- Sidekiq.logger = SemanticLogger[Sidekiq] if defined?(Sidekiq)
111
+ if defined?(Sidekiq)
112
+ if Sidekiq.respond_to?(:logger=)
113
+ Sidekiq.logger = SemanticLogger[Sidekiq]
114
+ elsif Sidekiq::VERSION[0..1] == '7.'
115
+ method = Sidekiq.server? ? :configure_server : :configure_client
116
+ Sidekiq.public_send(method) { |cfg| cfg.logger = SemanticLogger[Sidekiq] }
117
+ end
118
+ end
114
119
 
115
120
  # Replace the Sidetiq logger
116
- Sidetiq.logger = SemanticLogger[Sidetiq] if defined?(Sidetiq)
121
+ Sidetiq.logger = SemanticLogger[Sidetiq] if defined?(Sidetiq) && Sidetiq.respond_to?(:logger=)
117
122
 
118
123
  # Replace the DelayedJob logger
119
124
  if defined?(Delayed::Worker)
@@ -123,6 +128,9 @@ module RailsSemanticLogger
123
128
 
124
129
  # Replace the Bugsnag logger
125
130
  Bugsnag.configure { |config| config.logger = SemanticLogger[Bugsnag] } if defined?(Bugsnag)
131
+
132
+ # Set the IOStreams PGP logger
133
+ IOStreams::Pgp.logger = SemanticLogger["IOStreams::Pgp"] if defined?(IOStreams)
126
134
  end
127
135
 
128
136
  # After any initializers run, but after the gems have been loaded
@@ -182,19 +190,38 @@ module RailsSemanticLogger
182
190
  end
183
191
 
184
192
  # Action View
185
- RailsSemanticLogger::ActionView::LogSubscriber.rendered_log_level = :info if config.rails_semantic_logger.rendered
186
- RailsSemanticLogger.swap_subscriber(
187
- ::ActionView::LogSubscriber,
188
- RailsSemanticLogger::ActionView::LogSubscriber,
189
- :action_view
190
- )
193
+ if defined?(::ActionView)
194
+ require "action_view/log_subscriber"
195
+
196
+ RailsSemanticLogger::ActionView::LogSubscriber.rendered_log_level = :info if config.rails_semantic_logger.rendered
197
+ RailsSemanticLogger.swap_subscriber(
198
+ ::ActionView::LogSubscriber,
199
+ RailsSemanticLogger::ActionView::LogSubscriber,
200
+ :action_view
201
+ )
202
+ end
191
203
 
192
204
  # Action Controller
193
- RailsSemanticLogger.swap_subscriber(
194
- ::ActionController::LogSubscriber,
195
- RailsSemanticLogger::ActionController::LogSubscriber,
196
- :action_controller
197
- )
205
+ if defined?(::ActionController)
206
+ require "action_controller/log_subscriber"
207
+
208
+ RailsSemanticLogger.swap_subscriber(
209
+ ::ActionController::LogSubscriber,
210
+ RailsSemanticLogger::ActionController::LogSubscriber,
211
+ :action_controller
212
+ )
213
+ end
214
+
215
+ # Action Mailer
216
+ if defined?(::ActionMailer)
217
+ require "action_mailer/log_subscriber"
218
+
219
+ RailsSemanticLogger.swap_subscriber(
220
+ ::ActionMailer::LogSubscriber,
221
+ RailsSemanticLogger::ActionMailer::LogSubscriber,
222
+ :action_mailer
223
+ )
224
+ end
198
225
  end
199
226
 
200
227
  #
@@ -218,7 +245,10 @@ module RailsSemanticLogger
218
245
  console do |_app|
219
246
  # Don't use a background thread for logging
220
247
  SemanticLogger.sync!
221
- SemanticLogger.add_appender(io: STDERR, formatter: :color)
248
+ # Add a stderr logger when running inside a Rails console unless one has already been added.
249
+ if config.rails_semantic_logger.console_logger && !SemanticLogger.appenders.console_output?
250
+ SemanticLogger.add_appender(io: STDERR, formatter: :color)
251
+ end
222
252
 
223
253
  # Include method names on log entries in the console
224
254
  SemanticLogger.backtrace_level = SemanticLogger.default_level
@@ -1,4 +1,4 @@
1
- ActionCable::Connection::TaggedLoggerProxy
1
+ require "action_cable/connection/tagged_logger_proxy"
2
2
 
3
3
  module ActionCable
4
4
  module Connection
@@ -1,5 +1,6 @@
1
1
  # Log actual exceptions, not a string representation
2
- ActionController::Live
2
+ require "action_controller"
3
+
3
4
  module ActionController
4
5
  module Live
5
6
  undef_method :log_error
@@ -1,5 +1,6 @@
1
1
  # Log actual exceptions, not a string representation
2
- ActionDispatch::DebugExceptions
2
+ require "action_dispatch"
3
+
3
4
  module ActionDispatch
4
5
  class DebugExceptions
5
6
  private
@@ -1,5 +1,5 @@
1
1
  # Log actual exceptions, not a string representation
2
- ActionView::StreamingTemplateRenderer
2
+ require "action_view/renderer/streaming_template_renderer"
3
3
 
4
4
  module ActionView
5
5
  class StreamingTemplateRenderer
@@ -7,6 +7,7 @@ module ActiveJob
7
7
 
8
8
  private
9
9
 
10
+ undef_method :tag_logger
10
11
  def tag_logger(*tags, &block)
11
12
  logger.tagged(*tags, &block)
12
13
  end
@@ -1,13 +1,15 @@
1
1
  # Patch the Rails::Server log_to_stdout so that it logs via SemanticLogger
2
- Rails::Server
2
+ require "rails"
3
+
3
4
  module Rails
4
5
  class Server
5
6
  private
6
-
7
+
8
+ undef_method :log_to_stdout if method_defined?(:log_to_stdout)
7
9
  def log_to_stdout
8
10
  wrapped_app # touch the app so the logger is set up
9
11
 
10
- SemanticLogger.add_appender(io: $stdout, formatter: :color)
12
+ SemanticLogger.add_appender(io: $stdout, formatter: :color) unless SemanticLogger.appenders.console_output?
11
13
  end
12
14
  end
13
15
  end
@@ -40,7 +40,7 @@ module RailsSemanticLogger
40
40
  # When Semantic Logger fails to log to an appender it logs the error to an
41
41
  # internal logger, which by default writes to STDERR.
42
42
  # Example, change the default internal logger to log to stdout:
43
- # SemanticLogger::Processor.logger = SemanticLogger::Appender::File.new(io: STDOUT, level: :warn)
43
+ # SemanticLogger::Processor.logger = SemanticLogger::Appender::IO.new($stdout, level: :warn)
44
44
  #
45
45
  # config.rails_semantic_logger.add_file_appender = true
46
46
  #
@@ -48,6 +48,10 @@ module RailsSemanticLogger
48
48
  #
49
49
  # config.rails_semantic_logger.quiet_assets = false
50
50
  #
51
+ # * Disable automatic logging to stderr when running a Rails console.
52
+ #
53
+ # config.rails_semantic_logger.console_logger = false
54
+ #
51
55
  # * Override the output format for the primary Rails log file.
52
56
  #
53
57
  # Valid options:
@@ -98,7 +102,7 @@ module RailsSemanticLogger
98
102
  # config.rails_semantic_logger.named_tags = nil
99
103
  class Options
100
104
  attr_accessor :semantic, :started, :processing, :rendered, :ap_options, :add_file_appender,
101
- :quiet_assets, :format, :named_tags, :filter
105
+ :quiet_assets, :format, :named_tags, :filter, :console_logger
102
106
 
103
107
  # Setup default values
104
108
  def initialize
@@ -112,6 +116,7 @@ module RailsSemanticLogger
112
116
  @format = :default
113
117
  @named_tags = nil
114
118
  @filter = nil
119
+ @console_logger = true
115
120
  end
116
121
  end
117
122
  end
@@ -35,16 +35,18 @@ module RailsSemanticLogger
35
35
  @started_request_log_level = :debug
36
36
 
37
37
  def call_app(request, env)
38
- instrumenter = ActiveSupport::Notifications.instrumenter
39
- instrumenter.start "request.action_dispatch", request: request
38
+ instrumenter = ActiveSupport::Notifications.instrumenter
39
+ instrumenter_state = instrumenter.start "request.action_dispatch", request: request
40
+ instrumenter_finish = -> () {
41
+ instrumenter.finish_with_state(instrumenter_state, "request.action_dispatch", request: request)
42
+ }
40
43
 
41
44
  logger.send(self.class.started_request_log_level) { started_request_message(request) }
42
-
43
45
  status, headers, body = @app.call(env)
44
- body = ::Rack::BodyProxy.new(body) { finish(request) }
46
+ body = ::Rack::BodyProxy.new(body, &instrumenter_finish)
45
47
  [status, headers, body]
46
48
  rescue Exception
47
- finish(request)
49
+ instrumenter_finish.call
48
50
  raise
49
51
  end
50
52
 
@@ -90,11 +92,6 @@ module RailsSemanticLogger
90
92
  tagged
91
93
  end
92
94
 
93
- def finish(request)
94
- instrumenter = ActiveSupport::Notifications.instrumenter
95
- instrumenter.finish "request.action_dispatch", request: request
96
- end
97
-
98
95
  def logger
99
96
  self.class.logger
100
97
  end
@@ -1,3 +1,3 @@
1
1
  module RailsSemanticLogger
2
- VERSION = "4.6.1".freeze
2
+ VERSION = "4.12.0".freeze
3
3
  end
@@ -6,6 +6,9 @@ module RailsSemanticLogger
6
6
  module ActionController
7
7
  autoload :LogSubscriber, "rails_semantic_logger/action_controller/log_subscriber"
8
8
  end
9
+ module ActionMailer
10
+ autoload :LogSubscriber, "rails_semantic_logger/action_mailer/log_subscriber"
11
+ end
9
12
  module ActionView
10
13
  autoload :LogSubscriber, "rails_semantic_logger/action_view/log_subscriber"
11
14
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_semantic_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.6.1
4
+ version: 4.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-15 00:00:00.000000000 Z
11
+ date: 2023-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -30,28 +30,28 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '3.2'
33
+ version: '5.1'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '3.2'
40
+ version: '5.1'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: semantic_logger
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '4.8'
47
+ version: '4.13'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '4.8'
54
+ version: '4.13'
55
55
  description:
56
56
  email:
57
57
  executables: []
@@ -63,6 +63,7 @@ files:
63
63
  - Rakefile
64
64
  - lib/rails_semantic_logger.rb
65
65
  - lib/rails_semantic_logger/action_controller/log_subscriber.rb
66
+ - lib/rails_semantic_logger/action_mailer/log_subscriber.rb
66
67
  - lib/rails_semantic_logger/action_view/log_subscriber.rb
67
68
  - lib/rails_semantic_logger/active_job/log_subscriber.rb
68
69
  - lib/rails_semantic_logger/active_record/log_subscriber.rb
@@ -85,7 +86,11 @@ files:
85
86
  homepage: https://logger.rocketjob.io
86
87
  licenses:
87
88
  - Apache-2.0
88
- metadata: {}
89
+ metadata:
90
+ bug_tracker_uri: https://github.com/reidmorrison/rails_semantic_logger/issues
91
+ documentation_uri: https://logger.rocketjob.io
92
+ source_code_uri: https://github.com/reidmorrison/rails_semantic_logger/tree/4.12.0
93
+ rubygems_mfa_required: 'true'
89
94
  post_install_message:
90
95
  rdoc_options: []
91
96
  require_paths:
@@ -101,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
106
  - !ruby/object:Gem::Version
102
107
  version: '0'
103
108
  requirements: []
104
- rubygems_version: 3.2.22
109
+ rubygems_version: 3.4.9
105
110
  signing_key:
106
111
  specification_version: 4
107
112
  summary: Feature rich logging framework that replaces the Rails logger.