Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a public API for broadcasting logs #48615

Merged
merged 1 commit into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class Railtie < Rails::Railtie # :nodoc:
unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT)
console = ActiveSupport::Logger.new(STDERR)
console.level = Rails.logger.level
Rails.logger.extend ActiveSupport::Logger.broadcast console
Rails.logger = ActiveSupport::BroadcastLogger.new(Rails.logger, console)
end
ActiveRecord.verbose_query_logs = false
end
Expand Down
35 changes: 35 additions & 0 deletions activesupport/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
* Add a new public API for broadcasting logs

This feature existed for a while but was until now a private API.
Broadcasting log allows to send log message to difference sinks (STDOUT, a file ...) and
is used by default in the development environment to write logs both on STDOUT and in the
"development.log" file.

Basic usage:

```ruby
stdout_logger = Logger.new(STDOUT)
file_logger = Logger.new("development.log")
broadcast = ActiveSupport::BroadcastLogger.new(stdout_logger, file_logger)

broadcast.info("Hello!") # The "Hello!" message is written on STDOUT and in the log file.
```

Adding other sink(s) to the broadcast:

```ruby
broadcast = ActiveSupport::BroadcastLogger.new
broadcast.broadcast_to(Logger.new(STDERR))
```

Remove a sink from the broadcast:

```ruby
stdout_logger = Logger.new(STDOUT)
broadcast = ActiveSupport::BroadcastLogger.new(stdout_logger)

broadcast.stop_broadcasting_to(stdout_logger)
```

*Edouard Chin*

* Fix Range#overlap? not taking empty ranges into account on Ruby < 3.3

*Nobuyoshi Nakada*, *Shouichi Kamiya*, *Hartley McGuire*
Expand Down
1 change: 1 addition & 0 deletions activesupport/lib/active_support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
require "active_support/version"
require "active_support/deprecator"
require "active_support/logger"
require "active_support/broadcast_logger"
require "active_support/lazy_load_hooks"
require "active_support/core_ext/date_and_time/compatibility"

Expand Down
206 changes: 206 additions & 0 deletions activesupport/lib/active_support/broadcast_logger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# 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.
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

private
def dispatch(&block)
@broadcasts.each { |logger| block.call(logger) }
end
end
end
58 changes: 0 additions & 58 deletions activesupport/lib/active_support/logger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,64 +19,6 @@ def self.logger_outputs_to?(logger, *sources)
sources.any? { |source| source == logger_source }
end

# Broadcasts logs to multiple loggers.
def self.broadcast(logger) # :nodoc:
Module.new do
define_method(:add) do |*args, &block|
logger.add(*args, &block)
super(*args, &block)
end

define_method(:<<) do |x|
logger << x
super(x)
end

define_method(:close) do
logger.close
super()
end

define_method(:progname=) do |name|
logger.progname = name
super(name)
end

define_method(:formatter=) do |formatter|
logger.formatter = formatter
super(formatter)
end

define_method(:level=) do |level|
logger.level = level
super(level)
end

define_method(:local_level=) do |level|
logger.local_level = level if logger.respond_to?(:local_level=)
super(level) if respond_to?(:local_level=)
end

define_method(:silence) do |level = Logger::ERROR, &block|
if logger.respond_to?(:silence)
logger.silence(level) do
if defined?(super)
super(level, &block)
else
block.call(self)
end
end
else
if defined?(super)
super(level, &block)
else
block.call(self)
end
end
end
end
end

def initialize(*args, **kwargs)
super
@formatter ||= SimpleFormatter.new
Expand Down