Skip to content

Commit

Permalink
Merge pull request #48615 from Edouard-chin/ec-logger
Browse files Browse the repository at this point in the history
Add a public API for broadcasting logs
  • Loading branch information
rafaelfranca committed Sep 25, 2023
2 parents 88bb5f2 + 1fbd812 commit 4c72cc2
Show file tree
Hide file tree
Showing 9 changed files with 400 additions and 89 deletions.
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

0 comments on commit 4c72cc2

Please sign in to comment.