rails/activesupport/lib/active_support/broadcast_logger.rb
Maximo Mussini a054307bbc
Improve compatibility between Logger and ActiveSupport::BroadcastLogger
The usage of `dispatch` in all logging methods causes common usages such
as `logger.info` to return an array of loggers, making it unsafe for an
application to upgrade to Rails 7.1.

Returning `nil` is more efficient, and is the default behavior when
using `Logger`.
2024-05-29 22:18:37 +00:00

252 lines
7.5 KiB
Ruby

# frozen_string_literal: true
module ActiveSupport
# = Active Support Broadcast Logger
#
# 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(stdout_logger, file_logger)
#
# broadcast.info("Hello world!") # Writes the log to STDOUT and the development.log file.
#
# Add a logger to the broadcast.
#
# stdout_logger = Logger.new(STDOUT)
# broadcast = BroadcastLogger.new(stdout_logger)
# file_logger = Logger.new("development.log")
# broadcast.broadcast_to(file_logger)
#
# broadcast.info("Hello world!") # Writes the log to STDOUT and the development.log file.
#
# Modifying the log level for all broadcasted loggers.
#
# stdout_logger = Logger.new(STDOUT)
# file_logger = Logger.new("development.log")
# broadcast = BroadcastLogger.new(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(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.
#
# If you are adding a custom logger with custom methods to the broadcast,
# the `BroadcastLogger` will proxy them and return the raw value, or an array
# of raw values, depending on how many loggers in the broadcasts responded to
# the method:
#
# class MyLogger < ::Logger
# def loggable?
# true
# end
# end
#
# logger = BroadcastLogger.new
# logger.loggable? # => A NoMethodError exception is raised because no loggers in the broadcasts could respond.
#
# logger.broadcast_to(MyLogger.new(STDOUT))
# logger.loggable? # => true
# logger.broadcast_to(MyLogger.new(STDOUT))
# puts logger.broadcasts # => [MyLogger, MyLogger]
# logger.loggable? # [true, true]
class BroadcastLogger
include ActiveSupport::LoggerSilence
# Returns all the logger that are part of this broadcast.
attr_reader :broadcasts
attr_reader :formatter
attr_accessor :progname
def initialize(*loggers)
@broadcasts = []
@progname = "Broadcast"
broadcast_to(*loggers)
end
# Add logger(s) to the broadcast.
#
# broadcast_logger = ActiveSupport::BroadcastLogger.new
# broadcast_logger.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.
#
# sink = Logger.new(STDOUT)
# broadcast_logger = ActiveSupport::BroadcastLogger.new
#
# broadcast_logger.stop_broadcasting_to(sink)
def stop_broadcasting_to(logger)
@broadcasts.delete(logger)
end
def level
@broadcasts.map(&:level).min
end
def <<(message)
dispatch { |logger| logger.<<(message) }
end
def add(*args, &block)
dispatch { |logger| logger.add(*args, &block) }
end
alias_method :log, :add
def debug(*args, &block)
dispatch { |logger| logger.debug(*args, &block) }
end
def info(*args, &block)
dispatch { |logger| logger.info(*args, &block) }
end
def warn(*args, &block)
dispatch { |logger| logger.warn(*args, &block) }
end
def error(*args, &block)
dispatch { |logger| logger.error(*args, &block) }
end
def fatal(*args, &block)
dispatch { |logger| logger.fatal(*args, &block) }
end
def unknown(*args, &block)
dispatch { |logger| logger.unknown(*args, &block) }
end
def formatter=(formatter)
dispatch { |logger| logger.formatter = formatter }
@formatter = formatter
end
def level=(level)
dispatch { |logger| logger.level = level }
end
alias_method :sev_threshold=, :level=
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
# +True+ if the log level allows entries with severity Logger::DEBUG to be written
# to at least one broadcast. +False+ otherwise.
def debug?
@broadcasts.any? { |logger| logger.debug? }
end
# Sets the log level to Logger::DEBUG for the whole broadcast.
def debug!
dispatch { |logger| logger.debug! }
end
# +True+ if the log level allows entries with severity Logger::INFO to be written
# to at least one broadcast. +False+ otherwise.
def info?
@broadcasts.any? { |logger| logger.info? }
end
# Sets the log level to Logger::INFO for the whole broadcast.
def info!
dispatch { |logger| logger.info! }
end
# +True+ if the log level allows entries with severity Logger::WARN to be written
# to at least one broadcast. +False+ otherwise.
def warn?
@broadcasts.any? { |logger| logger.warn? }
end
# Sets the log level to Logger::WARN for the whole broadcast.
def warn!
dispatch { |logger| logger.warn! }
end
# +True+ if the log level allows entries with severity Logger::ERROR to be written
# to at least one broadcast. +False+ otherwise.
def error?
@broadcasts.any? { |logger| logger.error? }
end
# Sets the log level to Logger::ERROR for the whole broadcast.
def error!
dispatch { |logger| logger.error! }
end
# +True+ if the log level allows entries with severity Logger::FATAL to be written
# to at least one broadcast. +False+ otherwise.
def fatal?
@broadcasts.any? { |logger| logger.fatal? }
end
# Sets the log level to Logger::FATAL for the whole broadcast.
def fatal!
dispatch { |logger| logger.fatal! }
end
def initialize_copy(other)
@broadcasts = []
@progname = other.progname.dup
@formatter = other.formatter.dup
broadcast_to(*other.broadcasts.map(&:dup))
end
private
def dispatch(&block)
@broadcasts.each { |logger| block.call(logger) }
true
end
def method_missing(name, ...)
loggers = @broadcasts.select { |logger| logger.respond_to?(name) }
if loggers.none?
super
elsif loggers.one?
loggers.first.send(name, ...)
else
loggers.map { |logger| logger.send(name, ...) }
end
end
def respond_to_missing?(method, include_all)
@broadcasts.any? { |logger| logger.respond_to?(method, include_all) }
end
end
end