rails_semantic_logger 4.20.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +55 -98
  3. data/Rakefile +7 -4
  4. data/lib/rails_semantic_logger/action_controller/log_subscriber.rb +86 -16
  5. data/lib/rails_semantic_logger/action_mailer/log_subscriber.rb +36 -22
  6. data/lib/rails_semantic_logger/action_view/log_subscriber.rb +74 -40
  7. data/lib/rails_semantic_logger/active_job/log_subscriber.rb +216 -7
  8. data/lib/rails_semantic_logger/active_record/log_subscriber.rb +62 -160
  9. data/lib/rails_semantic_logger/appenders.rb +91 -0
  10. data/lib/rails_semantic_logger/engine.rb +47 -36
  11. data/lib/rails_semantic_logger/extensions/action_cable/tagged_logger_proxy.rb +44 -3
  12. data/lib/rails_semantic_logger/extensions/action_dispatch/debug_exceptions.rb +5 -14
  13. data/lib/rails_semantic_logger/extensions/active_job/logging.rb +2 -2
  14. data/lib/rails_semantic_logger/extensions/active_model_serializers/logging.rb +2 -2
  15. data/lib/rails_semantic_logger/extensions/active_support/logger.rb +24 -15
  16. data/lib/rails_semantic_logger/extensions/rails/server.rb +1 -1
  17. data/lib/rails_semantic_logger/extensions/sidekiq/sidekiq.rb +4 -4
  18. data/lib/rails_semantic_logger/options.rb +171 -20
  19. data/lib/rails_semantic_logger/rack/logger.rb +6 -13
  20. data/lib/rails_semantic_logger/sidekiq/defaults.rb +4 -2
  21. data/lib/rails_semantic_logger/sidekiq/job_logger.rb +13 -5
  22. data/lib/rails_semantic_logger/solid_queue/log_subscriber.rb +179 -0
  23. data/lib/rails_semantic_logger/version.rb +1 -1
  24. data/lib/rails_semantic_logger.rb +81 -26
  25. metadata +15 -21
  26. data/lib/rails_semantic_logger/delayed_job/plugin.rb +0 -11
  27. data/lib/rails_semantic_logger/extensions/active_support/log_subscriber.rb +0 -13
  28. data/lib/rails_semantic_logger/extensions/rack/server.rb +0 -12
  29. data/lib/rails_semantic_logger/extensions/rackup/server.rb +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8f7b94b3d6d4f231657586789b55ffe22c4a3d86e7f34c558b5c757a6e3d2af
4
- data.tar.gz: 72a661f99dae0261b5cd6c6dffe972bfee3903ea48dd3b16832d40aa747bc9c1
3
+ metadata.gz: 213d85a11e285b1353549f41b8a8c28068a33d3913e270d72eed94f6ee1d861c
4
+ data.tar.gz: 8233c4128fdc790e3cefedd3efc8d8c6b274393604527cc162dff97842227fea
5
5
  SHA512:
6
- metadata.gz: ae75985892a47040a291e94fed73756884aca9c74e21339b3d40fba964bad47b6ffa5237899cb34b05d7c8727e9a9ebe8967774b3693cc9d9371ef61d3696814
7
- data.tar.gz: 1601458b40ab94c03244b44aed2522f1c0a72f06076f9df46f42793de77f10ccff63d7f851be38a984eae9615fe7ca977141b50f473a282d9e1adf15e864665d
6
+ metadata.gz: 52330aaea8cf48aa21a62b8c5654163ce481382fa67bd6935c02d3deecfcef3a83a32b30c563941eb49404d86063ff2056778692781bd14a96174b55d2372f9f
7
+ data.tar.gz: 5da348c363745f1e3b7a2fc73c36221658f86dafcad53d5078990d9d204fe5bf2224a319a156395597a7916bb2a950b1c27974114bbee37662f388295999758a
data/README.md CHANGED
@@ -1,128 +1,85 @@
1
1
  # Rails Semantic Logger
2
2
  [![Gem Version](https://img.shields.io/gem/v/rails_semantic_logger.svg)](https://rubygems.org/gems/rails_semantic_logger) [![Build Status](https://github.com/reidmorrison/rails_semantic_logger/workflows/build/badge.svg)](https://github.com/reidmorrison/rails_semantic_logger/actions?query=workflow%3Abuild) [![Downloads](https://img.shields.io/gem/dt/rails_semantic_logger.svg)](https://rubygems.org/gems/rails_semantic_logger) [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](http://opensource.org/licenses/Apache-2.0) ![](https://img.shields.io/badge/status-Production%20Ready-blue.svg)
3
3
 
4
- Rails Semantic Logger replaces the Rails default logger with [Semantic Logger](https://logger.rocketjob.io/)
4
+ Rails Semantic Logger replaces the Rails default logger with [Semantic Logger](https://logger.rocketjob.io/), so that Rails, your application code, and many common gems all log through structured logging instead of plain text.
5
5
 
6
- When any large Rails application is deployed to production one of the first steps is to move to centralized logging, so that logs can be viewed and searched from a central location.
6
+ When any large Rails application is deployed to production one of the first steps is to move to centralized logging, so that logs can be viewed and searched from a central location. That quickly falls apart when consuming human readable text logs:
7
7
 
8
- Centralized logging quickly falls apart when trying to consume the current human readable log files:
9
- - Log entries often span multiple lines, resulting in unrelated log lines in the centralized logging system. For example, stack traces.
10
- - Complex Regular Expressions are needed to parse the text lines and make them machine readable. For example to build queries, or alerts that are looking for specific elements in the message.
11
- - Writing searches, alerts, or dashboards based on text logs is incredibly brittle, since a small change to the text logged can often break the parsing of those logs.
12
- - Every log entry often has a completely different format, making it difficult to make consistent searches against the data.
8
+ - Log entries often span multiple lines (for example, stack traces), so unrelated lines end up interleaved in the centralized system.
9
+ - Complex regular expressions are needed to parse the text into machine readable fields for queries and alerts.
10
+ - Searches, alerts, and dashboards built on text are brittle: a small change to the logged text breaks them.
11
+ - Every log entry has a different format, making consistent searches difficult.
13
12
 
14
- For these and many other reasons switching to structured logging, or logs in JSON format, in testing and production makes centralized logging incredibly powerful.
13
+ Switching to structured logging, or logs in JSON format, makes centralized logging in testing and production far more powerful. Rails Semantic Logger also collapses the several lines Rails normally logs per request into a single structured "Completed" line, while keeping every field (controller, action, status, durations, and so on) searchable.
15
14
 
16
- For example, adding these lines to `config/application.rb` and removing any other log overrides from other environments, will switch automatically to structured logging when running inside Kubernetes:
17
- ~~~ruby
18
- # Setup structured logging
19
- config.semantic_logger.application = "my_application"
20
- config.semantic_logger.environment = ENV["STACK_NAME"] || Rails.env
21
- config.log_level = ENV["LOG_LEVEL"] || :info
22
-
23
- # Switch to JSON Logging output to stdout when running on Kubernetes
24
- if ENV["LOG_TO_CONSOLE"] || ENV["KUBERNETES_SERVICE_HOST"]
25
- config.rails_semantic_logger.add_file_appender = false
26
- config.semantic_logger.add_appender(io: $stdout, formatter: :json)
27
- end
28
- ~~~
15
+ ## Installation
29
16
 
30
- Then configure the centralized logging system to tell it that the data is in JSON format, so that it will parse it for you into a hierarchy.
17
+ Add to your `Gemfile`:
31
18
 
32
- For example, the following will instruct [Observe](https://www.observeinc.com/) to parse the JSON data and create machine readable data from it:
33
19
  ~~~ruby
34
- interface "log", "log":log
35
-
36
- make_col event:parse_json(log)
37
-
38
- make_col
39
- time:parse_isotime(event.timestamp),
40
- application:string(event.application),
41
- environment:string(event.environment),
42
- duration:duration_ms(event.duration_ms),
43
- level:string(event.level),
44
- name:string(event.name),
45
- message:string(event.message),
46
- named_tags:event.named_tags,
47
- payload:event.payload,
48
- metric:string(event.metric),
49
- metric_amount:float64(event.metric_amount),
50
- tags:array(event.tags),
51
- exception:event.exception,
52
- host:string(event.host),
53
- pid:int64(event.pid),
54
- thread:string(event.thread),
55
- file:string(event.file),
56
- line:int64(event.line),
57
- dimensions:event.dimensions,
58
- backtrace:array(event.backtrace),
59
- level_index:int64(event.level_index)
60
-
61
- set_valid_from(time)
62
- drop_col timestamp, log, event, stream
63
- rename_col timestamp:time
20
+ gem "rails_semantic_logger"
21
+ gem "amazing_print" # optional, colorizes the structured payload in development
64
22
  ~~~
65
23
 
66
- Now queries can be built to drill down into each of these fields, including `payload` which is a nested object.
24
+ Then run `bundle install`. That is all that is required: Rails Semantic Logger automatically replaces the standard Rails logger and writes to the usual Rails log file.
67
25
 
68
- For example to find all failed Sidekiq job calls where the causing exception class name is `NoMethodError`:
69
- ~~~ruby
70
- filter environment = "uat2"
71
- filter level = "error"
72
- filter metric = "sidekiq.job.perform"
73
- filter (string(exception.cause.name) = "NoMethodError")
74
- ~~~
26
+ Remove the following gems if present, they conflict with or duplicate what this gem already does: `lograge`, `rails_stdout_logging`, `rails_12factor`.
75
27
 
76
- Example: create a dashboard showing the duration of all successful Sidekiq jobs:
77
- ~~~ruby
78
- filter environment = "production"
79
- filter level = "info"
80
- filter metric = "sidekiq.job.perform"
81
- timechart duration:avg(duration), group_by(name)
82
- ~~~
28
+ ## Out of the box
83
29
 
84
- Example: create a dashboard showing the queue latency of all Sidekiq jobs.
85
- The queue latency is the time between when the job was enqueued and when it was started:
86
- ~~~ruby
87
- filter environment = "production"
88
- filter level = "info"
89
- filter metric = "sidekiq.queue.latency"
90
- timechart latency:avg(metric_amount/1000), group_by(string(named_tags.queue))
91
- ~~~
30
+ With no configuration at all, Rails Semantic Logger:
92
31
 
93
- * http://github.com/reidmorrison/rails_semantic_logger
32
+ - Writes to `log/<environment>.log`, the same file Rails uses, colorized when Rails colorized logging is enabled.
33
+ - Logs to **standard out** when you run `rails server`, so you see requests in your terminal.
34
+ - Logs to **standard error** when you run `rails console`, so log lines do not get mixed up with command return values.
35
+ - Replaces the multi-line Rails request log with a single structured "Completed" line.
94
36
 
95
- ## Documentation
37
+ ## Configuring where logs go: the appenders block
96
38
 
97
- For complete documentation see: https://logger.rocketjob.io/rails
39
+ An **appender** is a destination for log output: a file, standard out, a centralized log service, and so on. Declare the appenders you want in a single block. **The method name says _when_ the appender is created; the arguments say _where_ it writes and _how_ it is formatted.**
98
40
 
99
- ## Upgrading to Semantic Logger V4.16 - Sidekiq Metrics Support
41
+ | Method | Created when… | Default destination |
42
+ |--------|---------------|---------------------|
43
+ | `add` | Always, during Rails initialization | (you must specify one) |
44
+ | `add_server` | Only when serving requests: `rails server`, a rack server, Sidekiq in server mode | `$stdout` |
45
+ | `add_console` | Only inside a `rails console` session | `$stderr` |
100
46
 
101
- Rails Semantic Logger now supports Sidekiq metrics.
102
- Below are the metrics that are now available when the JSON logging format is used:
103
- - `sidekiq.job.perform`
104
- - The duration of each Sidekiq job.
105
- - `duration` contains the time in milliseconds that the job took to run.
106
- - `sidekiq.queue.latency`
107
- - The time between when a Sidekiq job was enqueued and when it was started.
108
- - `metric_amount` contains the time in milliseconds that the job was waiting in the queue.
47
+ The arguments to all three are exactly the arguments to `SemanticLogger.add_appender`, so anything Semantic Logger can log to, any of these can declare.
109
48
 
110
- ## Upgrading to Semantic Logger v4.15 & V4.16 - Sidekiq Support
49
+ > **Important:** As soon as you declare **any** appender in this block, Rails Semantic Logger stops adding **all** of its automatic appenders (the default `log/<env>.log` file, the standard-out logger under `rails server`, and the standard-error logger in `rails console`). The block becomes the single source of truth for every destination.
111
50
 
112
- Rails Semantic Logger introduces direct support for Sidekiq v4, v5, v6, and v7.
113
- Please remove any previous custom patches or configurations to make Sidekiq work with Semantic Logger.
114
- To see the complete list of patches being made, and to contribute your own changes, see: [Sidekiq Patches](https://github.com/reidmorrison/rails_semantic_logger/blob/master/lib/rails_semantic_logger/extensions/sidekiq/sidekiq.rb)
51
+ A typical development setup, a color log file plus color to the screen while serving:
115
52
 
116
- ## Upgrading to Semantic Logger v4.4
53
+ ~~~ruby
54
+ config.rails_semantic_logger.appenders do |appenders|
55
+ appenders.add(file_name: "log/#{Rails.env}.log", formatter: :color)
56
+ appenders.add_server(formatter: :color) # → $stdout, only when serving
57
+ end
58
+ ~~~
117
59
 
118
- With some forking frameworks it is necessary to call `reopen` after the fork. With v4.4 the
119
- workaround for Ruby 2.5 crashes is no longer needed.
120
- I.e. Please remove the following line if being called anywhere:
60
+ On a container platform (Docker, Kubernetes, Heroku), log JSON to standard out and let the platform collect it:
121
61
 
122
62
  ~~~ruby
123
- SemanticLogger::Processor.instance.instance_variable_set(:@queue, Queue.new)
63
+ config.rails_semantic_logger.appenders do |appenders|
64
+ appenders.add(io: $stdout, formatter: :json)
65
+ end
124
66
  ~~~
125
67
 
68
+ Because declaring an appender replaces the default file appender, JSON to stdout becomes the only destination, exactly what a container platform wants. Once logs are emitted as structured JSON, a centralized logging system can parse each field, including the nested `payload` and any `metric` data, into a searchable hierarchy, so you can build searches, alerts, and dashboards against well-defined fields instead of brittle text matching.
69
+
70
+ See [Configuring appenders](https://logger.rocketjob.io/rails#configuring-where-logs-go-the-appenders-block) for the full guide, including formatters, third-party destinations, the [container platform recipe](https://logger.rocketjob.io/rails#production-on-a-container-platform-docker-kubernetes-heroku), tuning what Rails logs, and worked examples of querying the JSON.
71
+
72
+ ## Documentation
73
+
74
+ For complete documentation see: https://logger.rocketjob.io/rails
75
+
76
+ ## Upgrading
77
+
78
+ The way appenders (log destinations) are configured changed in v5. See the
79
+ [v4 to v5 migration guide](https://logger.rocketjob.io/rails#migrating-from-v4-to-v5) for the
80
+ before/after mapping, and [Migrating from earlier versions](https://logger.rocketjob.io/rails#migrating-from-earlier-versions)
81
+ for older releases.
82
+
126
83
  ## New Versions of Rails, etc.
127
84
 
128
85
  The primary purpose of the Rails Semantic Logger gem is to patch other gems, primarily Rails, to make them support structured logging though Semantic Logger.
@@ -136,7 +93,7 @@ Additionally, when new popular gems come out, we rely only the community to supp
136
93
 
137
94
  ## Supported Platforms
138
95
 
139
- 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).
96
+ For the complete list of supported Ruby and Rails versions, see the [Testing file](https://github.com/reidmorrison/rails_semantic_logger/blob/main/.github/workflows/ci.yml).
140
97
 
141
98
  ## Author
142
99
 
data/Rakefile CHANGED
@@ -10,8 +10,8 @@ task :gem do
10
10
  end
11
11
 
12
12
  task publish: :gem do
13
- # system "git tag -a v#{RailsSemanticLogger::VERSION} -m 'Tagging #{RailsSemanticLogger::VERSION}'"
14
- # system "git push --tags"
13
+ system "git tag -a v#{RailsSemanticLogger::VERSION} -m 'Tagging #{RailsSemanticLogger::VERSION}'"
14
+ system "git push --tags"
15
15
  system "gem push rails_semantic_logger-#{RailsSemanticLogger::VERSION}.gem"
16
16
  system "rm rails_semantic_logger-#{RailsSemanticLogger::VERSION}.gem"
17
17
  end
@@ -22,10 +22,13 @@ Rake::TestTask.new(:test) do |t|
22
22
  t.warning = false
23
23
  end
24
24
 
25
- # By default run tests against all appraisals
25
+ require "rubocop/rake_task"
26
+ RuboCop::RakeTask.new
27
+
28
+ # By default lint once, then run tests against all appraisals
26
29
  if !ENV["APPRAISAL_INITIALIZED"] && !ENV["TRAVIS"]
27
30
  require "appraisal"
28
- task default: :appraisal
31
+ task default: %i[rubocop appraisal]
29
32
  else
30
33
  task default: :test
31
34
  end
@@ -1,19 +1,39 @@
1
+ # This subscriber is a reimplementation of Rails' own ActionController::LogSubscriber that emits
2
+ # structured (message + payload) log entries instead of formatted text. When Rails changes its
3
+ # subscriber, those changes must be brought across here. Compare against the upstream source for
4
+ # each supported Rails version:
5
+ #
6
+ # Rails 8.1: https://github.com/rails/rails/blob/8-1-stable/actionpack/lib/action_controller/log_subscriber.rb
7
+ # Rails 8.0: https://github.com/rails/rails/blob/8-0-stable/actionpack/lib/action_controller/log_subscriber.rb
8
+ # Rails 7.2: https://github.com/rails/rails/blob/7-2-stable/actionpack/lib/action_controller/log_subscriber.rb
9
+ #
1
10
  module RailsSemanticLogger
2
11
  module ActionController
3
12
  class LogSubscriber < ActiveSupport::LogSubscriber
4
13
  INTERNAL_PARAMS = %w[controller action format _method only_path].freeze
5
14
 
15
+ class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new
16
+
6
17
  class << self
7
- attr_accessor :action_message_format
18
+ attr_accessor :action_message_format, :processing_log_level
8
19
  end
9
20
 
10
- # Log as debug to hide Processing messages in production
21
+ # Defaults to :debug so the Processing message is hidden in production. The engine raises it
22
+ # to :info when `config.rails_semantic_logger.processing` is true.
23
+ @processing_log_level = :debug
24
+
11
25
  def start_processing(event)
12
- controller_logger(event).debug { action_message("Processing", event.payload) }
26
+ controller_logger(event).send(self.class.processing_log_level) { action_message("Processing", event.payload) }
13
27
  end
14
28
 
15
29
  def process_action(event)
16
30
  controller_logger(event).info do
31
+ # `event.payload` is shared with every other subscriber on this notification, so we work on
32
+ # a copy. A shallow `dup` is sufficient: only mutate `payload` via top-level key reassignment
33
+ # (e.g. `payload[:format] = ...`) or by writing into a freshly-created hash (e.g. the `.except`
34
+ # result below). Never mutate a nested object that still belongs to the original payload
35
+ # (e.g. `payload[:foo][:bar] = ...` on an unduped key), or the change will leak back into the
36
+ # shared payload and corrupt what other subscribers see.
17
37
  payload = event.payload.dup
18
38
 
19
39
  # Unused, but needed for Devise 401 status code monkey patch to still work.
@@ -51,47 +71,93 @@ module RailsSemanticLogger
51
71
  payload[key] = payload[key].to_f.round(2) if key.to_s =~ /(.*)_runtime/
52
72
  end
53
73
 
54
- # Rails 6+ includes allocation count
55
- payload[:allocations] = event.allocations if event.respond_to?(:allocations)
74
+ payload[:allocations] = event.allocations
75
+ payload[:cpu_time] = event.cpu_time.round(2)
76
+ payload[:idle_time] = event.idle_time.round(2)
77
+ payload[:gc_time] = event.gc_time.round(2) if event.respond_to?(:gc_time)
56
78
 
57
79
  payload[:status_message] = ::Rack::Utils::HTTP_STATUS_CODES[payload[:status]] if payload[:status].present?
58
80
 
59
- # Causes excessive log output with Rails 5 RC1
60
81
  payload.delete(:headers)
61
- # Causes recursion in Rails 6.1.rc1
62
82
  payload.delete(:request)
63
83
  payload.delete(:response)
64
84
 
65
85
  {
66
86
  message: action_message("Completed", event.payload),
67
87
  duration: event.duration,
68
- payload: payload
88
+ payload: payload,
89
+ metric: "rails.controller.process_action"
69
90
  }
70
91
  end
71
92
  end
72
93
 
73
94
  def halted_callback(event)
74
- controller_logger(event).info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" }
95
+ controller_logger(event).info do
96
+ {
97
+ message: "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected",
98
+ metric: "rails.controller.halted_callback"
99
+ }
100
+ end
101
+ end
102
+
103
+ # Rails 8.1+ emits this event when an exception is handled by a `rescue_from` callback.
104
+ # On earlier Rails versions the event is never instrumented, so this handler is dormant.
105
+ def rescue_from_callback(event)
106
+ controller_logger(event).info do
107
+ exception = event.payload[:exception]
108
+ backtrace = exception.backtrace&.first
109
+ backtrace = backtrace&.delete_prefix("#{Rails.root}/") if defined?(Rails.root) && Rails.root
110
+
111
+ {
112
+ message: "rescue_from handled #{exception.class}",
113
+ payload: {
114
+ exception: exception.class.name,
115
+ exception_message: exception.message,
116
+ backtrace: backtrace
117
+ },
118
+ metric: "rails.controller.rescue_from_callback"
119
+ }
120
+ end
75
121
  end
76
122
 
77
123
  def send_file(event)
78
- controller_logger(event).info(message: "Sent file", payload: {path: event.payload[:path]}, duration: event.duration)
124
+ controller_logger(event).info(message: "Sent file",
125
+ payload: {path: event.payload[:path]},
126
+ duration: event.duration,
127
+ metric: "rails.controller.send_file")
79
128
  end
80
129
 
81
130
  def redirect_to(event)
82
- controller_logger(event).info(message: "Redirected to", payload: {location: event.payload[:location]})
131
+ payload = {location: event.payload[:location]}
132
+
133
+ # Rails 8.1+ optionally logs the source location of the redirect when
134
+ # ActionDispatch.verbose_redirect_logs is enabled.
135
+ if ActionDispatch.respond_to?(:verbose_redirect_logs) && ActionDispatch.verbose_redirect_logs
136
+ source = redirect_source_location
137
+ payload[:source] = source if source
138
+ end
139
+
140
+ controller_logger(event).info(message: "Redirected to", payload: payload, metric: "rails.controller.redirect_to")
83
141
  end
84
142
 
85
143
  def send_data(event)
86
144
  controller_logger(event).info(message: "Sent data",
87
145
  payload: {file_name: event.payload[:filename]},
88
- duration: event.duration)
146
+ duration: event.duration,
147
+ metric: "rails.controller.send_data")
89
148
  end
90
149
 
91
150
  def unpermitted_parameters(event)
92
151
  controller_logger(event).debug do
93
152
  unpermitted_keys = event.payload[:keys]
94
- "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(', ')}"
153
+ payload = {keys: unpermitted_keys}
154
+ # Rails includes the controller/action context alongside the rejected keys.
155
+ payload[:context] = event.payload[:context] if event.payload[:context]
156
+
157
+ {
158
+ message: "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(', ')}",
159
+ payload: payload
160
+ }
95
161
  end
96
162
  end
97
163
 
@@ -99,11 +165,10 @@ module RailsSemanticLogger
99
165
  expire_fragment expire_page write_page].each do |method|
100
166
  class_eval <<-METHOD, __FILE__, __LINE__ + 1
101
167
  def #{method}(event)
102
- # enable_fragment_cache_logging as of Rails 5
103
- return if ::ActionController::Base.respond_to?(:enable_fragment_cache_logging) && !::ActionController::Base.enable_fragment_cache_logging
168
+ return unless ::ActionController::Base.enable_fragment_cache_logging
104
169
  controller_logger(event).info do
105
170
  key_or_path = event.payload[:key] || event.payload[:path]
106
- {message: "#{method.to_s.humanize} \#{key_or_path}", duration: event.duration}
171
+ {message: "#{method.to_s.humanize} \#{key_or_path}", duration: event.duration, metric: "rails.controller.#{method.delete('?')}"}
107
172
  end
108
173
  end
109
174
  METHOD
@@ -127,6 +192,11 @@ module RailsSemanticLogger
127
192
  index ? path[0, index] : path
128
193
  end
129
194
 
195
+ # Rails 8.1+ BacktraceCleaner exposes #first_clean_frame for verbose redirect logging.
196
+ def redirect_source_location
197
+ backtrace_cleaner.first_clean_frame if backtrace_cleaner.respond_to?(:first_clean_frame)
198
+ end
199
+
130
200
  def action_message(message, payload)
131
201
  if self.class.action_message_format
132
202
  self.class.action_message_format.call(message, payload)
@@ -1,10 +1,23 @@
1
1
  require "active_support/log_subscriber"
2
2
  require "action_mailer"
3
3
 
4
+ # This subscriber is a reimplementation of Rails' own ActionMailer::LogSubscriber that emits
5
+ # structured (message + payload) log entries instead of formatted text. When Rails changes its
6
+ # subscriber, those changes must be brought across here. Compare against the upstream source for
7
+ # each supported Rails version:
8
+ #
9
+ # Rails 8.1: https://github.com/rails/rails/blob/8-1-stable/actionmailer/lib/action_mailer/log_subscriber.rb
10
+ # Rails 8.0: https://github.com/rails/rails/blob/8-0-stable/actionmailer/lib/action_mailer/log_subscriber.rb
11
+ # Rails 7.2: https://github.com/rails/rails/blob/7-2-stable/actionmailer/lib/action_mailer/log_subscriber.rb
12
+ #
4
13
  module RailsSemanticLogger
5
14
  module ActionMailer
6
15
  class LogSubscriber < ::ActiveSupport::LogSubscriber
7
16
  def deliver(event)
17
+ # Rails gates this event with `subscribe_log_level :deliver, :debug`, so the upstream
18
+ # subscriber only runs when the logger is at debug level (or lower). Match that here.
19
+ return unless logger.debug?
20
+
8
21
  ex = event.payload[:exception_object]
9
22
  message_id = event.payload[:message_id]
10
23
  duration = event.duration.round(1)
@@ -31,10 +44,14 @@ module RailsSemanticLogger
31
44
 
32
45
  # An email was generated.
33
46
  def process(event)
47
+ # Rails gates this event with `subscribe_log_level :process, :debug` and emits the message
48
+ # at debug level. Match both the gating and the level here.
49
+ return unless logger.debug?
50
+
34
51
  mailer = event.payload[:mailer]
35
52
  action = event.payload[:action]
36
53
  duration = event.duration.round(1)
37
- log_with_formatter event: event do |_fmt|
54
+ log_with_formatter event: event, level: :debug do |_fmt|
38
55
  {message: "#{mailer}##{action}: processed outbound mail in #{duration}ms"}
39
56
  end
40
57
  end
@@ -47,34 +64,30 @@ module RailsSemanticLogger
47
64
  @log_duration = log_duration
48
65
  end
49
66
 
50
- def mailer
51
- event.payload[:mailer]
52
- end
53
-
54
67
  def payload
68
+ p = event.payload
55
69
  {}.tap do |h|
56
70
  h[:event_name] = event.name
57
71
  h[:mailer] = mailer
58
72
  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]
73
+ h[:message_id] = p[:message_id]
74
+ h[:perform_deliveries] = p[:perform_deliveries]
75
+ h[:subject] = p[:subject]
76
+ h[:to] = p[:to]
77
+ h[:from] = p[:from]
78
+ h[:bcc] = p[:bcc]
79
+ h[:cc] = p[:cc]
66
80
  h[:date] = date
81
+ # Rails dumps the full encoded message at debug level via `debug { event.payload[:mail] }`.
82
+ # The `deliver` event is debug-gated, so include it here whenever it is present.
83
+ h[:mail] = p[:mail] if p[:mail]
67
84
  h[:duration] = event.duration.round(2) if log_duration?
68
85
  h[:args] = formatted_args
69
86
  end
70
87
  end
71
88
 
72
89
  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
- end
90
+ event.payload[:date].to_time.utc if event.payload[:date].respond_to?(:to_time)
78
91
  end
79
92
 
80
93
  private
@@ -90,11 +103,9 @@ module RailsSemanticLogger
90
103
  end
91
104
 
92
105
  def formatted_args
93
- if defined?(mailer.constantize.log_arguments?) && !mailer.constantize.log_arguments?
94
- ""
95
- elsif event.payload[:args].present?
96
- JSON.pretty_generate(event.payload[:args].map { |arg| format(arg) })
97
- end
106
+ return unless event.payload[:args].present?
107
+
108
+ JSON.pretty_generate(event.payload[:args].map { |arg| format(arg) })
98
109
  end
99
110
 
100
111
  def format(arg)
@@ -122,6 +133,9 @@ module RailsSemanticLogger
122
133
  def log_with_formatter(level: :info, **kw_args)
123
134
  fmt = EventFormatter.new(**kw_args)
124
135
  msg = yield fmt
136
+ # Emit a metric for every info/warn/error entry, named after the notification
137
+ # (e.g. "deliver.action_mailer" -> "rails.mailer.deliver"). Debug entries (process) are excluded.
138
+ msg[:metric] ||= "rails.mailer.#{kw_args[:event].name.split('.').first}" unless level == :debug
125
139
  logger.public_send(level, **msg, payload: fmt.payload)
126
140
  end
127
141