How can I have ruby logger log output to stdout as well as file?

Viewed 56741

Someting like a tee functionality in logger.

21 Answers

If you're okay with using ActiveSupport, then I would highly recommend checking out ActiveSupport::Logger.broadcast, which is an excellent and very concise way to add additional log destinations to a logger.

In fact, if you are using Rails 4+ (as of this commit), you don't need to do anything to get the desired behavior — at least if you're using the rails console. Whenever you use the rails console, Rails automatically extends Rails.logger such that it outputs both to its usual file destination (log/production.log, for example) and STDERR:

    console do |app|
      …
      unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT)
        console = ActiveSupport::Logger.new(STDERR)
        Rails.logger.extend ActiveSupport::Logger.broadcast console
      end
      ActiveRecord::Base.verbose_query_logs = false
    end

For some unknown and unfortunate reason, this method is undocumented but you can refer to the source code or blog posts to learn how it works or see examples.

https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destinations-using-activesupport-4.html has another example:

require "active_support/logger"
console_logger = ActiveSupport::Logger.new(STDOUT)
file_logger = ActiveSupport::Logger.new("my_log.log")
combined_logger = console_logger.extend(ActiveSupport::Logger.broadcast(file_logger))

combined_logger.debug "Debug level"
…

This is a simplification of @rado's solution.

def delegator(*methods)
  Class.new do
    def initialize(*targets)
      @targets = targets
    end

    methods.each do |m|
      define_method(m) do |*args|
        @targets.map { |t| t.send(m, *args) }
      end
    end

    class << self
      alias for new
    end
  end # new class
end # delegate

It has all the same benefits as his without the need of the outer class wrapper. Its a useful utility to have in a separate ruby file.

Use it as a one-liner to generate delegator instances like so:

IO_delegator_instance = delegator(:write, :read).for(STDOUT, STDERR)
IO_delegator_instance.write("blah")

OR use it as a factory like so:

logger_delegator_class = delegator(:log, :warn, :error)
secret_delegator = logger_delegator_class(main_logger, secret_logger)
secret_delegator.warn("secret")

general_delegator = logger_delegator_class(main_logger, debug_logger, other_logger) 
general_delegator.log("message")

You can use Loog::Tee object from loog gem:

require 'loog'
logger = Loog::Tee.new(first, second)

Exactly what you are looking for.

I also has this need recently so I implemented a library that does this. I just discovered this StackOverflow question, so I'm putting it out there for anyone that needs it: https://github.com/agis/multi_io.

Compared to the other solutions mentioned here, this strives to be an IO object of its own, so it can be used as a drop-in replacement for other regular IO objects (files, sockets etc.)

That said, I've not yet implemented all the standard IO methods, but those that are, follow the IO semantics (e.g. for example, #write returns the sum of the number of bytes written to all the underlying IO targets).

You can inherit Logger and override the write method:

class LoggerWithStdout < Logger
  def initialize(*)
    super

    def @logdev.write(msg)
      super

      puts msg
    end
  end
end

logger = LoggerWithStdout.new('path/to/log_file.log')
Related