#--
# =============================================================================
# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu)
# All rights reserved.
#
# This source file is distributed as part of the Needle dependency injection
# library for Ruby. This file (and the library as a whole) may be used only as
# allowed by either the BSD license, or the Ruby license (or, by association
# with the Ruby license, the GPL). See the "doc" subdirectory of the Needle
# distribution for the texts of these licenses.
# -----------------------------------------------------------------------------
# needle website : http://needle.rubyforge.org
# project website: http://rubyforge.org/projects/needle
# =============================================================================
#++
require 'needle/logger'
require 'thread'
module Needle
# A factory class that returns Logger instances. Since each registry
# has its own logger factory, the logger factory must be separately
# instantiable.
class LogFactory
# The default name of the log file to write to.
DEFAULT_LOG_FILENAME = "./needle.log"
# The default format of the log messages (see Logger for more info)
DEFAULT_MESSAGE_FORMAT = "[%-5p] %d -- %C: %m"
# Translate names of levels to their actual values.
LEVEL_TRANSLATOR = {
"DEBUG" => Logger::DEBUG,
"INFO" => Logger::INFO,
"WARN" => Logger::WARN,
"ERROR" => Logger::ERROR,
"FATAL" => Logger::FATAL,
"UNKNOWN" => Logger::UNKNOWN
}
# The default date format string to use when logging.
attr_reader :default_date_format
# The default message format string to use when logging.
attr_reader :default_message_format
# The default log level to use for logs that are created.
attr_reader :default_level
# The device that logs will write to.
attr_reader :device
# Create a new LogFactory using the given initialization parameters.
# The valid options are:
#
# * :device: the device (pseudo-IO object) to write log
# messages to. Either this, or :filename must be specified.
# * :filename: the filename of the log to write to.
# * :roll_age: the number of days before the log should be
# rolled.
# * :roll_frequency: either 'daily', 'weekly', or 'monthly'.
# * :roll_size: the maximum size of a log file.
# * :default_date_format: the default date format string for
# the log.
# * :default_message_format: the default message format string
# for the log.
# * :default_level: the default log level for the log.
# * :levels: a hash of patterns that map to a hash of 'level'
# 'date_format', and 'message_format' keys, specifying the log level,
# date format, and message format for any log whose name matches the key.
def initialize( opts={} )
if opts[:device]
@device = opts[:device]
else
filename = opts[:filename] || DEFAULT_LOG_FILENAME
roll_age = opts[:roll_age ] || opts[:roll_frequency] || 0
roll_size = opts[:roll_size] || 0
@device = Logger::LogDevice.new( filename,
:shift_age=>roll_age, :shift_size=>roll_size )
end
@default_date_format = opts[:default_date_format]
@default_message_format = opts[:default_message_format] ||
DEFAULT_MESSAGE_FORMAT
@default_level = opts[:default_level]
if @default_level.is_a?( String ) || @default_level.is_a?( Symbol )
@default_level = LEVEL_TRANSLATOR[@default_level.to_s]
end
@levels = Hash.new "level" => nil, "date-format" => nil,
"message-format" => nil
( opts[:levels] || {} ).each_pair do |key, value|
key = Regexp.new( "^" + key.gsub( /\./, "\\." ).gsub( /\*/, ".*" ) )
if value.is_a?( String ) || value.is_a?( Symbol )
value = { "level" => value.to_s }
end
@levels[ key ] = value
end
@loggers = Hash.new
@mutex = Mutex.new
@closed = false
end
# Changes the device that the loggers write to. Every log that was created
# by this log factory will be changed to use the given device.
def write_to( device, shift_age = 0, shift_size = 1048576 )
saved_critical = Thread.critical
Thread.critical = true
@device.close if @device unless [ $stdout, $stderr ].include?( @device )
if device.respond_to?( :write ) && device.respond_to?( :close )
@device = device
else
@device = Logger::LogDevice.new( device.to_str,
:shift_age => shift_age,
:shift_size => shift_size )
end
@loggers.each_value { |logger| logger.write_to( @device ) }
device
ensure
Thread.critical = saved_critical
end
# Searches for a level definition hash that matches the given log
# name.
def find_definition( name )
best_match = { :length => 0, :value => Hash.new }
@levels.each_pair do |key, value|
length = key.to_s.length
if best_match[ :length ] < length && key === name
best_match[ :length ] = length
best_match[ :value ] = value
end
end
return best_match[ :value ]
end
private :find_definition
# Retrieves the logger with the given name. If no such log has been
# created, the log will be created and initialized. Otherwise, the
# log with the given name is returned.
def get( name )
name = name.fullname if name.respond_to?( :fullname )
name = name.name if name.respond_to?( :name )
name = name.to_s
# the common case first, outside the synchronize, for speed
return @loggers[ name ] if @loggers[ name ]
@mutex.synchronize do
# check again, inside the synchronize, to avoid race conditions
return @loggers[ name ] if @loggers[ name ]
definition = find_definition( name )
level = definition[ "level" ] || @default_level
date_format = definition[ "date-format" ] || @default_date_format
message_format = definition[ "message-format" ] ||
@default_message_format
level = LEVEL_TRANSLATOR[ level ] || level
logger = Needle::Logger.new( @device )
logger.level = level if level
logger.progname = name
logger.datetime_format = date_format if date_format
logger.message_format = message_format if message_format
@loggers[ name ] = logger
return logger
end
end
# Closes the logging device and makes this factory invalid for future
# accesses.
def close
@mutex.synchronize do
return if @closed
if @device
@device.close unless [ $stdout, $stderr ].include?( @device )
end
@loggers = nil
@closed = true
end
end
# Returns true if the factory has been closed.
def closed?
@closed
end
end
end