Wondering how to set different log levels for different targets. Below is my Ruby code that writes a line to both Console and File.
# https://stackoverflow.com/a/6407200
class MultiIO
def initialize(*targets)
#targets = targets
end
def write(*args)
#targets.each do |t|
t.write(*args)
end
end
def close
#targets.each(&:close)
end
end
module Logging
def self.logger(logname, programname, debug = false)
log_file = File.open(logname, "a")
log_file.sync = true
zlogger = Logger.new MultiIO.new(log_file, STDOUT)
zlogger.level = Logger::INFO
zlogger.progname = programname
zlogger.formatter = proc do |serverity, datetime, progname, msg|
"#{datetime.strftime('%Y-%m-%d %I:%M:%S %p %:::z %Z')} - #{serverity} - [#{progname}] | #{msg}\n"
end
zlogger
end
end
I can set the level to Debug if a special env variable is found,
$logger.level = Logger::DEBUG if ENV['enable_debug_logs'] == 'true'
But, not sure how to always write Debug lines to the log file and only Info lines to console.
Does anyone know? Any help is greatly appreciated!
Since the log level is a property of the logger, not of the IO, it sounds to me like what you really need to do is define a MultiLogger rather than a MultiIO. Something along the lines of:
class MultiLogger
attr_reader :default_level, :default_progname, :default_formatter
def initialize(**args)
#default_level = args[:default_level]
#default_progname = args[:default_progname]
#default_formatter = args[:default_formatter]
#loggers = []
Array(args[:loggers]).each { |logger| add_logger(logger) }
end
def add_logger(logger)
logger.level = default_level if default_level
logger.progname = default_progname if default_progname
logger.formatter = default_formatter if default_formatter
#loggers << logger
end
def close
#loggers.map(&:close)
end
Logger::Severity.constants.each do |level|
define_method(level.downcase) do |*args|
#loggers.each { |logger| logger.send(__method__, args) }
end
# These methods are a bit weird in the context of a "multi-logger" with varying levels,
# since they are now returning an `Array`; never a falsey value.
# You may want to define them differently, e.g. `#loggers.all? {...}`, or use a non-predicate method name here.
define_method("#{level.downcase}?".to_sym) do
#loggers.map(&__method__)
end
end
end
# Usage:
log_file = File.open(logname, "a")
log_file.sync = true
file_logger = Logger.new(log_file)
console_logger = Logger.new(STDOUT)
console_logger.level = Logger::INFO
multi_logger = MultiLogger.new(
default_progname: programname,
default_formatter: proc do |severity, datetime, progname, msg|
"#{datetime.strftime('%Y-%m-%d %I:%M:%S %p %:::z %Z')} - #{severity} - [#{progname}] | #{msg}\n"
end,
loggers: [file_logger, console_logger]
)
Related
I am developing a script with a big main function, which I have split in several modules.
What I need is to have access to the log functionality from all of them, this means that the log file has to be opened only once, and the access be shared.
This is what I have:
require 'module_1'
require 'module_2'
require 'module_3'
module Main
Module_1.Function_startup()
Module_2.Function_configuration()
Module_3.Function_self_test()
end
Here is the dirty module for the logger I need available in all the other modules.
Ideally I would like to call it as "logger.error", where "logger" returns the instance of the logger, and "error" is the function call on rlogger as rlogger.error.
require 'logger'
module Logging
#rlogger = nil
def init_logger
if #rlogger.nil?
puts "initializing logger"
file_path = File.join(File.dirname(__FILE__), 'checker.log')
open_mode = File::TRUNC # or File::APPEND
file = File.open(file_path, File::WRONLY | open_mode)
#rlogger = Logger.new(file)
#rlogger.datetime_format = "%Y-%m-%d %H:%M:%S"
#rlogger.formatter = proc do |severity, datetime, progname, msg|
con_msg = ""
if msg.match("ERROR:")
con_msg = msg.color(:red)
elsif msg.match("OK!")
con_msg = msg.color(:green)
else
con_msg = msg
end
puts ">>>#{con_msg}"
# notice that the colors introduce extra non-printable characters
# which are not nice in the log file.
"#{datetime}: #{msg}\n"
end
# Here is the first log entry
#rlogger.info('Initialize') {"#{Time.new.strftime("%H-%M-%S")}: Checker v#{#version}"}
end
end
# returns the logger
def logger
if #rlogger.nil?
puts "requesting nil rlogger"
end
#rlogger
end
end
end
Just after require you can add this piece of code
$FILE_LOG = Logging.create_log(File.expand_path('LoggingFile.log'), Logger::DEBUG)
Explanation of the above line : It is calling a function in Logging Module, to create File , Level of Logging is debug.
Below is the piece of code for Module
module Logging
def self.create_log(output_location level)
log = Logger.new(output_location, 'weekly').tap do |l|
next unless l
l.level = level
l.progname = File.basename($0)
l.datetime_format = DateTime.iso8601
l.formatter = proc { |severity, datetime, progname, msg| "#{severity}: #{datetime} - (#{progname}) : #{msg}\n" }
end
log.level = level if level < log.level
log
end
def self.log(msg, level)
# Here I am just logging only FATAL and DEBUG, similarly you can add in different level of logs
if level == :FATAL
$FILE_LOG.fatal(msg)
elsif level == :DEBUG
$FILE_LOG.debug(msg)
end
end
end
Then in Every method every Ruby file, we can use this logging as follows
Logging.log("Message",:FATAL)
I'm having trouble understanding how to test for output with puts. I need to know what I need to do in my RSPEC file.
This is my RSPEC file:
require 'game_io'
require 'board'
describe GameIO do
before(:each) do
#gameio = GameIO.new
#board = Board.new
end
context 'welcome_message' do
it 'should display a welcome message' do
test_in = StringIO.new("some test input\n")
test_out = StringIO.new
test_io = GameIO.new(test_in, test_out)
test_io.welcome_message
test_io.game_output.string.should == "Hey, welcome to my game. Get ready to be defeated"
end
end
end
This is the file it is testing against:
class GameIO
attr_reader :game_input, :game_output
def initialize(game_input = $stdin, game_output = $stdout)
#stdin = game_input
#stdout = game_output
end
def welcome_message
output "Hey, welcome to my game. Get ready to be defeated"
end
def output(msg)
#stdout.puts msg
end
def input
#stdin.gets
end
end
NOTE: I updated my RSPEC code to reflect changes I made to my test file given suggestions found elsewhere. To resolve the poblem completly I used the changes suggested by Chris Heald in my main file. Thank you all and thank you Chris.
Your initializer should be:
def initialize(game_input = $stdin, game_output = $stdout)
#game_input = game_input
#game_output = game_output
end
The reason for this is that attr_accessor generates methods like this:
# attr_accessor :game_output
def game_output
#game_output
end
def game_output=(output)
#game_output = output
end
(attr_reader generates only the reader method)
Thus, since you never assign #game_output, your game_output method will always return nil.
Just check you are sending it the message:
#gameio.should_receive(:puts).with("Hey, welcome to my game. Get ready to be defeated")
You could stub puts and print.
Perhaps the most fundamental way is to temporarily reassign STDOUT to a variable, and confirm the variable matches what you expect for output.
And Minitest has must_output as an assertion/spec.
The code is thus:
##
# Fails if stdout or stderr do not output the expected results.
# Pass in nil if you don't care about that streams output. Pass in
# "" if you require it to be silent. Pass in a regexp if you want
# to pattern match.
#
# NOTE: this uses #capture_io, not #capture_subprocess_io.
#
# See also: #assert_silent
def assert_output stdout = nil, stderr = nil
out, err = capture_io do
yield
end
err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr
out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout
y = send err_msg, stderr, err, "In stderr" if err_msg
x = send out_msg, stdout, out, "In stdout" if out_msg
(!stdout || x) && (!stderr || y)
end
I am confused with how to write decent code when using a lot of asynchronous code.
In the following code snippet I log in to get the authentication cookie and use that cookie for the next request to get a list of projects name (as an example):
def self.populateProjectsTable(projects_controller)
payload = {email: "email", password: "pass"}
HTTP.post("http://example.com/login", {payload: payload}) do |response|
authCookie = response.headers['Set-Cookie']
HTTP.get("http://example.com/projects.json", {cookie: authCookie}) do |response|
projects = JSON.parse(response.body.to_str)
projects_controller.projects = projects
projects_controller.reloadData
end
end
end
While this will work the code feels dirty. Not really following the single responsibility principle. I would like to extract this in a few methods:
def self.populateProjectsTable(projects_controller)
#taskList = TaskList.new
#taskList.doLogin
projects = #taskList.getProjects
projects_controller.projects = projects
projects_controller.reloadData
end
def doLogin
payload = {email: "email", password: "pass"}
HTTP.post("http://example.com/login", {payload: payload}) do |response|
#authCookie = response.headers['Set-Cookie']
end
end
def getProjects
HTTP.get("http://example.com/projects.json", {cookie: #authCookie}) do |response|
projects = JSON.parse(response.body.to_str)
end
end
This obviously does not work. The getProjects method is called before doLogin is finished and the projects are only known in the scope of the block, not giving back the data to the populateProjectsTable method.
How does one program such applications without the nesting shown in the first example?
You're not going to totally get away from the nesting. Taking Alan's answer and massaging it a bit, this is what I've come up with. It involves passing a block through a couple of methods.
def self.populateProjectsTable(projects_controller)
#taskList = TaskList.new
#taskList.loginAndGetProjects do |projects|
projects_controller.projects = projects
projects_controller.reloadData
end
end
def loginAndGetProjects(&block)
payload = {email: "email", password: "pass"}
HTTP.post("http://example.com/login", {payload: payload}) do |response|
#authCookie = response.headers['Set-Cookie']
getProjects(&block)
end
end
def getProjects(&block)
HTTP.get("http://example.com/projects.json", {cookie: #authCookie}) do |response|
projects = JSON.parse(response.body.to_str)
block.call(projects)
end
end
I've had a similar problem trying to wrap methods that themselves took blocks. I wanted the new wrapper methods to still be able to take blocks. Here's what I did in ParseModel:
# with block:
# ParseModel::Cloud.callFunction("myFunction", {"myParam" => "myValue"}) do |result, error|
# # do something...
# end
# without block:
# ParseModel::Cloud.callFunction("myFunction", {"myParam" => "myValue"})
module ParseModel
class Cloud
def self.callFunction(function, params={}, &block)
return PFCloud.callFunction(function, withParameters:params) unless block_given?
PFCloud.callFunctionInBackground(function, withParameters:params, block:lambda do |result, error|
block.call(result, error)
end)
end
end
end
Applying this concept to your problem, you could rewrite your methods to take blocks themselves. Here's a bit of a refactor that I think might be helpful:
def self.populateProjectsTable(projects_controller)
#taskList = TaskList.new
#taskList.doLogin do |login_response|
authCookie = login_response.headers['Set-Cookie']
#taskList.getProjects(authCookie) do |projects_response|
projects = JSON.parse(projects_response.body.to_str)
projects_controller.projects = projects
projects_controller.reloadData
end
end
end
def doLogin(&block)
payload = {email: "email", password: "pass"}
HTTP.post("http://example.com/login", {payload: payload}) do |response|
block.call(response)
end
end
def getProjects(cookie, &block)
HTTP.get("http://example.com/projects.json", {cookie: cookie}) do |response|
block.call(response)
end
end
I don't think you're totally out of the woods regarding SRP, but this should be a good start.
+1 for Jamon's answer.
I might suggest using a class to manage your session and splitting out the API into a module if you like SRP. This is especially helpful as you add additional API calls. Here I queue up requests that will be satisfied once login is completed. Later you can add handling for timeouts, etc.
module ProjectApi
def get_projects(&block)
with_session do
HTTP.get("http://example.com/projects.json", {cookie: #auth_cookie}) do |response|
projects = JSON.parse(response.body.to_str)
block.call(projects)
end
end
end
end
class MySession
include ProjectApi
def initialize(login, password)
#login = login
#password = password
#state = nil
#requests = []
end
def active?
#state == :active
end
def with_session(&block)
#requests << &block
active? ? handle_requests : login(true)
end
private
def login(do_handle_requests = false)
payload = {login: #login, password: #password}
#state = nil
HTTP.post("http://example.com/login", {payload: payload}) do |response|
#state = :active
#auth_cookie = response.headers['Set-Cookie']}
handle_requests if do_handle_requests
end
end
def handle_requests
while request = #requests.shift do
request.call
end if active?
end
end
def self.populateProjectsTable(projects_controller)
#session ||= MySession.new('mylogin', 'mypassword')
#session.get_projects do |projects|
projects_controller.projects = projects
projects_controller.reloadData
end
end
When your own test suite had executed, there were some results.
i want to keep thoes infomation in a txt file or html,
but i don't how to save those outputted message,
if anyone knows,please share with me, thanks in advance
the belowing code is my experiment, but it doesn't work.
require File.join(File.dirname(__FILE__), 'person')
require "test/unit"
filename = "logfile.txt"
$logfile = File.new(filename,"a")
open(logfile,'a') { |f| f << $stdout}
class PersonTest < Test::Unit::TestCase
FIRST_NAME, LAST_NAME, AGE = 'Nathaniel', 'Taldtretbott', 25
def setup
#person = Person.new(FIRST_NAME, LAST_NAME, AGE)
end
def test_first_name
assert_equal "asv", #person.first_name,"try to compare"
end
def test_last_name
assert_equal "Taldtretbott", #person.last_name
end
def test_full_name
assert_equal FIRST_NAME + ' ' + LAST_NAME, #person.full_name
end
end
Why not do
ruby testfile.rb > text_output.txt
I had some time to try some testing on a solution. I managed to put it to work this way:
require 'test/unit'
STDOUT = $stdout = File.open("stdout.txt", "a+")
STDERR = $stderr = File.open("stderr.txt", "a+")
class AreasTest < Test::Unit::TestCase
def test_ok
puts "#{Time.now} Hello "
a = 10
assert_equal(a, 10)
end
def test_fail
a = 9
assert_equal(a, 10)
end
end
It gives a warning cause STDOUT and STDERR were already initialized but just redirecting $stdout and $stderr does not work (works only for normal puts).
I hope it helps.
Try:
$stdout = $logfile
instead of:
open(logfile,'a') { |f| f << $stdout}
This means that you are redirecting the stdout to the file.
I'd like to include the name of the class that invokes the logger in the log output, such as:
[MyClass] here is the message
I've seen the option of using the Contexts but I don't want to have to do something like this throughout my app when I log stuff (keep in mind, I want the class name in every log message):
NDC.push('class:' + self.class.name)
logger.debug 'hello'
I'd like to just call:
logger.debug 'hello'
Any suggestions?
Using the contexts is preferable, but you can use your own formatter (see Log4r formatters)
logger = Logger.new 'test'
outputter = Outputter.stdout
outputter.formatter = PatternFormatter.new(:pattern => "%l - kittens - %m")
logger.outputters = outputter
logger.info 'adorable' # => INFO - kittens - adorable
Or, actually, because you want it to reference self.class my advice would actually to create a Logging module that works like so:
module Logging
def logger
return #logger if #logger
#logger = Logger.new 'test'
outputter = Outputter.stdout
outputter.formatter = PatternFormatter.new(:pattern => "%l - #{self.class} - %m")
#logger.outputters = outputter
#logger
end
end
class HasLogging
include Logging
def test
logger.info 'adorable'
end
end
test = HasLogging.new
test.test # => INFO - HasLogging - adorable
Probably not exactly like that, but you get the idea.