# logger.rb - simple logging utility
# Copyright (C) 2000-2003, 2005 NAKAMURA, Hiroshi <nakahiro@sarion.co.jp>.
# Simple logging utility.
# Author:: NAKAMURA, Hiroshi <nakahiro@sarion.co.jp>
# Documentation:: NAKAMURA, Hiroshi and Gavin Sinclair
# You can redistribute it and/or modify it under the same terms of Ruby's
# license; either the dual license version in 2003, or any later version.
# Revision:: $Id: logger.rb 31806 2011-05-30 02:08:57Z nahi $
# The Logger class provides a simple but sophisticated logging utility that
# anyone can use because it's included in the Ruby 1.8.x standard library.
# The HOWTOs below give a code-based overview of Logger's usage, but the basic
# concept is as follows. You create a Logger object (output to a file or
# elsewhere), and use it to log messages. The messages will have varying
# levels (+info+, +error+, etc), reflecting their varying importance. The
# levels, and their meanings, are:
# +FATAL+:: an unhandleable error that results in a program crash
# +ERROR+:: a handleable error condition
# +INFO+:: generic (useful) information about system operation
# +DEBUG+:: low-level information for developers
# So each message has a level, and the Logger itself has a level, which acts
# as a filter, so you can control the amount of information emitted from the
# logger without having to remove actual messages.
# For instance, in a production system, you may have your logger(s) set to
# +INFO+ (or +WARN+ if you don't want the log files growing large with
# repetitive information). When you are developing it, though, you probably
# want to know about the program's internal state, and would set them to
# A simple example demonstrates the above explanation:
# log = Logger.new(STDOUT)
# log.level = Logger::WARN
# log.debug("Created logger")
# log.info("Program started")
# log.warn("Nothing to do!")
# File.each_line(path) do |line|
# unless line =~ /^(\w+) = (.*)$/
# log.error("Line in wrong format: #{line}")
# log.fatal("Caught exception; exiting")
# Because the Logger's level is set to +WARN+, only the warning, error, and
# fatal messages are recorded. The debug and info messages are silently
# There are several interesting features that Logger provides, like
# auto-rolling of log files, setting the format of log messages, and
# specifying a program name in conjunction with the message. The next section
# shows you how to achieve these things.
# === How to create a logger
# The options below give you various choices, in more or less increasing
# 1. Create a logger which logs messages to STDERR/STDOUT.
# logger = Logger.new(STDERR)
# logger = Logger.new(STDOUT)
# 2. Create a logger for the file which has the specified name.
# logger = Logger.new('logfile.log')
# 3. Create a logger for the specified file.
# file = File.open('foo.log', File::WRONLY | File::APPEND)
# # To create new (and to remove old) logfile, add File::CREAT like;
# # file = open('foo.log', File::WRONLY | File::APPEND | File::CREAT)
# logger = Logger.new(file)
# 4. Create a logger which ages logfile once it reaches a certain size. Leave
# 10 "old log files" and each file is about 1,024,000 bytes.
# logger = Logger.new('foo.log', 10, 1024000)
# 5. Create a logger which ages logfile daily/weekly/monthly.
# logger = Logger.new('foo.log', 'daily')
# logger = Logger.new('foo.log', 'weekly')
# logger = Logger.new('foo.log', 'monthly')
# === How to log a message
# Notice the different methods (+fatal+, +error+, +info+) being used to log
# messages of various levels. Other methods in this family are +warn+ and
# +debug+. +add+ is used below to log a message of an arbitrary (perhaps
# logger.fatal { "Argument 'foo' not given." }
# 2. Message as a string.
# logger.error "Argument #{ @foo } mismatch."
# logger.info('initialize') { "Initializing..." }
# logger.add(Logger::FATAL) { 'Fatal error!' }
# === How to close a logger
# === Setting severity threshold
# logger.sev_threshold = Logger::WARN
# 2. Log4r (somewhat) compatible interface.
# logger.level = Logger::INFO
# DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN
# Log messages are rendered in the output stream in a certain format. The
# default format and a sample are shown below:
# SeverityID, [Date Time mSec #pid] SeverityLabel -- ProgName: message
# I, [Wed Mar 03 02:34:24 JST 1999 895701 #19074] INFO -- Main: info.
# You may change the date and time format in this manner:
# logger.datetime_format = "%Y-%m-%d %H:%M:%S"
# # e.g. "2004-01-03 00:54:26"
# There is currently no supported way to change the overall format, but you may
# have some luck hacking the Format constant.
ProgName = "#{File.basename(__FILE__)}/#{VERSION}"
class Error < RuntimeError; end
class ShiftingError < Error; end
# Logging severity threshold (e.g. <tt>Logger::INFO</tt>).
# Logging date-time format (string passed to +strftime+).
def datetime_format=(datetime_format)
@default_formatter.datetime_format = datetime_format
@default_formatter.datetime_format
# Logging formatter. formatter#call is invoked with 4 arguments; severity,
# time, progname and msg for each log. Bear in mind that time is a Time and
# msg is an Object that user passed and it could not be a String. It is
# expected to return a logdev#write-able Object. Default formatter is used
# when no formatter is set.
alias sev_threshold level
alias sev_threshold= level=
# Returns +true+ iff the current severity level allows for the printing of
def debug?; @level <= DEBUG; end
# Returns +true+ iff the current severity level allows for the printing of
def info?; @level <= INFO; end
# Returns +true+ iff the current severity level allows for the printing of
def warn?; @level <= WARN; end
# Returns +true+ iff the current severity level allows for the printing of
def error?; @level <= ERROR; end
# Returns +true+ iff the current severity level allows for the printing of
def fatal?; @level <= FATAL; end
# Logger.new(name, shift_age = 7, shift_size = 1048576)
# Logger.new(name, shift_age = 'weekly')
# The log device. This is a filename (String) or IO object (typically
# +STDOUT+, +STDERR+, or an open file).
# Number of old log files to keep, *or* frequency of rotation (+daily+,
# +weekly+ or +monthly+).
# Maximum logfile size (only applies when +shift_age+ is a number).
def initialize(logdev, shift_age = 0, shift_size = 1048576)
@default_formatter = Formatter.new
@logdev = LogDevice.new(logdev, :shift_age => shift_age,
:shift_size => shift_size)
# Logger#add(severity, message = nil, progname = nil) { ... }
# Severity. Constants are defined in Logger namespace: +DEBUG+, +INFO+,
# +WARN+, +ERROR+, +FATAL+, or +UNKNOWN+.
# The log message. A String or Exception.
# Program name string. Can be omitted. Treated as a message if no +message+ and
# Can be omitted. Called to get a message string if +message+ is nil.
# +true+ if successful, +false+ otherwise.
# When the given severity is not high enough (for this particular logger), log
# no message, and return +true+.
# Log a message if the given severity is high enough. This is the generic
# logging method. Users will be more inclined to use #debug, #info, #warn,
# <b>Message format</b>: +message+ can be any object, but it has to be
# converted to a String in order to log it. Generally, +inspect+ is used
# if the given object is not a String.
# A special case is an +Exception+ object, which will be printed in detail,
# including message, class, and backtrace. See #msg2str for the
# implementation if required.
# * Logfile is not locked.
# * Append open does not need to lock file.
# * But on the OS which supports multi I/O, records possibly be mixed.
def add(severity, message = nil, progname = nil, &block)
if @logdev.nil? or severity < @level
format_message(format_severity(severity), Time.now, progname, message))
# Dump given message to the log device without any formatting. If no log
# device exists, return +nil+.
# See #info for more information.
def debug(progname = nil, &block)
add(DEBUG, nil, progname, &block)
# The message can come either from the +progname+ argument or the +block+. If
# both are provided, then the +block+ is used as the message, and +progname+
# is used as the program name.
# logger.info("MainApp") { "Received connection from #{ip}" }
# logger.info "Waiting for input from user"
# logger.info { "User typed #{input}" }
# You'll probably stick to the second form above, unless you want to provide a
# program name (which you can do with <tt>Logger#progname=</tt> as well).
def info(progname = nil, &block)
add(INFO, nil, progname, &block)
# See #info for more information.
def warn(progname = nil, &block)
add(WARN, nil, progname, &block)
# Log an +ERROR+ message.
# See #info for more information.
def error(progname = nil, &block)
add(ERROR, nil, progname, &block)
# See #info for more information.
def fatal(progname = nil, &block)
add(FATAL, nil, progname, &block)
# Log an +UNKNOWN+ message. This will be printed no matter what the logger
# See #info for more information.
def unknown(progname = nil, &block)
add(UNKNOWN, nil, progname, &block)
# Close the logging device.
# Severity label for logging. (max 5 char)
SEV_LABEL = %w(DEBUG INFO WARN ERROR FATAL ANY)
def format_severity(severity)
SEV_LABEL[severity] || 'ANY'
def format_message(severity, datetime, progname, msg)
(@formatter || @default_formatter).call(severity, datetime, progname, msg)
Format = "%s, [%s#%d] %5s -- %s: %s\n"
attr_accessor :datetime_format
def call(severity, time, progname, msg)
Format % [severity[0..0], format_datetime(time), $$, severity, progname,
def format_datetime(time)
time.strftime("%Y-%m-%dT%H:%M:%S.") << "%06d " % time.usec
time.strftime(@datetime_format)
"#{ msg.message } (#{ msg.class })\n" <<
(msg.backtrace || []).join("\n")
def initialize(log = nil, opt = {})
@dev = @filename = @shift_age = @shift_size = nil
@mutex = LogDeviceMutex.new
if log.respond_to?(:write) and log.respond_to?(:close)
@shift_age = opt[:shift_age] || 7
@shift_size = opt[:shift_size] || 1048576
if @shift_age and @dev.respond_to?(:stat)
raise Logger::ShiftingError.new("Shifting failed. #{$!}")