# -----------------------------------------------------------------------------
#
# Sawmill logger class
#
# -----------------------------------------------------------------------------
# Copyright 2009 Daniel Azuma
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the copyright holder, nor the names of any other
# contributors to this software, may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# -----------------------------------------------------------------------------
;
begin
require 'securerandom'
rescue ::LoadError
end
module Sawmill
# This is the Sawmill logger.
# It duck-types most of the API of the logger class from the ruby
# standard library, and adds capabilities specific to Sawmill.
class Logger
# Create a new logger.
#
# Supported options include:
#
# :levels::
# Use a custom Sawmill::LevelGroup. Normally, you should leave this
# set to the default, which is Sawmill::STANDARD_LEVELS.
# :level::
# Default level to use for log messages when no level is explicitly
# provided. By default, this is set to the level group's default,
# which in the case of the standard levels is :INFO.
# :attribute_level::
# Default level to use for attributes when no level is explicitly
# provided. By default, this is set to the level group's highest,
# level, which in the case of the standard levels is :ANY.
# :progname::
# Progname to use in log messages. Default is "sawmill".
# :record_progname::
# Progname to use in special log entries dealing with log records
# (i.e. record delimiters and attribute messages). Default is the
# same as the normal progname setting.
# :record_id_generator::
# A proc that generates and returns a new record ID if one is not
# explicitly passed into begin_record. If you do not provide a
# generator, the default one is used, which generates an ID using the
# variant 4 (random) UUID standard.
# :processor::
# A processor for log entries generated by this logger.
# If not specified, log entries are written out to STDOUT.
def initialize(opts_={})
@levels = opts_[:levels] || STANDARD_LEVELS
@level = @levels.get(opts_[:level])
if opts_.include?(:attribute_level)
@attribute_level = @levels.get(opts_[:attribute_level])
else
@attribute_level = @levels.highest
end
@progname = opts_[:progname] || 'sawmill'
@record_progname = opts_[:record_progname]
@record_id_generator = opts_[:record_id_generator] || Logger._get_default_record_id_generator
@processor = opts_[:processor] || Formatter.new(::STDOUT)
@current_record_id = nil
end
# Emit a log message. This method has the same behavior as the
# corresponding method in ruby's logger class.
def add(level_, message_=nil, progname_=nil, &block_)
level_obj_ = @levels.get(level_)
if level_obj_.nil?
raise Errors::UnknownLevelError, level_
end
return true if level_obj_ < @level
progname_ ||= @progname
if message_.nil?
if block_given?
message_ = yield
else
message_ = progname_
progname_ = @progname
end
end
case message_
when ::String
# Do nothing
when ::Exception
message_ = "#{message_.message} (#{message_.class})\n" +
(message_.backtrace || []).join("\n")
else
message_ = message_.inspect
end
@processor.message(Entry::Message.new(level_obj_, ::Time.now, progname_, @current_record_id, message_))
true
end
alias_method :log, :add
def to_s # :nodoc:
inspect
end
def inspect # :nodoc:
"#<#{self.class}:0x#{object_id.to_s(16)} progname=#{@progname.inspect} level=#{@level.name}>"
end
# Emits an "unknown" log entry. This is equivalent to the corresponding
# method in ruby's logger class, which dumps the given string to the log
# device without any formatting. Normally, you would not use this method
# because it bypasses the log formatting and parsing capability.
def <<(message_)
add(@levels.default, message_)
end
# Emits a begin_record log entry. This begins a new log record.
#
# If you pass a string ID, that ID is used as the record ID for the new
# log record. If you leave it as nil, an ID is generated for you, using
# the record id generator for this logger. In either case, the record ID
# for the new record is returned.
#
# If you call this when a record is already open, the current record is
# automatically closed before the new record is opened. That is, an
# end_record is implicitly called in this case.
def begin_record(id_=nil)
end_record if @current_record_id
@current_record_id = (id_ || @record_id_generator.call).to_s
@processor.begin_record(Entry::BeginRecord.new(@levels.highest, ::Time.now, @record_progname || @progname, @current_record_id))
@current_record_id
end
# Returns the record ID for the currently open log record, or nil if
# there is not a log record currently open.
def current_record_id
@current_record_id
end
# Ends the current log record by emitting an end_record log entry, if
# a record is currently open. Returns the record ID of the ended log
# record if one was open, or nil if no log record was open.
def end_record
if @current_record_id
@processor.end_record(Entry::EndRecord.new(@levels.highest, ::Time.now, @record_progname || @progname, @current_record_id))
id_ = @current_record_id
@current_record_id = nil
id_
else
nil
end
end
# Emits an attribute log entry in the current record.
# You must specify a key and a value as strings, and an operation.
# The operation defaults to :set if not specified.
#
# If you specify a level, it will be used; otherwise the logger's
# default attribute level is used.
# Raises Errors::UnknownLevelError if you specify a level that doesn't
# exist.
def attribute(key_, value_, operation_=nil, level_=true, progname_=nil)
if level_ == true
level_obj_ = @attribute_level
else
level_obj_ = @levels.get(level_)
if level_obj_.nil?
raise Errors::UnknownLevelError, level_
end
end
return true if level_obj_ < @level
@processor.attribute(Entry::Attribute.new(level_obj_, ::Time.now, progname_ || @record_progname || @progname, @current_record_id, key_, value_, operation_))
true
end
# Emits a set-attribute log entry in the current record.
# You must specify a key and a value as strings.
def set_attribute(key_, value_)
attribute(key_, value_, :set)
end
# Emits an append-attribute log entry in the current record.
# You must specify a key and a value as strings.
def append_attribute(key_, value_)
attribute(key_, value_, :append)
end
# Close the logger by finishing the log entry processor to which it is
# emitting log entries. Returns the value returned by the processor's
# finish method.
def close
@processor.finish
end
# Get the current progname setting for this logger
def progname
@progname
end
# Set the current progname setting for this logger
def progname=(value_)
@progname = value_.to_s.gsub(/\s+/, '')
end
# Get the current level setting for this logger as a Sawmill::Level.
def level
@level
end
# Set the current level setting for this logger.
# You may specify the level as a string, a symbol, an integer, or a
# Sawmill::Level. Ruby's logger constants such as ::Logger::INFO
# will also work.
def level=(value_)
if value_.kind_of?(Level)
@level = value_
else
level_obj_ = @levels.get(value_)
if level_obj_.nil?
raise Errors::UnknownLevelError, value_
end
@level = level_obj_
end
end
alias_method :sev_threshold=, :level=
alias_method :sev_threshold, :level
# Provide a block that generates and returns a unique record ID string.
# This block will be called when begin_record is called without an
# explicit ID provided. If you do not provide a block, Sawmill will use
# a default generator which uses the variant 4 (random) UUID standard.
def to_generate_record_id(&block_)
@record_id_generator = block_ || Logger._get_default_record_id_generator
end
# You may call additional methods on the logger as shortcuts to log
# messages at specific levels, or to query whether the logger is logging
# to a given level. These methods match the corresponding methods in the
# classic ruby logger object, except that they are configurable for
# custom level schemes.
#
# For example, in the standard level scheme, the method "info" is
# defined, so you may call:
#
# logger.info("MainApp") { "Received connection from #{ip}" }
# # ...
# logger.info "Waiting for input from user"
# # ...
# logger.info { "User typed #{input}" }
#
# You may also call:
#
# logger.info? # Returns true if INFO messages are accepted
#
# Methods available in the standard level scheme are as follows:
#
# * debug
# * info
# * warn
# * error
# * fatal
# * unknown
# * any
#
# The "unknown" and "any" methods both correspond to the +ANY+ level.
# The latter is the preferred name under Sawmill. The former is for
# backward compatibility with ruby's classic logger.
def method_missing(method_, *args_, &block_)
method_name_ = method_.to_s
question_ = method_name_[-1..-1] == '?'
method_name_ = method_name_[0..-2] if question_
level_ = @levels.lookup_method(method_name_)
return super(method_, *args_, &block_) unless level_
if question_
level_ >= @level
else
add(level_, nil, args_[0], &block_)
end
end
def self._get_default_record_id_generator # :nodoc:
unless @_default_generator
if defined?(::SecureRandom)
def self._random_hex32
::SecureRandom.hex(32)
end
elsif defined?(::ActiveSupport::SecureRandom)
def self._random_hex32
::ActiveSupport::SecureRandom.hex(32)
end
else
def self._random_hex32
::Kernel.rand(0x100000000000000000000000000000000).to_s(16).rjust(32, '0')
end
end
@_default_generator = ::Proc.new do
uuid_ = _random_hex32
uuid_[12] = '4'
uuid_[16] = (uuid_[16,1].to_i(16)&3|8).to_s(16)
uuid_.insert(8, '-')
uuid_.insert(13, '-')
uuid_.insert(18, '-')
uuid_.insert(23, '-')
uuid_
end
end
@_default_generator
end
end
end