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
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.
@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)
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')
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.
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)
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"
# 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))
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
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.
@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:
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:
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 ...')
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: How can I have ruby logger log output to stdout as well as file?. 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.
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:
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.
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')