From f86529ec251d8beb6b17b4042d7875fb104e285f Mon Sep 17 00:00:00 2001 From: Edouard CHIN Date: Fri, 30 Jun 2023 02:14:28 +0200 Subject: [PATCH] Refactor tagged logging and add a public API for broadcasting logs: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Opening this PR to get some first impression and feedback and see if that’s the path we want to take. ## Context While working on https://github.com/rails/rails/pull/44695, I realised that Broadcasting was still a private API, although it’s commonly used. Rafael mentioned that making it public would require some refactor because of the original implementation which was hard to understand and maintain. TaggedLogging is another piece of this PR, while it’s not related to broadcasting, both features combined were a source of issues and confusion (see #38850, #27792, #45854 and some more). Broadcasting and tagged logging were a bit entangled and I felt it would be easier to have the bigger picture in a single PR. TaggedLogging is public and the new implementation doesn’t introduce any breaking change. Broadcasting is in a grey zone, it’s not referenced in our docs but I saw it often in apps and libraries. This refactor would break them. Happy to revisit that and find a way to make it compatible. ## Implementation The changes would make a lot of diff chunks, so to make it easier to review I opted to not modify the original files and free the constant name (Logger, TaggedLogging) for the new implementation that are inside new files. All code in this PR is new and uses code from the previous implementation that don’t appear in the diff. The goal would be to copy/paste the required code at the end of the review process. --------------- ### Changing how broadcasting works: Broadcasting in a nutshell worked by “transforming” an existing logger into a broadcasted one. The logger would then be responsible to log and format its own messages as well as passing the message along to other logger it broadcasts to. The problem with this approach was the following: - Heavy use of metaprogramming. - Accessing the loggers in the broadcast wasn’t possible. Removing a logger from the broadcast either. - More importantly, modifying the main logger (the one that broadcasts logs to the others) wasn’t possible and the main source of misunderstanding. ```ruby logger = Logger.new(STDOUT) stderr_logger = Logger.new(STDER)) logger.extend(AS::Logger.broadcast(stderr_logger)) logger.level = DEBUG # This modifies the level on all other loggers logger.formatter = … # Modified the formatter on all other loggers ``` -> New approach To keep the contract unchanged on what Rails.logger returns, the new implementation is still a subclass of Logger. The difference is that now the broadcast logger just delegate al methods to all the other loggers it’s broadcasting to. It’s simple and boring and it’s now just an array that gets iterated over. Now, users can access all loggers inside the broadcast and modify them on the fly. They can also remove any logger from the broadcast at any time. ```ruby # Before stdout_logger = Logger.new(STDOUT) stderr_logger = Logger.new(STDER) file_logger = Logger.new(“development.log”) stdout_logger.extend(AS::Logger.broadcast(stderr_logger)) stdout_logger.extend(AS::Logger.broadcast(file_logger)) # After broadcast = BroadcastLogger.new broadcast.broadcast_to(stdout_logger, stderr_logger, file_logger) ``` I also think that now, it should be more clear for users that the broadcast sole job is to pass everything to the whole loggers in the broadcast. So there should be no surprise that all loggers in the broadcast get their level modified when they call `broadcast.level = DEBUG` . It’s also easier to wrap your head around more complex setup such as broadcasting logs to another broadcast: `broadcast.broadcast_to(stdout_logger, other_broadcast)` ### Changing TaggedLogging Tagged logging is painful to implement because there is basically no good way to hook into the vanilla logger code. The easiest is to hook on the formatter but IMHO this is implemented at the wrong level. Adding tags on the formatter means: - Monkeypatching the formatter on the logger. With the broadcasting feature, that meant modifying all formatters on all loggers. - From its name, I would assume that a formatter job is just to format. Not add modify the logs and add extra information. What I felt was missing was an object responsible to process the logs just before it gets formatted. So I implemented a “LogProcessor” which seats just after the user pass a log, but before it gets formatted. I thought it is a good addition that would allow to have multiple processors in the case users or libraries need to pass their logs into multiple processors. --- activesupport/lib/active_support.rb | 4 +- .../lib/active_support/broadcast_logger.rb | 195 ++++++++++++++++++ .../lib/active_support/new_logger.rb | 67 ++++++ .../lib/active_support/new_tagged_logging.rb | 137 ++++++++++++ activesupport/test/broadcast_logger_test.rb | 42 ++++ .../test/log_broadcast_and_tagging_test.rb | 77 +++++++ activesupport/test/tagged_logging_test.rb | 52 +++++ 7 files changed, 573 insertions(+), 1 deletion(-) create mode 100644 activesupport/lib/active_support/broadcast_logger.rb create mode 100644 activesupport/lib/active_support/new_logger.rb create mode 100644 activesupport/lib/active_support/new_tagged_logging.rb create mode 100644 activesupport/test/log_broadcast_and_tagging_test.rb diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 203a234c2d01f..36d40b0dc9a73 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -28,6 +28,8 @@ require "active_support/version" require "active_support/deprecator" require "active_support/logger" +require "active_support/new_logger" +require "active_support/broadcast_logger" require "active_support/lazy_load_hooks" require "active_support/core_ext/date_and_time/compatibility" @@ -79,7 +81,7 @@ module ActiveSupport autoload :OrderedOptions autoload :StringInquirer autoload :EnvironmentInquirer - autoload :TaggedLogging + autoload :TaggedLogging, "active_support/new_tagged_logging" autoload :XmlMini autoload :ArrayInquirer end diff --git a/activesupport/lib/active_support/broadcast_logger.rb b/activesupport/lib/active_support/broadcast_logger.rb new file mode 100644 index 0000000000000..067fe7664a9d4 --- /dev/null +++ b/activesupport/lib/active_support/broadcast_logger.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true + +module ActiveSupport + # The Broadcast logger is a logger used to write messages to multiple IO. It is commonly used + # in development to display messages on STDOUT and also write them to a file (development.log). + # With the Broadcast logger, you can broadcast your logs to a unlimited number of sinks. + # + # The BroadcastLogger acts as a standard logger and all methods you are used to are available. + # However, all the methods on this logger will propagate and be delegated to the other loggers + # that are part of the broadcast. + # + # Broadcasting your logs. + # + # stdout_logger = Logger.new(STDOUT) + # file_logger = Logger.new("development.log") + # broadcast = BroadcastLogger.new + # broadcast.broadcast_to(stdout_logger, file_logger) + # + # broadcast.info("Hello world!") # Writes the log to STDOUT and the development.log file. + # + # Modifying the log level to all broadcasted loggers. + # + # stdout_logger = Logger.new(STDOUT) + # file_logger = Logger.new("development.log") + # broadcast = BroadcastLogger.new + # broadcast.broadcast_to(stdout_logger, file_logger) + # + # broadcast.level = Logger::FATAL # Modify the log level for the whole broadcast. + # + # Stop broadcasting log to a sink. + # + # stdout_logger = Logger.new(STDOUT) + # file_logger = Logger.new("development.log") + # broadcast = BroadcastLogger.new + # broadcast.broadcast_to(stdout_logger, file_logger) + # broadcast.info("Hello world!") # Writes the log to STDOUT and the development.log file. + # + # broadcast.stop_broadcasting_to(file_logger) + # broadcast.info("Hello world!") # Writes the log *only* to STDOUT. + # + # At least one sink has to be part of the broadcast. Otherwise, your logs will not + # be written anywhere. For instance: + # + # broadcast = BroadcastLogger.new + # broadcast.info("Hello world") # The log message will appear nowhere. + # + # ====== A note on tagging logs while using the Broadcast logger + # + # It is quite frequent to tag logs using the `ActiveSupport::TaggedLogging` module + # while also broadcasting them (the default Rails.logger in development is + # configured in such a way). + # Tagging your logs can be done for the whole broadcast or for each sink independently. + # + # Tagging logs for the whole broadcast + # + # broadcast = BroadcastLogger.new.extend(ActiveSupport::TaggedLogging) + # broadcast.broadcast_to(stdout_logger, file_logger) + # broadcast.tagged("BMX") { broadcast.info("Hello world!") } + # + # Outputs: "[BMX] Hello world!" is written on both STDOUT and in the file. + # + # Tagging logs for a single logger + # + # stdout_logger.extend(ActiveSupport::TaggedLogging) + # stdout_logger.push_tags("BMX") + # broadcast = BroadcastLogger.new + # broadcast.broadcast_to(stdout_logger, file_logger) + # broadcast.info("Hello world!") + # + # Outputs: "[BMX] Hello world!" is written on STDOUT + # Outputs: "Hello world!" is written in the file + # + # Adding tags for the whole broadcast and adding extra tags on a specific logger + # + # stdout_logger.extend(ActiveSupport::TaggedLogging) + # stdout_logger.push_tags("BMX") + # broadcast = BroadcastLogger.new.extend(ActiveSupport::TaggedLogging) + # broadcast.broadcast_to(stdout_logger, file_logger) + # broadcast.tagged("APP") { broadcast.info("Hello world!") } + # + # Outputs: "[APP][BMX] Hello world!" is written on STDOUT + # Outputs: "[APP] Hello world!" is written in the file + class BroadcastLogger < Logger + # @return [Array] All the logger that are part of this broadcast. + attr_reader :broadcasts + + def initialize(logdev = File::NULL, *args, **kwargs) + @broadcasts = [] + + super(logdev, *args, **kwargs) + end + + # Add logger(s) to the broadcast. + # + # @param loggers [Logger] Loggers that will be part of this broadcast. + # + # @example Broadcast yours logs to STDOUT and STDERR + # broadcast.broadcast_to(Logger.new(STDOUT), Logger.new(STDERR)) + def broadcast_to(*loggers) + @broadcasts.concat(loggers) + end + + # Remove a logger from the broadcast. When a logger is removed, messages sent to + # the broadcast will no longer be written to its sink. + # + # @param logger [Logger] + def stop_broadcasting_to(logger) + @broadcasts.delete(logger) + end + + def <<(message) + dispatch { |logger| logger.<<(message) } + end + + def add(*args, &block) + dispatch_with_processors do |logger| + logger.add(*args, &block) + end + end + + def debug(*args, &block) + dispatch_with_processors do |logger| + logger.debug(*args, &block) + end + end + + def info(*args, &block) + dispatch_with_processors do |logger| + logger.info(*args, &block) + end + end + + def warn(*args, &block) + dispatch_with_processors do |logger| + logger.warn(*args, &block) + end + end + + def error(*args, &block) + dispatch_with_processors do |logger| + logger.error(*args, &block) + end + end + + def fatal(*args, &block) + dispatch_with_processors do |logger| + logger.fatal(*args, &block) + end + end + + def unknown(*args, &block) + dispatch_with_processors do |logger| + logger.unknown(*args, &block) + end + end + + def formatter=(formatter) + dispatch { |logger| logger.formatter = formatter } + end + + def progname=(progname) + dispatch { |logger| logger.progname = progname } + end + + def level=(level) + dispatch { |logger| logger.level = level } + end + + def local_level=(level) + dispatch do |logger| + logger.local_level = level if logger.respond_to?(:local_level=) + end + end + + def close + dispatch { |logger| logger.close } + end + + private + def dispatch(&block) + @broadcasts.each { |logger| block.call(logger) } + end + + def dispatch_with_processors(&block) + @broadcasts.each do |logger| + logger.extend(LogProcessor) unless logger.is_a?(LogProcessor) + logger.processors.unshift(processors) + + block.call(logger) + ensure + logger.processors.shift(processors.count) + end + end + end +end diff --git a/activesupport/lib/active_support/new_logger.rb b/activesupport/lib/active_support/new_logger.rb new file mode 100644 index 0000000000000..8e75e0241b8a5 --- /dev/null +++ b/activesupport/lib/active_support/new_logger.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "logger" +require "active_support/logger_silence" + +module ActiveSupport + module LogProcessor # :nodoc: ? + attr_accessor :processors + + def self.extended(base) + base.processors = [] + end + + def initialize(*args, **kwargs) + super + + self.processors = [] + end + + private + def format_message(severity, datetime, progname, msg) + processors.flatten.reverse_each do |processor| + msg = processor.call(msg, self) + end + + super(severity, datetime, progname, msg) + end + end + + # The ActiveSupport logger is a logger with couple enhanced functionalities. + # + # === Default formatter + # + # The formatter by default will just output the log message with no timestamp or PID. + # + # Using a vanilla Ruby Logger + # logger.info("hello") Outputs: "I, [2023-06-30T02:36:57.164173 #85447] INFO -- : hello" + # + # Using the ActiveSupport::Logger + # logger.info("hello") Outputs: "hello" + # + # === Silence + # + # The ActiveSupport Logger allows to silence logs up to a given severity. This is useful + # to silence DEBUG or INFO logs temporarily but keep more important logs at the ERROR or FATAL level. + # + # Silencing logs up to the ERROR severity + # + # logger.silence(Logger::ERROR) { logger.info("Hello") } Doesn't output anything + # logger.silence(Logger::ERROR) { logger.error("Hello") } Outputs: "Hello" + class Logger < ::Logger + SimpleFormatter = OldLogger::SimpleFormatter + + include LoggerSilence + include LogProcessor + + def self.logger_outputs_to?(logger, *sources) + OldLogger.logger_outputs_to?(logger, *sources) + end + + def initialize(*args, **kwargs) + super + + @formatter ||= Logger::SimpleFormatter.new + end + end +end diff --git a/activesupport/lib/active_support/new_tagged_logging.rb b/activesupport/lib/active_support/new_tagged_logging.rb new file mode 100644 index 0000000000000..1bdba624a68da --- /dev/null +++ b/activesupport/lib/active_support/new_tagged_logging.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require "active_support/core_ext/module/delegation" +require "active_support/core_ext/object/blank" +require "active_support/tagged_logging" +require "logger" +require "active_support/logger" + +module ActiveSupport + # = Active Support Tagged Logging + # + # Wraps any standard Logger object to provide tagging capabilities. + # + # May be called with a block: + # + # logger = Logger.new(STDOUT).extend(ActiveSupport::TaggedLogging) + # logger.tagged('BCX') { logger.info 'Stuff' } # Logs "[BCX] Stuff" + # logger.tagged('BCX', "Jason") { |tagged_logger| tagged_logger.info 'Stuff' } # Logs "[BCX] [Jason] Stuff" + # logger.tagged('BCX') { logger.tagged('Jason') { logger.info 'Stuff' } } # Logs "[BCX] [Jason] Stuff" + # + # If called without a block, a new logger will be returned with applied tags: + # + # logger = Logger.new(STDOUT).extend(ActiveSupport::TaggedLogging) + # logger.tagged("BCX").info "Stuff" # Logs "[BCX] Stuff" + # logger.tagged("BCX", "Jason").info "Stuff" # Logs "[BCX] [Jason] Stuff" + # logger.tagged("BCX").tagged("Jason").info "Stuff" # Logs "[BCX] [Jason] Stuff" + # + # This is used by the default Rails.logger as configured by Railties to make + # it easy to stamp log lines with subdomains, request ids, and anything else + # to aid debugging of multi-user production applications. + module TaggedLogging + class TagProcessor + include OldTaggedLogging::Formatter + + def call(msg, logger) + if logger.formatter.nil? + ActiveSupport.deprecator.warn(<<~EOM) + ActiveSupport::TaggedLogging will no longer set a default formatter on your logger. + To keep your logs unchanged in the future, use the `ActiveSupport::Logger` class or + set the `ActiveSupport::Logger::SimpleFormatter` formatter explicitly on the logger. + + Example: + logger = Rails::Logger.new + logger.extend(ActiveSupport::TaggedLogging) + + Example: + custom_logger = CustomLogger.new(formatter: ActiveSupport::Logger::SimpleFormatter) + custom_logger.extend(ActiveSupport::TaggedLogging) + EOM + + logger.formatter ||= Logger::SimpleFormatter.new + end + + tag_stack.format_message(msg) + end + end + + attr_accessor :tag_processor + delegate :push_tags, :current_tags, :pop_tags, :clear_tags!, to: :tag_processor + + def self.extended(base) # :nodoc: + base.tag_processor = TagProcessor.new + base.extend(ActiveSupport::LogProcessor) + + base.processors << base.tag_processor + end + + def self.new(logger) # :nodoc: + if logger.is_a?(TaggedLogging) + ActiveSupport.deprecator.warn(<<~EOM) + `ActiveSupport::TaggedLogging.new` is deprecated. + To create a new logger from an existing logger, use `logger#clone` instead. + + Before: `new_tagged_logger = ActiveSupport::TaggedLogging.new(existing_logger)` + Now: `new_tagged_logger = existing_logger.clone` + EOM + + logger.clone + else + ActiveSupport.deprecator.warn(<<~EOM) + To create a new Logger instance with tagging functionalities, extend + the `ActiveSupport::TaggedLogging` module. + + Example: `my_logger.extend(ActiveSupport::TaggedLogging)` + EOM + + logger.extend(TaggedLogging) + end + end + + def initialize_clone(_) # :nodoc: + self.tag_processor = TagProcessor.new + self.processors = [tag_processor] + + super + end + + # Add +tags+ to your logs. This method can be used with or a without a block. + # + # With a block: + # All logs will be tagged with +tags+ for the duration of the block. + # + # Without a block: + # A *new* logger instance will be returned. Logs written with this new logger will be tagged + # using the +tags+ and any other tags from the previous logger. For example: + # + # logger1 = Logger.new(STDOUT).extend(ActiveSupport::TaggedLogging) + # logger2 = logger1.tagged("BMX") + # + # logger1.info("Hello world!") # Outputs: Hello world! + # logger2.info("Hello world!") # Outputs: [BMX] Hello world! + # + # logger3 = logger2.tagged("APP") + # logger3.info("Hello world!") # Output: [BMX][APP] Hello world! + # + # If you want to permanently add tags to a logger, without creating a new logger instance, + # use `logger#push_tags` instead. + # + # @param tags [Array] Tags to be added to the log messages. + def tagged(*tags) + if block_given? + tag_processor.tagged(*tags) { yield(self) } + else + logger = clone + logger.tag_processor.extend(OldTaggedLogging::LocalTagStorage) + logger.tag_processor.push_tags(*tag_processor.current_tags, *tags) + + logger + end + end + + def flush + clear_tags! + super if defined?(super) + end + end +end diff --git a/activesupport/test/broadcast_logger_test.rb b/activesupport/test/broadcast_logger_test.rb index 2080af76a96fd..c15d510127ead 100644 --- a/activesupport/test/broadcast_logger_test.rb +++ b/activesupport/test/broadcast_logger_test.rb @@ -71,6 +71,24 @@ class BroadcastLoggerTest < TestCase assert_equal ::Logger::FATAL, log2.local_level end + test "severity methods get called on all loggers" do + my_logger = Class.new(::Logger) do + attr_reader :info_called + + def info(msg, &block) + @info_called = true + end + end.new(StringIO.new) + + @logger.broadcast_to(my_logger) + + assert_changes(-> { my_logger.info_called }, from: nil, to: true) do + @logger.info("message") + end + ensure + @logger.stop_broadcasting_to(my_logger) + end + test "#silence does not break custom loggers" do new_logger = FakeLogger.new custom_logger = CustomLogger.new @@ -117,6 +135,30 @@ class BroadcastLoggerTest < TestCase assert_equal [[::Logger::FATAL, "seen", nil]], log2.adds end + test "stop broadcasting to a logger" do + @logger.stop_broadcasting_to(@log2) + + @logger.info("Hello") + + assert_equal([[1, "Hello", nil]], @log1.adds) + assert_empty(@log2.adds) + end + + test "can broadcast to another broadcast" do + @log3 = FakeLogger.new + @log4 = FakeLogger.new + @broadcast2 = ActiveSupport::BroadcastLogger.new + @broadcast2.broadcast_to(@log3, @log4) + + @logger.broadcast_to(@broadcast2) + @logger.info("Hello") + + assert_equal([[1, "Hello", nil]], @log1.adds) + assert_equal([[1, "Hello", nil]], @log2.adds) + assert_equal([[1, "Hello", nil]], @log3.adds) + assert_equal([[1, "Hello", nil]], @log4.adds) + end + class CustomLogger attr_reader :adds, :closed, :chevrons attr_accessor :level, :progname, :formatter, :local_level diff --git a/activesupport/test/log_broadcast_and_tagging_test.rb b/activesupport/test/log_broadcast_and_tagging_test.rb new file mode 100644 index 0000000000000..c9aabdbd42047 --- /dev/null +++ b/activesupport/test/log_broadcast_and_tagging_test.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require_relative "abstract_unit" + +class LogBroadcastAndTaggingTest < ActiveSupport::TestCase + setup do + @sink1 = StringIO.new + @sink2 = StringIO.new + @logger1 = Logger.new(@sink1, formatter: ActiveSupport::Logger::SimpleFormatter.new) + @logger2 = Logger.new(@sink2, formatter: ActiveSupport::Logger::SimpleFormatter.new) + + @broadcast = ActiveSupport::BroadcastLogger.new + @broadcast.broadcast_to(@logger1, @logger2) + end + + test "tag logs for the whole broadcast with a block" do + @broadcast.extend(ActiveSupport::TaggedLogging) + + @broadcast.tagged("BMX") { @broadcast.info("Hello") } + + assert_equal("[BMX] Hello\n", @sink1.string) + assert_equal("[BMX] Hello\n", @sink2.string) + end + + test "tag logs for the whole broadcast without a block" do + @broadcast.extend(ActiveSupport::TaggedLogging) + + @broadcast.tagged("BMX").info("Hello") + + assert_equal("[BMX] Hello\n", @sink1.string) + assert_equal("[BMX] Hello\n", @sink2.string) + end + + test "tag logs only for one sink" do + @logger1.extend(ActiveSupport::TaggedLogging) + @logger1.push_tags("BMX") + + @broadcast.info { "Hello" } + + assert_equal("[BMX] Hello\n", @sink1.string) + assert_equal("Hello\n", @sink2.string) + end + + test "tag logs for the whole broadcast and extra tags are added to one sink" do + @broadcast.extend(ActiveSupport::TaggedLogging) + @logger1.extend(ActiveSupport::TaggedLogging) + @logger1.push_tags("APP") + + @broadcast.tagged("BMX") { @broadcast.info("Hello") } + + assert_equal("[BMX] [APP] Hello\n", @sink1.string) + assert_equal("[BMX] Hello\n", @sink2.string) + end + + test "can broadcast to another broadcast logger with tagging functionalities" do + @sink3 = StringIO.new + @sink4 = StringIO.new + @logger3 = Logger.new(@sink3, formatter: ActiveSupport::Logger::SimpleFormatter.new) + @logger4 = Logger.new(@sink4, formatter: ActiveSupport::Logger::SimpleFormatter.new) + @broadcast2 = ActiveSupport::BroadcastLogger.new + + @broadcast2.broadcast_to(@logger3, @logger4) + @broadcast.broadcast_to(@broadcast2) + + @broadcast.extend(ActiveSupport::TaggedLogging) + @broadcast2.extend(ActiveSupport::TaggedLogging) + + @broadcast2.push_tags("BROADCAST2") + + @broadcast.tagged("BMX") { @broadcast.info("Hello") } + + assert_equal("[BMX] Hello\n", @sink1.string) + assert_equal("[BMX] Hello\n", @sink2.string) + assert_equal("[BMX] [BROADCAST2] Hello\n", @sink3.string) + assert_equal("[BMX] [BROADCAST2] Hello\n", @sink4.string) + end +end diff --git a/activesupport/test/tagged_logging_test.rb b/activesupport/test/tagged_logging_test.rb index 64d84fdb2c2da..f755791e82d5f 100644 --- a/activesupport/test/tagged_logging_test.rb +++ b/activesupport/test/tagged_logging_test.rb @@ -232,6 +232,33 @@ class TaggedLoggingWithoutBlockTest < ActiveSupport::TestCase assert_equal "[OMG] Broadcasting...\n", broadcast_output.string end + test "#tagged without a block doesn't leak to other loggers" do + sink1 = StringIO.new + logger1 = ActiveSupport::Logger.new(sink1).extend(ActiveSupport::TaggedLogging) + sink2 = StringIO.new + logger2 = ActiveSupport::Logger.new(sink2).extend(ActiveSupport::TaggedLogging) + broadcast_logger = ActiveSupport::BroadcastLogger.new.extend(ActiveSupport::TaggedLogging) + broadcast_logger.broadcast_to(logger1, logger2) + + broadcast_logger.tagged("tag") + broadcast_logger.info("text") + + assert_equal("text\n", sink1.string) + assert_equal("text\n", sink2.string) + end + + test "keeps broadcasting functionality when passed a block" do + output = StringIO.new + logger = Logger.new(output) + broadcast_logger = ActiveSupport::BroadcastLogger.new.extend(ActiveSupport::TaggedLogging) + broadcast_logger.broadcast_to(@logger, logger) + + broadcast_logger.tagged("OMG") { |logger| logger.info "Broadcasting..." } + + assert_equal "[OMG] Broadcasting...\n", @output.string + assert_equal "[OMG] Broadcasting...\n", output.string + end + test "keeps formatter singleton class methods" do plain_output = StringIO.new plain_logger = Logger.new(plain_output) @@ -250,4 +277,29 @@ def crozz_method @logger.tagged("tag") { @logger.info [1, 2, 3] } assert_equal "[tag] [1, 2, 3]\n", @output.string end + + test "setting a default formatter is deprecated" do + output = StringIO.new + logger = Logger.new(output).extend(ActiveSupport::TaggedLogging) + + assert_deprecated(/will no longer set a default formatter on your logger/) do + logger.info("hello") + end + end + + test "#new is deprecated to create a new logger" do + logger = Logger.new(STDOUT) + + assert_deprecated(/To create a new Logger instance with tagging functionalities, extend/) do + ActiveSupport::TaggedLogging.new(logger) + end + end + + test "#new is deprecated when cloning a logger" do + tagged_logger = Logger.new(STDOUT).extend(ActiveSupport::TaggedLogging) + + assert_deprecated(/To create a new logger from an existing logger, use/) do + ActiveSupport::TaggedLogging.new(tagged_logger) + end + end end