I'd like to temporarily redirect stderr in a Ruby script for the duration of a block, ensuring that I reset it to its original value at the end of the block.
I had trouble finding how to do this in the ruby docs.
In Ruby, $stderr refers to the output stream that is currently used as stderr, whereas STDERR is the default stderr stream. It is easy to temporarily assign a different output stream to $stderr.
require "stringio"
def capture_stderr
# The output stream must be an IO-like object. In this case we capture it in
# an in-memory IO object so we can return the string value. You can assign any
# IO object here.
previous_stderr, $stderr = $stderr, StringIO.new
yield
$stderr.string
ensure
# Restore the previous value of stderr (typically equal to STDERR).
$stderr = previous_stderr
end
Now you can do the following:
captured_output = capture_stderr do
# Does not output anything directly.
$stderr.puts "test"
end
captured_output
#=> "test\n"
The same principle also works for $stdout and STDOUT.
Here is a more abstract solution (credit goes to David Heinemeier Hansson):
def silence_streams(*streams)
on_hold = streams.collect { |stream| stream.dup }
streams.each do |stream|
stream.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null')
stream.sync = true
end
yield
ensure
streams.each_with_index do |stream, i|
stream.reopen(on_hold[i])
end
end
Usage:
silence_streams(STDERR) { do_something }
Essentially the same as #molf's answer, and has the same usage:
require "stringio"
def capture_stderr
real_stderr, $stderr = $stderr, StringIO.new
yield
$stderr.string
ensure
$stderr = real_stderr
end
It uses StringIO very slightly more concisely, and preserves $stderr as whatever it was before capture_stderr was called.
I like the StringIO answers. But if you are calling an external process, and $stderr = StringIO.new doesn't work, you might write stderr out to a temporary file:
require 'tempfile'
def capture_stderr
backup_stderr = STDERR.dup
begin
Tempfile.open("captured_stderr") do |f|
STDERR.reopen(f)
yield
f.rewind
f.read
end
ensure
STDERR.reopen backup_stderr
end
end
What I've found so far is that i can redirect the stdout to e.g. StringIO like this:
#original_output = $stdout
#new_output = StringIO.new
$stdout = #new_output
But I also need a "callback" everytime there is smth. written to the new stream. It would be fine to subclass StringIO or whatever but I do not want to overwrite very method puts/print/...
Is there one method that I can overwrite, or how would I just get everything that is written to that IO?
method_missing and message transmission seems like a path of least resistance (although others may have better suggestions)
e.g.
class FakeStdout
attr_reader :output
def initialize(output=STDOUT)
#output= output
end
def some_callback
#output.puts 'called'
# logic
end
def method_missing(method_name,*args,**kwargs,&block)
if #output.respond_to?(method_name)
some_callback
#output.public_send(method_name,*args,**kwargs,&block)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
#output.respond_to?(method_name, include_private) || super
end
end
Then you can use as
$stdout = FakeStdout.new
# called
#=> #<FakeStdout:0x00007ffff4897500 #output=#<IO:<STDOUT>>>
'hello'
# called
#=> "hello"
Caveat: Any method inside FakeStdout that would write to the output stream (e.g. print, puts, etc.) should call directly to the #output instance variable or you will end up with a SystemStackError
Extracting the relevant code snippets from my project to a small standalone test module for clarity ...
require "stringio"
#output = $stdout
$buffer = StringIO.new
$stdout = $buffer
#output.puts "puts method to buffer text"
$stdout = STDOUT
$buffer.rewind
puts "buffer contents: #{$buffer.read}"
Running this code, returns an empty buffer. I have to use the #output.puts to pass an rspec #output(:puts) should_receive rspec test. If I replace the #output.puts with a simple "puts", the buffer is populated but the rspec test fails.
I have searched various online ruby resources for a couple of hours now and cannot use any of the content to answer this question. Any help gratefully received.
OK. Simple error in the end.
Firstly, inserting a
$buffer.write
line means I am actually writing to the buffer so it's not empty!
Secondly,
#output.puts("string")
doesn't actually assign the string contents to #output ,just uses it as an output channel so
$buffer.write(#output)
just writes hex characters not my string to the buffer.
So I have to explicitly write the string or assign it to the variable i.e.
#output="string"
$buffer.write(#output)
to write the string to the buffer.
Thanks for all the responses which set me researching/thinking in the right way - onwards and upwards.
Try this:
require 'stringio'
def read_stdout(&block)
tmp = $stdout
$stdout = tmp = StringIO.new
block.call
tmp.string
ensure
$stdout = tmp # ensures that always evaluated
end
You could redesign your code to not rely on puts and allow injection of the output stream it writes to with STDOUT being the default so your app code doesn't have to change.
class Foo
def something_cool
puts "this is an artifact I want to test"
end
end
Becomes...
class Foo
def initialize(out=STDOUT)
#out = out
end
def something_cool
output "this is an artifact I want to test"
end
private
def output(msg)
#out.write("#{msg}\n")
end
end
And your test becomes...
out = StringIO.new
Foo.new(out)
Foo.something_cool
out.rewind.read.should eq("this is an artifact I want to test\n")
Someting like a tee functionality in logger.
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.
#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)
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))
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.
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')
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)
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
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')
#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
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
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.
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"
…
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"
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.
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 ...')
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')
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.
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 :-)
I am writing some unit tests like the following:
def executing_a_signal
a_method(a_signal.new, a_model, a_helper);
assert_equal(new_state, a_model.state)
end
The tests work fine, but the method which runs just before the assertion to execute the logic prints various messages to the console, mainly via puts.
Is there a quick, perhaps built-in, way to suppress that output to the console? I am only interested in the final effect of the method on the model object, and for the sake of keeping the console clean basically, I was hoping to find a way to simply prevent all output to the console without re-writing or commenting out those puts statements just for my tests.
It is definitely not a critical issue, but would very much like to hear any thoughts or ideas (or workaround) on it.
I use the following snippet in tests to capture and test STDOUT
def capture_stdout(&block)
original_stdout = $stdout
$stdout = fake = StringIO.new
begin
yield
ensure
$stdout = original_stdout
end
fake.string
end
With this method, the above would become:
def executing_a_signal
capture_stdout { a_method(a_signal.new, a_model, a_helper) }
assert_equal(new_state, a_model.state)
end
A slightly cleaner take on #cldwalker's solution:
def silenced
$stdout = StringIO.new
yield
ensure
$stdout = STDOUT
end
silenced do
something_that_prints
end
There's two solutions: redirecting where puts writes to (the solution given by #cldwalker above), or overwriting the puts method itself to be a no-op. (The implementation should be obvious: module Kernel; def puts(*args) end end).
However, in this case, what would really be the best solution is "listening to your tests". Because, oftentimes when something is awkward to test, your tests are really trying to tell you that something is wrong with your design. In this case, I smell a violation of the Single Responsibility Principle: why the heck does a Model object need to know how to write to the console? Its responsibility is representing a Domain Concept, not logging! That's what Logger objects are for!
So, an alternative solution would be to have the Model object delegate the responsibility for logging to a Logger object, and use dependency injection to inject the Model object with a suitable Logger object. That way, you can simply inject a fake logger for the test. Here's an example:
#!/usr/bin/env ruby
class SomeModel
def initialize(logger=Kernel) #logger = logger end
def some_method_that_logs; #logger.puts 'bla' end
end
require 'test/unit'
require 'stringio'
class TestQuietLogging < Test::Unit::TestCase
def setup; #old_stdout, $> = $>, (#fake_logdest = StringIO.new) end
def teardown; $> = #old_stdout end
def test_that_default_logging_is_still_noisy
SomeModel.new.some_method_that_logs
assert_equal "bla\n", #fake_logdest.string
end
def test_that_logging_can_be_made_quiet
fake_logger = Object.new
def fake_logger.puts *args; end
SomeModel.new(fake_logger).some_method_that_logs
assert_equal '', #fake_logdest.string
end
end
At the very least, the Model object should take the IO object that it is logging to as an argument, so that you can simply inject StringIO.new into it for the test:
#!/usr/bin/env ruby
class SomeModel
def initialize(logdest=$>) #logdest = logdest end
def some_method_that_logs; #logdest.puts 'bla' end
end
require 'test/unit'
require 'stringio'
class TestQuietLogging < Test::Unit::TestCase
def setup; #old_stdout, $> = $>, (#fake_logdest = StringIO.new) end
def teardown; $> = #old_stdout end
def test_that_default_logging_is_still_noisy
SomeModel.new.some_method_that_logs
assert_equal "bla\n", #fake_logdest.string
end
def test_that_logging_can_be_made_quiet
fake_logdest = (#fake_logdest = StringIO.new)
SomeModel.new(fake_logdest).some_method_that_logs
assert_equal '', #fake_logdest.string
assert_equal "bla\n", fake_logdest.string
end
end
If you still want to be able to just say puts whatever in your Model or you are afraid that someone might forget to call puts on the logger object, you can provide your own (private) puts method:
class SomeModel
def initialize(logdest=$>) #logdest = logdest end
def some_method_that_logs; puts 'bla' end
private
def puts(*args) #logdest.puts *args end
end
reopen '/dev/null'
Another option is to redirecting to /dev/null with:
STDOUT.reopen('/dev/null', 'w')
STDERR.reopen('/dev/null', 'w')
This technique is used on WEBrick::Daemon of the stdlib (toggle source).
It has the advantage of being more efficient than StringIO.new since it does not store the stdout on a StringIO, but it is less portable.
I just used the below code at the beginning of my .rb file.. so it disable all console print statements..
def puts(*args) end