Ruby: Send logger messages to a string variable? - ruby

I have a small framework that is logging some info and debug messages using the Logger object built into ruby. At run time, this works great. At unit test time (using rspec if it matters...) i would like to dump the logged messages to an in memory string variable. What's the easiest way to go about doing this?
I was considering a monkey patch that would replace the info and debug methods, like this:
class Logger
def info msg
$logs = msg
super msg
end
end
is there a better way to go about sending my log messages to a string variable?

Use StringIO
require 'stringio'
require 'logger'
strio = StringIO.new
l = Logger.new strio
l.warn "whee, i am logging to a string!"
puts strio.string

Related

Rspec cannot check logger info output

I have an non-rails ruby application, that uses logger to handle log output to stdout.
I want to add specs for checking that output and found two ways to implement this and seems both ways don't work.
So both ways are in this spec.rb file:
require 'rspec'
require 'logger'
describe 'Logger' do
it 'expect to output' do
expect { Logger.new(STDOUT).info('Hello') }.to output.to_stdout
end
it 'expect to recieve' do
expect(Logger.new(STDOUT).info('Hello')).to receive(:info)
end
end
I have output to terminal with log messages, but both verification are failed
Replace STDOUT with $stdout. This works:
expect { Logger.new($stdout).info('Hello') }.to output.to_stdout
The reason is that Rspec internally modifies the global variable $stdout to capture its contents with an StringIO. When using the constant, this mechanism does not work because STDOUT keeps the original value of $stdout.
Notice that you will have a similar problem is you initialize the logger outside of the block. The logger object will store internally the original reference and it won't work:
logger = Logger.new($stdout)
expect { logger.info('Hello') }.to output.to_stdout # This won't work
As per the second question:
expect(Logger.new(STDOUT).info('Hello')).to receive(:info)
This is just a wrong usage of Rspec. It should be something like:
logger = Logger.new(STDOUT)
expect(logger).to receive(:info)
logger.info("Hello")
I typically just use doubles like this:
describe 'logging' do
it 'handles output' do
logger = double('logger')
allow(Logger).to receive(:new).and_return(logger)
run_some_code
expect(logger).to receive(:info).with("Hello")
end
end

Capture Ruby Logger output for testing

I have a ruby class like this:
require 'logger'
class T
def do_something
log = Logger.new(STDERR)
log.info("Here is an info message")
end
end
And a test script line this:
#!/usr/bin/env ruby
gem "minitest"
require 'minitest/autorun'
require_relative 't'
class TestMailProcessorClasses < Minitest::Test
def test_it
me = T.new
out, err = capture_io do
me.do_something
end
puts "Out is '#{out}'"
puts "err is '#{err}'"
end
end
When I run this test, both out and err are empty strings. I see the message printed on stderr (on the terminal). Is there a way to make Logger and capture_io to play nicely together?
I'm in a straight Ruby environment, not Ruby on Rails.
The magic is to use capture_subprocess_io
out, err = capture_subprocess_io do
do_stuff
end
MiniTest's #capture_io temporarily switches $stdout and $stderr for StringIO objects to capture output written to $stdout or $stderr. But Logger has its own reference to the original standard error stream, which it will write to happily. I think you can consider this a bug or at least a limitation of MiniTest's #capture_io.
In your case, you're creating the Logger inside the block to #capture_io with the argument STDERR. STDERR still points to the original standard error stream, which is why it doesn't work as expected.
Changing STDERR to $stderr (which at that points does point to a StringIO object) works around this problem, but only if the Logger is actually created in the #capture_io block, since outside that block it points to the original standard error stream.
class T
def do_something
log = Logger.new($stderr)
log.info("Here is an info message")
end
end
Documentation of capture_subprocess_io
Basically Leonard's example fleshed out and commented with working code and pointing to the docs.
Captures $stdout and $stderr into strings, using Tempfile to ensure that subprocess IO is captured as well.
out, err = capture_subprocess_io do
system "echo Some info" # echos to standard out
system "echo You did a bad thing 1>&2" # echos to standard error
end
assert_match %r%info%, out
assert_match %r%bad%, err
NOTE: This method is approximately 10x slower than #capture_io so only use it when you need to test the output of a subprocess.
See Documentation
This is an old question, but one way we do this is to mock out the logger with an expects. Something like
logger.expects(:info).with("Here is an info message")
This allows us to assert the code under test without changing how logger works out of the box.
As an example of capture_io, we have a logger implementation to allow us to pass in hashes and output them to json. When we test that implementation we use capture_io. This is possible because we initialize the logger implementation in our subject line with $stdout.
subject { CustomLogging.new(ActiveSupport::Logger.new($stdout)) }
in the test
it 'processes a string message' do
msg = "stuff"
out, err = capture_io do
subject.info(msg)
end
out.must_equal "#{msg}\n"
end
You need to provide a different StringIO object while initializing Logger.new to capture the output, rather than the usual: STDERR which actually points to the console.
I modified the above two files a bit and made into a single file so that you can copy and test easily:
require 'logger'
require 'minitest'
class T
def do_something(io = nil)
io ||= STDERR
log = Logger.new io
log.info("Here is an info message")
end
end
class TestT < Minitest::Test
def test_something
t = T.new
string_io = StringIO.new
t.do_something string_io
puts "Out: #{string_io.string}"
end
end
Minitest.autorun
Explanation:
Method do_something will function normally in all other code when used without the argument.
When a StringIO method is provided, it uses that instead of the typical STDERR thus enabling to capture output like into a file or in this case for testing.

Logging Closures and non Sinatra classes

I am new to Ruby and Sinatra and trying to access logger object inside closures (EM.run do .. end).
Here is extracts from working code where logging statements with messagge "LOGGER IS NOT ACCESSIBLE HERE" give compilation error.
class Connection
def get_updates
logger.info "LOGGER IS NOT ACCESSIBLE HERE 1"
end
end
class Streamer < Sinatra::Base
def stream
logger.info "Inside stream"
EM.run do
logger.info "LOGGER IS NOT ACCESSIBLE HERE 3"
Connection.new.get_updates
EM::PeriodicTimer.new(10) do
logger.info "LOGGER IS NOT ACCESSIBLE HERE 4"
end
end
end
end
get '/' do
logger.info "loading data"
Streamer.new.stream
end
From the document I found Sinatra uses env['rack.logger']. How can we use same in non Sinatra classes like Connection and Streamer in above code?
Actually a very interesting question. As it turns out, the logger field is not actually a variable - it is a method within Sinatra::Base. So when you are executing code that is in the scope of a Sinatra::Base object, you can call logger by just typing it and it will return you the logger object.
Since it's a part of the Sinatra::Base object, it won't be visible within the scope of a Connection object.
Now we're ending the simple Ruby stuff, time for the more advanced bit.
The more interesting part is when you call it from within the EM.run do section. The reason that you can't use logger in there is because those blocks are not executed within the scope of the Streamer object, they are executed in some other scope by eventmachine itself using something like instance_eval or class_eval - this post shows a good example of how instance_eval works in a DSL like EM.
This is also why the logger method is accessible from the get '/' block - Sinatra will execute this block in the scope of an object that has a logger method using instance_eval.
In order to do what you want to do, you can try creating a local variable containing the logger:
class Streamer < Sinatra::Base
def stream
# This will create a local variable called logger that will
# save whatever is returned by the logger method
logger = self.logger
logger.info "Inside stream"
EM.run do
logger.info "This should now be accessible"
Connection.new.get_updates
EM::PeriodicTimer.new(10) do
logger.info "This should now be accessible"
end
end
end
end
This still doesn't solve the problem of logging from within your Connection class. The way to do that would be to either use a global log variable (bad idea) or pass in the logger variable when you do Connection.new (not amazing idea, but better than a global).

Logging in Sinatra?

I'm having trouble figuring out how to log messages with Sinatra. I'm not looking to log requests, but rather custom messages at certain points in my app. For example, when fetching a URL I would like to log "Fetching #{url}".
Here's what I'd like:
The ability to specify log levels (ex: logger.info("Fetching #{url}"))
In development and testing environments, the messages would be written to the console.
In production, only write out messages matching the current log level.
I'm guessing this can easily be done in config.ru, but I'm not 100% sure which setting I want to enable, and if I have to manually create a Logger object myself (and furthermore, which class of Logger to use: Logger, Rack::Logger, or Rack::CommonLogger).
(I know there are similar questions on StackOverflow, but none seem to directly answer my question. If you can point me to an existing question, I will mark this one as a duplicate).
Sinatra 1.3 will ship with such a logger object, exactly usable as above. You can use edge Sinatra as described in "The Bleeding Edge". Won't be that long until we'll release 1.3, I guess.
To use it with Sinatra 1.2, do something like this:
require 'sinatra'
use Rack::Logger
helpers do
def logger
request.logger
end
end
I personally log in Sinatra via:
require 'sinatra'
require 'sequel'
require 'logger'
class MyApp < Sinatra::Application
configure :production do
set :haml, { :ugly=>true }
set :clean_trace, true
Dir.mkdir('logs') unless File.exist?('logs')
$logger = Logger.new('logs/common.log','weekly')
$logger.level = Logger::WARN
# Spit stdout and stderr to a file during production
# in case something goes wrong
$stdout.reopen("logs/output.log", "w")
$stdout.sync = true
$stderr.reopen($stdout)
end
configure :development do
$logger = Logger.new(STDOUT)
end
end
# Log all DB commands that take more than 0.2s
DB = Sequel.postgres 'mydb', user:'dbuser', password:'dbpass', host:'localhost'
DB << "SET CLIENT_ENCODING TO 'UTF8';"
DB.loggers << $logger if $logger
DB.log_warn_duration = 0.2
If you are using something like unicorn logging or other middleware that tails IO streams, you can easily set up a logger to STDOUT or STDERR
# unicorn.rb
stderr_path "#{app_root}/shared/log/unicorn.stderr.log"
stdout_path "#{app_root}/shared/log/unicorn.stdout.log"
# sinatra_app.rb
set :logger, Logger.new(STDOUT) # STDOUT & STDERR is captured by unicorn
logger.info('some info') # also accessible as App.settings.logger
this allows you to intercept messages at application scope, rather than just having access to logger as request helper
Here's another solution:
module MySinatraAppLogger
extend ActiveSupport::Concern
class << self
def logger_instance
#logger_instance ||= ::Logger.new(log_file).tap do |logger|
::Logger.class_eval { alias :write :'<<' }
logger.level = ::Logger::INFO
end
end
def log_file
#log_file ||= File.new("#{MySinatraApp.settings.root}/log/#{MySinatraApp.settings.environment}.log", 'a+').tap do |log_file|
log_file.sync = true
end
end
end
included do
configure do
enable :logging
use Rack::CommonLogger, MySinatraAppLogger.logger_instance
end
before { env["rack.errors"] = MySinatraAppLogger.log_file }
end
def logger
MySinatraAppLogger.logger_instance
end
end
class MySinatraApp < Sinatra::Base
include MySinatraAppLogger
get '/' do
logger.info params.inspect
end
end
Of course, you can do it without ActiveSupport::Concern by putting the configure and before blocks straight into MySinatraApp, but what I like about this approach is that it's very clean--all logging configuration is totally abstracted out of the main app class.
It's also very easy to spot where you can change it. For instance, the SO asked about making it log to console in development. It's pretty obvious here that all you need to do is a little if-then logic in the log_file method.

How to add a custom log level to logger in ruby?

I need to add a custom log level like "verbose" or "traffic" to ruby logger, how to do?
Your own logger just need to overwrite the Logger#format_severity method, something like this :
class MyLogger < Logger
SEVS = %w(DEBUG INFO WARN ERROR FATAL VERBOSE TRAFFIC)
def format_severity(severity)
SEVS[severity] || 'ANY'
end
def verbose(progname = nil, &block)
add(5, nil, progname, &block)
end
end
You can simply add to the Logger class:
require 'logger'
class Logger
def self.custom_level(tag)
SEV_LABEL << tag
idx = SEV_LABEL.size - 1
define_method(tag.downcase.gsub(/\W+/, '_').to_sym) do |progname, &block|
add(idx, nil, progname, &block)
end
end
# now add levels like this:
custom_level 'TRAFFIC'
custom_level 'ANOTHER-TAG'
end
# using it:
log = Logger.new($stdout)
log.traffic('testing')
log.another_tag('another message here.')
Log levels are nothing but integer constants defined in logger.rb:
# Logging severity.
module Severity
DEBUG = 0
INFO = 1
WARN = 2
ERROR = 3
FATAL = 4
UNKNOWN = 5
end
You can log messages with any level you like using Logger#add method:
l.add 6, 'asd'
#=> A, [2010-02-17T16:25:47.763708 #14962] ANY -- : asd
If you start needing a bunch of custom stuff, it may be worth checking out log4r, which is a flexible logging library that lets you do a bunch of interesting/useful stuff easily.
This is an old question, but since it comes up so high on google, I figured it'd be useful to have to correct answer. You can actually use the Logging.init method. Here's how you would add a trace log level
require 'logging'
Logging.init %w(trace debug info warn error fatal)
Logging.logger.root.level = :trace
Logging.logger.root.add_appenders Logging.appenders.stdout
Logging.logger['hello'].trace 'TEST'
This is using the 2.0.0 of the logging gem.
You can create your own logger by overloading the Logger class

Resources