# ----------------------------------------------------------------------------- # # 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