require 'logger'
require 'delegate'
require 'fileutils'
class Loops::Logger < ::Delegator
# @return [Boolean]
# A value indicating whether all logging output should be
# also duplicated to the console.
attr_reader :write_to_console
# @return [Boolean]
# A value inidicating whether critical errors should be highlighted
# with ANSI colors in the log.
attr_reader :colorful_logs
# Initializes a new instance of the {Logger} class.
#
# @param [String, IO] logfile
# The log device. This is a filename (String), 'stdout' or
# 'stderr' (String), 'default' for default framework's
# log file, or +IO+ object (typically +STDOUT+, +STDERR+,
# or an open file).
# @param [Integer] level
# Logging level. Constants are defined in +Logger+ namespace: +DEBUG+, +INFO+,
# +WARN+, +ERROR+, +FATAL+, or +UNKNOWN+.
# @param [Integer] number_of_files
# A number of files to keep.
# @param [Integer] max_file_size
# A max file size. When file become larger, next one will be created.
# @param [Boolean] write_to_console
# When +true+, all logging output will be dumped to the +STDOUT+ also.
#
def initialize(logfile = $stdout, level = ::Logger::INFO, number_of_files = 10, max_file_size = 100 * 1024 * 1024,
write_to_console = false)
@number_of_files, @level, @max_file_size, @write_to_console =
number_of_files, level, max_file_size, write_to_console
self.logfile = logfile
super(@implementation)
end
# Sets the default log file (see {#logfile=}).
#
# @param [String, IO] logfile
# the log file path or IO.
# @return [String, IO]
# the log file path or IO.
#
def default_logfile=(logfile)
@default_logfile = logfile
self.logfile = logfile
end
# Sets the log file.
#
# @param [String, IO] logfile
# The log device. This is a filename (String), 'stdout' or
# 'stderr' (String), 'default' for default framework's
# log file, or +IO+ object (typically +STDOUT+, +STDERR+,
# or an open file).
# @return [String, IO]
# the log device.
#
def logfile=(logfile)
logfile = @default_logfile || $stdout if logfile == 'default'
coerced_logfile =
case logfile
when 'stdout' then $stdout
when 'stderr' then $stderr
when IO, StringIO then logfile
else
if Loops.root
logfile =~ /^\// ? logfile : Loops.root.join(logfile).to_s
else
logfile
end
end
# Ensure logging directory does exist
FileUtils.mkdir_p(File.dirname(coerced_logfile)) if String === coerced_logfile
# Create a logger implementation.
@implementation = LoggerImplementation.new(coerced_logfile, @number_of_files, @max_file_size, @write_to_console, @colorful_logs)
@implementation.level = @level
logfile
end
# Remember the level at the proxy level.
#
# @param [Integer] level
# Logging severity.
# @return [Integer]
# Logging severity.
#
def level=(level)
@level = level
@implementation.level = @level if @implementation
level
end
# Sets a value indicating whether to dump all logs to the console.
#
# @param [Boolean] value
# a value indicating whether to dump all logs to the console.
# @return [Boolean]
# a value indicating whether to dump all logs to the console.
#
def write_to_console=(value)
@write_to_console = value
@implementation.write_to_console = value if @implementation
value
end
# Sets a value indicating whether to highlight with red ANSI color
# all critical messages.
#
# @param [Boolean] value
# a value indicating whether to highlight critical errors in log.
# @return [Boolean]
# a value indicating whether to highlight critical errors in log.
#
def colorful_logs=(value)
@colorful_logs = value
@implementation.colorful_logs = value if @implementation
value
end
# @private
# Send everything else to @implementation.
def __getobj__
@implementation or raise "Logger implementation not initialized"
end
# @private
# Delegator's method_missing ignores the &block argument (!!!?)
def method_missing(m, *args, &block)
target = self.__getobj__
unless target.respond_to?(m)
super(m, *args, &block)
else
target.__send__(m, *args, &block)
end
end
# @private
class LoggerImplementation < ::Logger
attr_reader :prefix
attr_accessor :write_to_console, :colorful_logs
class Formatter
def initialize(logger)
@logger = logger
end
def call(severity, time, progname, message)
if (@logger.prefix || '').empty?
"#{severity[0..0]} : #{time.strftime('%Y-%m-%d %H:%M:%S')} : #{message || progname}\n"
else
"#{severity[0..0]} : #{time.strftime('%Y-%m-%d %H:%M:%S')} : #{@logger.prefix} : #{message || progname}\n"
end
end
end
def initialize(log_device, number_of_files = 10, max_file_size = 10 * 1024 * 1024, write_to_console = true, colorful_logs = false)
super(log_device, number_of_files, max_file_size)
self.formatter = Formatter.new(self)
@write_to_console = write_to_console
@colorful_logs = colorful_logs
@prefix = nil
end
def add(severity, message = nil, progname = nil, &block)
begin
if @colorful_logs
message = color_errors(severity, message)
progname = color_errors(severity, progname)
end
super(severity, message, progname, &block)
if @write_to_console && (message || progname)
puts self.formatter.call(%w(D I W E F A)[severity] || 'A', Time.now, progname, message)
end
rescue
# ignore errors in logging
end
end
def with_prefix(prefix)
raise "No block given" unless block_given?
old_prefix = @prefix
@prefix = prefix
begin
yield
ensure
@prefix = old_prefix
end
end
def color_errors(severity, line)
if severity < ::Logger::ERROR
line
else
if line && line !~ /\e/
"\e[31m#{line}\e[0m"
else
line
end
end
end
end
end