Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Redirect stderr to Logger instance

Tags:

ruby

stderr

I have a Logger instance:

require 'logger'
logger = Logger.new( 'foo.log', 'weekly' )

I want to redirect runtime errors (stderr output) into the log as well. I found this forum thread which has the advice:

new_fd = logger.get_logger_file_descriptor
$stderr.reopen new_fd

However, Logger does not have an instance method get_logger_file_descriptor, nor can I find any exposed methods around gaining access to the log device or file.

How can I cause all $stderr output to go into the log?

like image 512
Phrogz Avatar asked Mar 09 '12 15:03

Phrogz


2 Answers

If you're creating the logger yourself, you can create the File object first, then use it to create the logger and assign it to $stderr:

log_file = File.new('foo.log', 'a')
logger = Logger.new(log_file, 'weekly')
$stderr = log_file   #usually no need to use reopen

Note that this will result in the log output being mixed up with the output of $stderr, which may cause problems if you're parsing the log file expecting it to be in a certain format (this will happen with your solution too).

If you don't have the underlying file but just receive the logger from somewhere else, it's a bit more tricky. What is needed is an IO like object that can be assigned to $stderr and passes anything written to it to the logger. The IO class in Ruby is unfortunately fairly closely tied to the underlying i/o system (file descriptors and the like), and there's no general interface that can be used to create input and output streams. (StringIO being the notable exception).

However most, if not all, of the output methods on IO ultimately go through #write, so by overriding this one method you can get close to what you're after:

class IOToLog < IO

  def initialize(logger)
    @logger = logger
  end

  def write(string)
    #assume anything written to stderr is an error
    @logger.error(message)
  end

end

logger = get_logger_from_somewhere

$stderr = IOToLog.new(logger)

Now anything written to $stderr will end up going to the log file. The formatting however will be a bit odd. Anytime any of the writing methods calls #write a new entry will be made in the logfile. For example, #puts called with an array will call #write for each entry of the array, and again with a newline character between each entry, resulting in 2n - 1 log entries, n - 1 of which will be blank.

You could make the overridden #write method more complex to handle this, perhaps using an internal buffer, and only call the logger when you think you have a full message. Alternatively you could override the individual methods to write to the logger themselves. If you did this the IOToLog class wouldn't necessarily have to inherit from IO.

Your best solution will depend on how you want to standard error output to appear in the logfile, how your program uses $stderr, and how much work you want to do implementing methods from IO.

like image 65
matt Avatar answered Oct 08 '22 15:10

matt


The best I've been able to come up with is this reach-around hack:

$stderr.reopen logger.instance_variable_get(:@logdev).dev

It works, but of course breaks the encapsulation of Logger which I assume was there for a reason.

like image 24
Phrogz Avatar answered Oct 08 '22 14:10

Phrogz