Skip to content

Commit 0f6da08

Browse files
committed
Reimplement TaggedLogging:
- ### Context The Tagged Logging functionality has been a source of a few issues over the years, especially when combined with the broadcasting feature. Initializating a Tagged Logger wasn't very intuitive: ```ruby logger = Logger.new(STDOUT) tagged_logger = ActiveSupport::TaggedLogging.new(logger) # Would expect the `tagged_logger` to be an instance of `AS::TaggedLogging` # but it's a clone of the `logger`. tagged_logger.formatter = ->(_, _, _, message) do { message: message } end # Modifies the formatter to output JSON formatted logs. # This breaks tagged logging. ``` I believe the main reason of those issues is because tagged logging is implemented at the wrong level. ### Solution I made a proposal on the Ruby logger upstream in ruby/logger#90 to help solve this but it hasn't been reviewed yet. So I thought about adding it here for now. The TL;DR is to decouple formatting and adding extra information to logs (which is what tagged logging does). ### Deprecation Since TaggedLogging will no longer access the formatter, there is a few things I'd like to deprecate (such as setting a default formatter https://github.com/rails/rails/blob/d68e43922bc11829c52ad9f736ad5549fc97631b/activesupport/lib/active_support/tagged_logging.rb#L124) but doing so in this PR would increase the size of the diff significantly and would maybe distract for PR reviews. Another thing that I believe should be deprecated is `ActiveSupport::TaggedLogging.new`. Adding tagging functionality to a logger should be done using a more ruby approach such as `logger.extend(AS::TaggedLogging)`. Fix rails#49757 Fix rails#49745 Fix rails#46084 Fix rails#44668 I made a propose on the Ruby logger upstream in ruby/logger#90, but it hasn't been reviewed it.
1 parent ca5132b commit 0f6da08

File tree

8 files changed

+310
-48
lines changed

8 files changed

+310
-48
lines changed

actioncable/lib/action_cable/connection/tagged_logger_proxy.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def add_tags(*tags)
2222

2323
def tag(logger, &block)
2424
if logger.respond_to?(:tagged)
25-
current_tags = tags - logger.formatter.current_tags
25+
current_tags = tags - logger.tag_processor.current_tags
2626
logger.tagged(*current_tags, &block)
2727
else
2828
yield

activejob/lib/active_job/logging.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def tag_logger(*tags, &block)
2929
end
3030

3131
def logger_tagged_by_active_job?
32-
logger.formatter.current_tags.include?("ActiveJob")
32+
logger.tag_processor.current_tags.include?("ActiveJob")
3333
end
3434
end
3535
end

activesupport/lib/active_support/broadcast_logger.rb

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require "active_support/tagged_logging"
4+
35
module ActiveSupport
46
# = Active Support Broadcast Logger
57
#
@@ -71,6 +73,40 @@ module ActiveSupport
7173
# logger.broadcast_to(MyLogger.new(STDOUT))
7274
# puts logger.broadcasts # => [MyLogger, MyLogger]
7375
# logger.loggable? # [true, true]
76+
#
77+
# ====== A note on tagging logs while using the Broadcast logger
78+
#
79+
# It is quite frequent to tag logs using the `ActiveSupport::TaggedLogging` module
80+
# while also broadcasting them (the default Rails.logger in development is
81+
# configured in such a way).
82+
# Tagging your logs can be done for the whole broadcast or for each sink independently.
83+
#
84+
# Tagging logs for the whole broadcast
85+
#
86+
# broadcast = BroadcastLogger.new(stdout_logger, file_logger)
87+
# broadcast.tagged("BMX") { broadcast.info("Hello world!") }
88+
#
89+
# Outputs: "[BMX] Hello world!" is written on both STDOUT and in the file.
90+
#
91+
# Tagging logs for a single logger
92+
#
93+
# stdout_logger.extend(ActiveSupport::TaggedLogging)
94+
# stdout_logger.push_tags("BMX")
95+
# broadcast = BroadcastLogger.new(stdout_logger, file_logger)
96+
# broadcast.info("Hello world!")
97+
#
98+
# Outputs: "[BMX] Hello world!" is written on STDOUT
99+
# Outputs: "Hello world!" is written in the file
100+
#
101+
# Adding tags for the whole broadcast and adding extra tags on a specific logger
102+
#
103+
# stdout_logger.extend(ActiveSupport::TaggedLogging)
104+
# stdout_logger.push_tags("BMX")
105+
# broadcast = BroadcastLogger.new(stdout_logger, file_logger)
106+
# broadcast.tagged("APP") { broadcast.info("Hello world!") }
107+
#
108+
# Outputs: "[APP][BMX] Hello world!" is written on STDOUT
109+
# Outputs: "[APP] Hello world!" is written in the file
74110
class BroadcastLogger
75111
include ActiveSupport::LoggerSilence
76112

@@ -80,6 +116,8 @@ class BroadcastLogger
80116
attr_accessor :progname
81117

82118
def initialize(*loggers)
119+
extend ActiveSupport::TaggedLogging
120+
83121
@broadcasts = []
84122
@progname = "Broadcast"
85123

@@ -114,32 +152,32 @@ def <<(message)
114152
end
115153

116154
def add(*args, &block)
117-
dispatch { |logger| logger.add(*args, &block) }
155+
dispatch_with_processors { |logger| logger.add(*args, &block) }
118156
end
119157
alias_method :log, :add
120158

121159
def debug(*args, &block)
122-
dispatch { |logger| logger.debug(*args, &block) }
160+
dispatch_with_processors { |logger| logger.debug(*args, &block) }
123161
end
124162

125163
def info(*args, &block)
126-
dispatch { |logger| logger.info(*args, &block) }
164+
dispatch_with_processors { |logger| logger.info(*args, &block) }
127165
end
128166

129167
def warn(*args, &block)
130-
dispatch { |logger| logger.warn(*args, &block) }
168+
dispatch_with_processors { |logger| logger.warn(*args, &block) }
131169
end
132170

133171
def error(*args, &block)
134-
dispatch { |logger| logger.error(*args, &block) }
172+
dispatch_with_processors { |logger| logger.error(*args, &block) }
135173
end
136174

137175
def fatal(*args, &block)
138-
dispatch { |logger| logger.fatal(*args, &block) }
176+
dispatch_with_processors { |logger| logger.fatal(*args, &block) }
139177
end
140178

141179
def unknown(*args, &block)
142-
dispatch { |logger| logger.unknown(*args, &block) }
180+
dispatch_with_processors { |logger| logger.unknown(*args, &block) }
143181
end
144182

145183
def formatter=(formatter)
@@ -218,7 +256,7 @@ def fatal!
218256
dispatch { |logger| logger.fatal! }
219257
end
220258

221-
def initialize_copy(other)
259+
def initialize_dup(other)
222260
@broadcasts = []
223261
@progname = other.progname.dup
224262
@formatter = other.formatter.dup
@@ -231,6 +269,17 @@ def dispatch(&block)
231269
@broadcasts.each { |logger| block.call(logger) }
232270
end
233271

272+
def dispatch_with_processors(&block)
273+
@broadcasts.each do |logger|
274+
logger.extend(LogProcessor) unless logger.is_a?(LogProcessor)
275+
logger.processors.unshift(processors)
276+
277+
block.call(logger)
278+
ensure
279+
logger.processors.shift(processors.count)
280+
end
281+
end
282+
234283
def method_missing(name, *args, **kwargs, &block)
235284
loggers = @broadcasts.select { |logger| logger.respond_to?(name) }
236285

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveSupport
4+
module LogProcessor # :nodoc:
5+
attr_accessor :processors
6+
7+
def self.extended(base)
8+
base.processors = []
9+
end
10+
11+
def initialize(*args, **kwargs)
12+
super
13+
14+
self.processors = []
15+
end
16+
17+
private
18+
def format_message(severity, datetime, progname, msg)
19+
processors.flatten.reverse_each do |processor|
20+
msg = processor.call(msg, self)
21+
end
22+
23+
super(severity, datetime, progname, msg)
24+
end
25+
end
26+
end

activesupport/lib/active_support/logger.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
require "active_support/logger_silence"
44
require "active_support/logger_thread_safe_level"
5+
require "active_support/log_processor"
56
require "logger"
67

78
module ActiveSupport
89
class Logger < ::Logger
910
include LoggerSilence
11+
include LogProcessor
1012

1113
# Returns true if the logger destination matches one of the sources
1214
#

activesupport/lib/active_support/tagged_logging.rb

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ module ActiveSupport
2828
# it easy to stamp log lines with subdomains, request ids, and anything else
2929
# to aid debugging of multi-user production applications.
3030
module TaggedLogging
31-
module Formatter # :nodoc:
32-
# This method is invoked when a log event occurs.
33-
def call(severity, timestamp, progname, msg)
34-
super(severity, timestamp, progname, tag_stack.format_message(msg))
31+
class TagProcessor # :nodoc:
32+
def call(msg, logger)
33+
if logger.formatter.nil?
34+
logger.formatter ||= Logger::SimpleFormatter.new
35+
end
36+
37+
tag_stack.format_message(msg)
3538
end
3639

3740
def tagged(*tags)
@@ -114,29 +117,39 @@ def self.extended(base)
114117
end
115118
end
116119

117-
def self.new(logger)
118-
logger = logger.clone
119-
120-
if logger.formatter
121-
logger.formatter = logger.formatter.clone
120+
def self.new(logger) # :nodoc:
121+
if logger.is_a?(TaggedLogging)
122+
logger.clone
122123
else
123-
# Ensure we set a default formatter so we aren't extending nil!
124-
logger.formatter = ActiveSupport::Logger::SimpleFormatter.new
124+
logger.extend(TaggedLogging)
125125
end
126+
end
127+
128+
def self.extended(base)
129+
base.tag_processor = TagProcessor.new
130+
base.extend(ActiveSupport::LogProcessor)
126131

127-
logger.formatter.extend Formatter
128-
logger.extend(self)
132+
base.processors << base.tag_processor
129133
end
130134

131-
delegate :push_tags, :pop_tags, :clear_tags!, to: :formatter
135+
def initialize_clone(_)
136+
self.tag_processor = TagProcessor.new
137+
self.processors = [tag_processor]
138+
139+
super
140+
end
141+
142+
delegate :push_tags, :pop_tags, :clear_tags!, to: :tag_processor
143+
attr_accessor :tag_processor
132144

133145
def tagged(*tags)
134146
if block_given?
135-
formatter.tagged(*tags) { yield self }
147+
tag_processor.tagged(*tags) { yield(self) }
136148
else
137-
logger = ActiveSupport::TaggedLogging.new(self)
138-
logger.formatter.extend LocalTagStorage
139-
logger.push_tags(*formatter.current_tags, *tags)
149+
logger = clone
150+
logger.tag_processor.extend(LocalTagStorage)
151+
logger.tag_processor.push_tags(*tag_processor.current_tags, *tags)
152+
140153
logger
141154
end
142155
end
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "abstract_unit"
4+
5+
class LogBroadcastAndTaggingTest < ActiveSupport::TestCase
6+
setup do
7+
@sink1 = StringIO.new
8+
@sink2 = StringIO.new
9+
@logger1 = Logger.new(@sink1, formatter: ActiveSupport::Logger::SimpleFormatter.new)
10+
@logger2 = Logger.new(@sink2, formatter: ActiveSupport::Logger::SimpleFormatter.new)
11+
12+
@broadcast = ActiveSupport::BroadcastLogger.new
13+
@broadcast.broadcast_to(@logger1, @logger2)
14+
end
15+
16+
test "tag logs for the whole broadcast with a block" do
17+
@broadcast.tagged("BMX") { @broadcast.info("Hello") }
18+
19+
assert_equal("[BMX] Hello\n", @sink1.string)
20+
assert_equal("[BMX] Hello\n", @sink2.string)
21+
end
22+
23+
test "tag logs for the whole broadcast without a block" do
24+
@broadcast.tagged("BMX").info("Hello")
25+
26+
assert_equal("[BMX] Hello\n", @sink1.string)
27+
assert_equal("[BMX] Hello\n", @sink2.string)
28+
29+
@sink1.reopen
30+
@sink2.reopen
31+
@broadcast.info("Hello")
32+
33+
assert_equal("Hello\n", @sink1.string)
34+
assert_equal("Hello\n", @sink2.string)
35+
end
36+
37+
test "tag logs only for one sink" do
38+
@logger1.extend(ActiveSupport::TaggedLogging)
39+
@logger1.push_tags("BMX")
40+
41+
@broadcast.info { "Hello" }
42+
43+
assert_equal("[BMX] Hello\n", @sink1.string)
44+
assert_equal("Hello\n", @sink2.string)
45+
end
46+
47+
test "tag logs for multiple sinks" do
48+
@logger1.extend(ActiveSupport::TaggedLogging)
49+
@logger1.push_tags("BMX")
50+
51+
@logger2.extend(ActiveSupport::TaggedLogging)
52+
@logger2.push_tags("APP")
53+
54+
@broadcast.info { "Hello" }
55+
56+
assert_equal("[BMX] Hello\n", @sink1.string)
57+
assert_equal("[APP] Hello\n", @sink2.string)
58+
end
59+
60+
test "tag logs for the whole broadcast and extra tags are added to one sink (block version)" do
61+
@logger1.extend(ActiveSupport::TaggedLogging)
62+
@logger1.push_tags("APP1")
63+
64+
@logger2.extend(ActiveSupport::TaggedLogging)
65+
@logger2.push_tags("APP2")
66+
67+
@broadcast.tagged("BMX") { @broadcast.info("Hello") }
68+
69+
assert_equal("[BMX] [APP1] Hello\n", @sink1.string)
70+
assert_equal("[BMX] [APP2] Hello\n", @sink2.string)
71+
end
72+
73+
test "tag logs for the whole broadcast and extra tags are added to one sink (non-block version)" do
74+
@logger1.extend(ActiveSupport::TaggedLogging)
75+
@logger1.push_tags("APP1")
76+
77+
@logger2.extend(ActiveSupport::TaggedLogging)
78+
@logger2.push_tags("APP2")
79+
80+
@broadcast.tagged("BMX").info("Hello")
81+
82+
assert_equal("[BMX] [APP1] Hello\n", @sink1.string)
83+
assert_equal("[BMX] [APP2] Hello\n", @sink2.string)
84+
85+
@sink1.reopen
86+
@sink2.reopen
87+
@broadcast.info("Hello")
88+
89+
assert_equal("[APP1] Hello\n", @sink1.string)
90+
assert_equal("[APP2] Hello\n", @sink2.string)
91+
end
92+
93+
test "can broadcast to another broadcast logger with tagging functionalities" do
94+
@sink3 = StringIO.new
95+
@sink4 = StringIO.new
96+
@logger3 = Logger.new(@sink3, formatter: ActiveSupport::Logger::SimpleFormatter.new)
97+
@logger4 = Logger.new(@sink4, formatter: ActiveSupport::Logger::SimpleFormatter.new)
98+
@broadcast2 = ActiveSupport::BroadcastLogger.new
99+
100+
@broadcast2.broadcast_to(@logger3, @logger4)
101+
@broadcast.broadcast_to(@broadcast2)
102+
103+
@broadcast2.push_tags("BROADCAST2")
104+
105+
@broadcast.tagged("BMX") { @broadcast.info("Hello") }
106+
107+
assert_equal("[BMX] Hello\n", @sink1.string)
108+
assert_equal("[BMX] Hello\n", @sink2.string)
109+
assert_equal("[BMX] [BROADCAST2] Hello\n", @sink3.string)
110+
assert_equal("[BMX] [BROADCAST2] Hello\n", @sink4.string)
111+
end
112+
113+
test "#silence works while broadcasting to tagged loggers" do
114+
my_logger = Class.new(::Logger) do
115+
include ActiveSupport::LoggerSilence
116+
end
117+
118+
logger1_io = StringIO.new
119+
logger2_io = StringIO.new
120+
121+
logger1 = my_logger.new(logger1_io).extend(ActiveSupport::TaggedLogging)
122+
logger2 = my_logger.new(logger2_io).extend(ActiveSupport::TaggedLogging)
123+
124+
broadcast = ActiveSupport::BroadcastLogger.new(logger1, logger2)
125+
126+
broadcast.tagged("TEST") do
127+
broadcast.silence do
128+
broadcast.info("Silenced")
129+
end
130+
131+
broadcast.info("Not silenced")
132+
end
133+
134+
assert_equal("[TEST] Not silenced\n", logger1_io.string)
135+
assert_equal("[TEST] Not silenced\n", logger2_io.string)
136+
end
137+
end

0 commit comments

Comments
 (0)