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.
- checksums.yaml +4 -4
- data/README.md +55 -98
- data/Rakefile +7 -4
- data/lib/rails_semantic_logger/action_controller/log_subscriber.rb +86 -16
- data/lib/rails_semantic_logger/action_mailer/log_subscriber.rb +36 -22
- data/lib/rails_semantic_logger/action_view/log_subscriber.rb +74 -40
- data/lib/rails_semantic_logger/active_job/log_subscriber.rb +216 -7
- data/lib/rails_semantic_logger/active_record/log_subscriber.rb +62 -160
- data/lib/rails_semantic_logger/appenders.rb +91 -0
- data/lib/rails_semantic_logger/engine.rb +47 -36
- data/lib/rails_semantic_logger/extensions/action_cable/tagged_logger_proxy.rb +44 -3
- data/lib/rails_semantic_logger/extensions/action_dispatch/debug_exceptions.rb +5 -14
- data/lib/rails_semantic_logger/extensions/active_job/logging.rb +2 -2
- data/lib/rails_semantic_logger/extensions/active_model_serializers/logging.rb +2 -2
- data/lib/rails_semantic_logger/extensions/active_support/logger.rb +24 -15
- data/lib/rails_semantic_logger/extensions/rails/server.rb +1 -1
- data/lib/rails_semantic_logger/extensions/sidekiq/sidekiq.rb +4 -4
- data/lib/rails_semantic_logger/options.rb +171 -20
- data/lib/rails_semantic_logger/rack/logger.rb +6 -13
- data/lib/rails_semantic_logger/sidekiq/defaults.rb +4 -2
- data/lib/rails_semantic_logger/sidekiq/job_logger.rb +13 -5
- data/lib/rails_semantic_logger/solid_queue/log_subscriber.rb +179 -0
- data/lib/rails_semantic_logger/version.rb +1 -1
- data/lib/rails_semantic_logger.rb +81 -26
- metadata +15 -21
- data/lib/rails_semantic_logger/delayed_job/plugin.rb +0 -11
- data/lib/rails_semantic_logger/extensions/active_support/log_subscriber.rb +0 -13
- data/lib/rails_semantic_logger/extensions/rack/server.rb +0 -12
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 213d85a11e285b1353549f41b8a8c28068a33d3913e270d72eed94f6ee1d861c
|
|
4
|
+
data.tar.gz: 8233c4128fdc790e3cefedd3efc8d8c6b274393604527cc162dff97842227fea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52330aaea8cf48aa21a62b8c5654163ce481382fa67bd6935c02d3deecfcef3a83a32b30c563941eb49404d86063ff2056778692781bd14a96174b55d2372f9f
|
|
7
|
+
data.tar.gz: 5da348c363745f1e3b7a2fc73c36221658f86dafcad53d5078990d9d204fe5bf2224a319a156395597a7916bb2a950b1c27974114bbee37662f388295999758a
|
data/README.md
CHANGED
|
@@ -1,128 +1,85 @@
|
|
|
1
1
|
# Rails Semantic Logger
|
|
2
2
|
[](https://rubygems.org/gems/rails_semantic_logger) [](https://github.com/reidmorrison/rails_semantic_logger/actions?query=workflow%3Abuild) [](https://rubygems.org/gems/rails_semantic_logger) [](http://opensource.org/licenses/Apache-2.0) 
|
|
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
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
37
|
+
## Configuring where logs go: the appenders block
|
|
96
38
|
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
#
|
|
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).
|
|
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
|
-
|
|
55
|
-
payload[:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
60
|
-
h[:perform_deliveries] =
|
|
61
|
-
h[:subject] =
|
|
62
|
-
h[:to] =
|
|
63
|
-
h[:from] =
|
|
64
|
-
h[:bcc] =
|
|
65
|
-
h[: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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|