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

RubyLogging

Ruby Problem Overview


Someting like a tee functionality in logger.

Ruby Solutions


Solution 1 - Ruby

You can write a pseudo IO class that will write to multiple IO objects. Something like:

class MultiIO
  def initialize(*targets)
     @targets = targets
  end

  def write(*args)
    @targets.each {|t| t.write(*args)}
  end

  def close
    @targets.each(&:close)
  end
end

Then set that as your log file:

log_file = File.open("log/debug.log", "a")
Logger.new MultiIO.new(STDOUT, log_file)

Every time Logger calls puts on your MultiIO object, it will write to both STDOUT and your log file.

Edit: I went ahead and figured out the rest of the interface. A log device must respond to write and close (not puts). As long as MultiIO responds to those and proxies them to the real IO objects, this should work.

Solution 2 - Ruby

@David's solution is very good. I've made a generic delegator class for multiple targets based on his code.

require 'logger'

class MultiDelegator
  def initialize(*targets)
    @targets = targets
  end

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

  class <<self
    alias to new
  end
end

log_file = File.open("debug.log", "a")
log = Logger.new MultiDelegator.delegate(:write, :close).to(STDOUT, log_file)

Solution 3 - Ruby

If you're in Rails 3 or 4, as this blog post points out, Rails 4 has this functionality built in. So you can do:

# config/environment/production.rb
file_logger = Logger.new(Rails.root.join("log/alternative-output.log"))
config.logger.extend(ActiveSupport::Logger.broadcast(file_logger))

Or if you're on Rails 3, you can backport it:

# config/initializers/alternative_output_log.rb

# backported from rails4
module ActiveSupport
  class Logger < ::Logger
    # Broadcasts logs to multiple loggers. Returns a module to be
    # `extended`'ed into other logger instances.
    def self.broadcast(logger)
      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
      end
    end
  end
end

file_logger = Logger.new(Rails.root.join("log/alternative-output.log"))
Rails.logger.extend(ActiveSupport::Logger.broadcast(file_logger))

Solution 4 - Ruby

For those who like it simple:

log = Logger.new("| tee test.log") # note the pipe ( '|' )
log.info "hi" # will log to both STDOUT and test.log

source

Or print the message in the Logger formatter:

log = Logger.new("test.log")
log.formatter = proc do |severity, datetime, progname, msg|
    puts msg
    msg
end
log.info "hi" # will log to both STDOUT and test.log

I'm actually using this technique to print to a log file, a cloud logger service (logentries) and if it's dev environment - also print to STDOUT.

Solution 5 - Ruby

You can also add multiple device logging functionality directly into the Logger:

require 'logger'

class Logger
  # Creates or opens a secondary log file.
  def attach(name)
    @logdev.attach(name)
  end

  # Closes a secondary log file.
  def detach(name)
    @logdev.detach(name)
  end

  class LogDevice # :nodoc:
    attr_reader :devs

    def attach(log)
      @devs ||= {}
      @devs[log] = open_logfile(log)
    end

    def detach(log)
      @devs ||= {}
      @devs[log].close
      @devs.delete(log)
    end

    alias_method :old_write, :write
    def write(message)
      old_write(message)

      @devs ||= {}
      @devs.each do |log, dev|
        dev.write(message)
      end
    end
  end
end

For instance:

logger = Logger.new(STDOUT)
logger.warn('This message goes to stdout')

logger.attach('logfile.txt')
logger.warn('This message goes both to stdout and logfile.txt')

logger.detach('logfile.txt')
logger.warn('This message goes just to stdout')

Solution 6 - Ruby

While I quite like the other suggestions, I found I had this same issue but wanted the ability to have different logging levels for STDERR and the file.

I ended up with a routing strategy that multiplexes at the logger level rather than at the IO level, so that each logger could then operate at independent log-levels:

class MultiLogger
  def initialize(*targets)
    @targets = targets
  end

  %w(log debug info warn error fatal unknown).each do |m|
    define_method(m) do |*args|
      @targets.map { |t| t.send(m, *args) }
    end
  end
end

stderr_log = Logger.new(STDERR)
file_log = Logger.new(File.open('logger.log', 'a'))

stderr_log.level = Logger::INFO
file_log.level = Logger::DEBUG

log = MultiLogger.new(stderr_log, file_log)

Solution 7 - Ruby

Here's another implementation, inspired by @jonas054's answer.

This uses a pattern similar to Delegator. This way you don't have to list all the methods you want to delegate, since it will delegate all methods that are defined in any of the target objects:

class Tee < DelegateToAllClass(IO)
end
 
$stdout = Tee.new(STDOUT, File.open("#{__FILE__}.log", "a"))

You should be able to use this with Logger as well.

delegate_to_all.rb is available from here: https://gist.github.com/TylerRick/4990898

Solution 8 - Ruby

Quick and dirty (ref: https://coderwall.com/p/y_b3ra/log-to-stdout-and-a-file-at-the-same-time)

require 'logger'
ll=Logger.new('| tee script.log')
ll.info('test')

Solution 9 - Ruby

@jonas054's answer above is great, but it pollutes the MultiDelegator class with every new delegate. If you use MultiDelegator several times, it will keep adding methods to the class, which is undesirable. (See bellow for example)

Here is the same implementation, but using anonymous classes so the methods don't pollute the delegator class.

class BetterMultiDelegator

  def self.delegate(*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 to new
      end
    end # new class
  end # delegate

end

Here is an example of the method pollution with the original implementation, contrasted with the modified implementation:

tee = MultiDelegator.delegate(:write).to(STDOUT)
tee.respond_to? :write
# => true
tee.respond_to? :size
# => false 

All is good above. tee has a write method, but no size method as expected. Now, consider when we create another delegate:

tee2 = MultiDelegator.delegate(:size).to("bar")
tee2.respond_to? :size
# => true
tee2.respond_to? :write
# => true   !!!!! Bad
tee.respond_to? :size
# => true   !!!!! Bad

Oh no, tee2 responds to size as expected, but it also responds to write because of the first delegate. Even tee now responds to size because of the method pollution.

Contrast this to the anonymous class solution, everything is as expected:

see = BetterMultiDelegator.delegate(:write).to(STDOUT)
see.respond_to? :write
# => true
see.respond_to? :size
# => false

see2 = BetterMultiDelegator.delegate(:size).to("bar")
see2.respond_to? :size
# => true
see2.respond_to? :write
# => false
see.respond_to? :size
# => false

Solution 10 - Ruby

I have written a little RubyGem that allows you to do several of these things:

# Pipe calls to an instance of Ruby's logger class to $stdout
require 'teerb'

log_file = File.open("debug.log", "a")
logger = Logger.new(TeeRb::IODelegate.new(log_file, STDOUT))

logger.warn "warn"
$stderr.puts "stderr hello"
puts "stdout hello"

You can find the code on github: teerb

Solution 11 - Ruby

Are you restricted to the standard logger?

If not you may use log4r:

require 'log4r' 

LOGGER = Log4r::Logger.new('mylog')
LOGGER.outputters << Log4r::StdoutOutputter.new('stdout')
LOGGER.outputters << Log4r::FileOutputter.new('file', :filename => 'test.log') #attach to existing log-file

LOGGER.info('aa') #Writs on STDOUT and sends to file

One advantage: You could also define different log-levels for stdout and file.

Solution 12 - Ruby

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"

Solution 13 - Ruby

I went to the same idea of "Delegating all methods to sub-elements" that other people already explored, but am returning for each of them the return value of the last call of the method. If I didn't, it broke logger-colors which were expecting an Integer and map was returning an Array.

class MultiIO
  def self.delegate_all
    IO.methods.each do |m|
      define_method(m) do |*args|
        ret = nil
        @targets.each { |t| ret = t.send(m, *args) }
        ret
      end
    end
  end

  def initialize(*targets)
    @targets = targets
    MultiIO.delegate_all
  end
end

This will redelegate every method to all targets, and return only the return value of the last call.

Also, if you want colors, STDOUT or STDERR must be put last, since it's the only two were colors are supposed to be output. But then, it will also output colors to your file.

logger = Logger.new MultiIO.new(File.open("log/test.log", 'w'), STDOUT)
logger.error "Roses are red"
logger.unknown "Violets are blue"

Solution 14 - Ruby

One more way. If you're using tagged logging and need tags in another logfile as well, you could do it in this way

# backported from rails4
# config/initializers/active_support_logger.rb
module ActiveSupport
 class Logger < ::Logger
 
 # Broadcasts logs to multiple loggers. Returns a module to be
 # `extended`'ed into other logger instances.
 def self.broadcast(logger)
  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

   end # Module.new
 end # broadcast

 def initialize(*args)
   super
   @formatter = SimpleFormatter.new
 end

  # Simple formatter which only displays the message.
  class SimpleFormatter < ::Logger::Formatter
   # This method is invoked when a log event occurs
   def call(severity, time, progname, msg)
   element = caller[4] ? caller[4].split("/").last : "UNDEFINED"
    "#{Thread.current[:activesupport_tagged_logging_tags]||nil } # {time.to_s(:db)} #{severity} #{element} -- #{String === msg ? msg : msg.inspect}\n"
   end
  end

 end # class Logger
end # module ActiveSupport

custom_logger = ActiveSupport::Logger.new(Rails.root.join("log/alternative_#{Rails.env}.log"))
Rails.logger.extend(ActiveSupport::Logger.broadcast(custom_logger))

After this you'll get uuid tags in alternative logger

["fbfea87d1d8cc101a4ff9d12461ae810"] 2015-03-12 16:54:04 INFO logger.rb:28:in `call_app' -- 
["fbfea87d1d8cc101a4ff9d12461ae810"] 2015-03-12 16:54:04 INFO   logger.rb:31:in `call_app' -- Started POST "/psp/entrypoint" for 192.168.56.1 at 2015-03-12 16:54:04 +0700

Hope that helps someone.

Solution 15 - Ruby

One more option ;-)

require 'logger'

class MultiDelegator
  def initialize(*targets)
    @targets = targets
  end

  def method_missing(method_sym, *arguments, &block)
    @targets.each do |target|
      target.send(method_sym, *arguments, &block) if target.respond_to?(method_sym)
    end
  end
end

log = MultiDelegator.new(Logger.new(STDOUT), Logger.new(File.open("debug.log", "a")))

log.info('Hello ...')

Solution 16 - Ruby

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")

Solution 17 - Ruby

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

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

Exactly what you are looking for.

Solution 18 - Ruby

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).

Solution 19 - Ruby

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')

Solution 20 - Ruby

I like the MultiIO approach. It works well with Ruby Logger. If you use pure IO it stops working because it lacks some methods that IO objects are expected to have. Pipes were mentioned before here: https://stackoverflow.com/questions/6407141/how-can-i-have-ruby-logger-log-output-to-stdout-as-well-as-file#comment63470228_6407141. Here is what works best for me.

def watch(cmd)
  output = StringIO.new
  IO.popen(cmd) do |fd|
    until fd.eof?
      bit = fd.getc
      output << bit
      $stdout.putc bit
    end
  end
  output.rewind
  [output.read, $?.success?]
ensure
  output.close
end

result, success = watch('./my/shell_command as a String')

Note I know this doesn't answer the question directly but it is strongly related. Whenever I searched for output to multiple IOs I came across this thread.So, I hope you find this useful too.

Solution 21 - Ruby

I think your STDOUT is used for critical runtime info and errors raised.

So I use

  $log = Logger.new('process.log', 'daily')

to log debug and regular logging, and then wrote a few

  puts "doing stuff..."

where I need to see STDOUT information that my scripts were running at all!

Bah, just my 10 cents :-)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionManish SapariyaView Question on Stackoverflow
Solution 1 - RubyDavidView Answer on Stackoverflow
Solution 2 - Rubyjonas054View Answer on Stackoverflow
Solution 3 - RubyphillbakerView Answer on Stackoverflow
Solution 4 - RubyIgorView Answer on Stackoverflow
Solution 5 - RubyRamon de C ValleView Answer on Stackoverflow
Solution 6 - RubydszView Answer on Stackoverflow
Solution 7 - RubyTyler RickView Answer on Stackoverflow
Solution 8 - RubyJose AlbanView Answer on Stackoverflow
Solution 9 - RubyRadoView Answer on Stackoverflow
Solution 10 - RubyPatrick HüslerView Answer on Stackoverflow
Solution 11 - RubyknutView Answer on Stackoverflow
Solution 12 - RubyTyler RickView Answer on Stackoverflow
Solution 13 - RubyJerskaView Answer on Stackoverflow
Solution 14 - RubyretgoatView Answer on Stackoverflow
Solution 15 - RubyMichael VoigtView Answer on Stackoverflow
Solution 16 - RubyCharles MurphyView Answer on Stackoverflow
Solution 17 - Rubyyegor256View Answer on Stackoverflow
Solution 18 - RubyAgisView Answer on Stackoverflow
Solution 19 - Rubykhiav reoyView Answer on Stackoverflow
Solution 20 - RubyknugieView Answer on Stackoverflow
Solution 21 - RubyrupwebView Answer on Stackoverflow