From ae994453a39e4bfe85523457a49cccb5b1b50f7c Mon Sep 17 00:00:00 2001
From: George Ogata <george.ogata@gmail.com>
Date: Wed, 22 Mar 2023 01:14:05 -0400
Subject: [PATCH] Expose the logdev

The motivation here is to fix a Rails issue caused by the way
ActiveSupport extends the ruby Logger to add broadcasting of messages to
multiple destinations. [1] I believe the problem could be much more
elegantly solved if Logger exposed the underlying LogDevice. Going by
repo history, the existence of this object hasn't changed since 2003, so
I think it's stable enough to expose.

In addition to letting you read the logdev, this also lets you pass a
logdev in. To implement broadcasting, we could now define a LogDevice
subclass that delegates its 3 methods to the LogDevices of a list of
underlying loggers, and simply create a new logger with this device.

[1]: https://github.com/rails/rails/blob/ba19dbc49956a73f417abd68c7a5f33e302eacd3/activesupport/lib/active_support/logger.rb#L23
---
 lib/logger.rb              | 19 +++++++++++++++----
 test/logger/test_logger.rb | 10 ++++++++++
 2 files changed, 25 insertions(+), 4 deletions(-)

diff --git a/lib/logger.rb b/lib/logger.rb
index 4940999..c632a5a 100644
--- a/lib/logger.rb
+++ b/lib/logger.rb
@@ -546,6 +546,7 @@ def fatal!; self.level = FATAL; end
   #   new entries are appended.
   # - An IO stream (typically +$stdout+, +$stderr+. or an open file):
   #   entries are to be written to the given stream.
+  # - An instance of Logger::LogDevice, such as the #logdev of another Logger.
   # - +nil+ or +File::NULL+: no entries are to be written.
   #
   # Examples:
@@ -587,13 +588,23 @@ def initialize(logdev, shift_age = 0, shift_size = 1048576, level: DEBUG,
     @logdev = nil
     @level_override = {}
     if logdev && logdev != File::NULL
-      @logdev = LogDevice.new(logdev, shift_age: shift_age,
-        shift_size: shift_size,
-        shift_period_suffix: shift_period_suffix,
-        binmode: binmode)
+      if logdev.is_a?(LogDevice)
+        @logdev = logdev
+      else
+        @logdev = LogDevice.new(logdev, shift_age: shift_age,
+          shift_size: shift_size,
+          shift_period_suffix: shift_period_suffix,
+          binmode: binmode)
+      end
     end
   end
 
+  # The underlying log device.
+  #
+  # This is the first argument passed to the constructor, wrapped in a
+  # Logger::LogDevice, along with the binmode flag and rotation options.
+  attr_reader :logdev
+
   # Sets the logger's output stream:
   #
   # - If +logdev+ is +nil+, reopens the current output stream.
diff --git a/test/logger/test_logger.rb b/test/logger/test_logger.rb
index 37d0f58..aefe5e6 100644
--- a/test/logger/test_logger.rb
+++ b/test/logger/test_logger.rb
@@ -168,6 +168,16 @@ def test_initialize
     assert_nil(logger.datetime_format)
   end
 
+  def test_logdev
+    logger = Logger.new(STDERR)
+    assert_instance_of(Logger::LogDevice, logger.logdev)
+
+    logdev = Logger::LogDevice.new(STDERR)
+    logger = Logger.new(logdev)
+    assert_instance_of(Logger::LogDevice, logger.logdev)
+    assert_equal(STDERR, logger.logdev.dev)
+  end
+
   def test_initialize_with_level
     # default
     logger = Logger.new(STDERR)