gshutler/hatchet

View on GitHub
lib/hatchet/hatchet_logger.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# -*- encoding: utf-8 -*-

require 'logger'

module Hatchet

  # Public: Class that handles logging calls and distributes them to all its
  # appenders.
  #
  # Each logger has 5 logging methods. Those are, in decreasing order of
  # severity:
  #
  #  * fatal
  #  * error
  #  * warn
  #  * info
  #  * debug
  #
  # All these methods have the same signature. You can either provide a message
  # as a direct string, or as a block to the method is lazily evaluated (this is
  # the recommended option).
  #
  # Examples
  #
  #   log.info "Informational message"
  #   log.info { "Informational message #{potentially_expensive}" }
  #
  # It is also possible to pass an error to associate with a message. It is down
  # to the appender what it will do with the error (such as including the stack
  # trace) so it is recommended you still include basic information within the
  # message you pass.
  #
  # Examples
  #
  #   log.error "Something bad happened - #{error.message}", error
  #
  # Log messages are sent to each appender where they will be filtered and
  # invoked as configured.
  #
  # Each logger also has 5 inspection methods. Those are, in decreasing order of
  # severity:
  #
  #  * fatal?
  #  * error?
  #  * warn?
  #  * info?
  #  * debug?
  #
  # All these methods take no arguments and return true if any of the loggers'
  # appenders will log a message at that level for the current context,
  # otherwise they will return false.
  #
  class HatchetLogger

    # Internal: Map from standard library levels to Symbols.
    #
    STANDARD_TO_SYMBOL = {
      Logger::DEBUG => :debug,
      Logger::INFO  => :info,
      Logger::WARN  => :warn,
      Logger::ERROR => :error,
      Logger::FATAL => :fatal
    }

    # Public: Gets the NestedDiagnosticContext for the logger.
    #
    attr_reader :ndc

    # Internal: Creates a new logger.
    #
    # host          - The object the logger gains its context from.
    # configuration - The configuration of Hatchet.
    # ndc           - The nested diagnostic context of the logger.
    #
    def initialize(host, configuration, ndc)
      @context = host_name(host)
      @configuration = configuration
      @ndc = ndc
    end

    # Public: Logs a message at debug level.
    #
    # message - An already evaluated message, usually a String (default: nil).
    # error   - An error which is associated with the message (default: nil).
    # block   - An optional block which will provide a message when invoked.
    #
    # One of message or block must be provided. If both are provided then the
    # block is preferred as it is assumed to provide more detail.
    #
    # In general, you should use the block style for any message not related
    # to an error. This is because any unneccessary String interpolation is
    # avoided making unwritten debug calls, for example, less expensive.
    #
    # When logging errors it is advised that you include some details of the
    # error within the regular message, perhaps the error's message, but leave
    # the inclusion of the stack trace up to your appenders and their
    # formatters.
    #
    # Examples
    #
    #   debug { "A fine grained message" }
    #   debug "A message relating to an exception", e
    #
    # Returns nothing.
    #
    def debug(message = nil, error = nil, &block)
      add_to_appenders(:debug, message, error, &block)
    end

    # Public: Returns true if any of the appenders will log messages for the
    # current context at debug level, otherwise returns false.
    #
    # Writes messages to STDOUT if any appender fails to complete the check.
    #
    def debug?
      enabled? :debug
    end

    # Public: Logs a message at info level.
    #
    # message - An already evaluated message, usually a String (default: nil).
    # error   - An error which is associated with the message (default: nil).
    # block   - An optional block which will provide a message when invoked.
    #
    # One of message or block must be provided. If both are provided then the
    # block is preferred as it is assumed to provide more detail.
    #
    # In general, you should use the block style for any message not related
    # to an error. This is because any unneccessary String interpolation is
    # avoided making unwritten info calls, for example, less expensive.
    #
    # When logging errors it is advised that you include some details of the
    # error within the regular message, perhaps the error's message, but leave
    # the inclusion of the stack trace up to your appenders and their
    # formatters.
    #
    # Examples
    #
    #   info { "A fine grained message" }
    #   info "A message relating to an exception", e
    #
    # Returns nothing.
    #
    def info(message = nil, error = nil, &block)
      add_to_appenders(:info, message, error, &block)
    end

    # Public: Returns true if any of the appenders will log messages for the
    # current context at info level, otherwise returns false.
    #
    # Writes messages to STDOUT if any appender fails to complete the check.
    #
    def info?
      enabled? :info
    end

    # Public: Logs a message at warn level.
    #
    # message - An already evaluated message, usually a String (default: nil).
    # error   - An error which is associated with the message (default: nil).
    # block   - An optional block which will provide a message when invoked.
    #
    # One of message or block must be provided. If both are provided then the
    # block is preferred as it is assumed to provide more detail.
    #
    # In general, you should use the block style for any message not related
    # to an error. This is because any unneccessary String interpolation is
    # avoided making unwritten warn calls, for example, less expensive.
    #
    # When logging errors it is advised that you include some details of the
    # error within the regular message, perhaps the error's message, but leave
    # the inclusion of the stack trace up to your appenders and their
    # formatters.
    #
    # Examples
    #
    #   warn { "A fine grained message" }
    #   warn "A message relating to an exception", e
    #
    # Returns nothing.
    #
    def warn(message = nil, error = nil, &block)
      add_to_appenders(:warn, message, error, &block)
    end

    # Public: Returns true if any of the appenders will log messages for the
    # current context at warn level, otherwise returns false.
    #
    # Writes messages to STDOUT if any appender fails to complete the check.
    #
    def warn?
      enabled? :warn
    end

    # Public: Logs a message at error level.
    #
    # message - An already evaluated message, usually a String (default: nil).
    # error   - An error which is associated with the message (default: nil).
    # block   - An optional block which will provide a message when invoked.
    #
    # One of message or block must be provided. If both are provided then the
    # block is preferred as it is assumed to provide more detail.
    #
    # In general, you should use the block style for any message not related
    # to an error. This is because any unneccessary String interpolation is
    # avoided making unwritten error calls, for example, less expensive.
    #
    # When logging errors it is advised that you include some details of the
    # error within the regular message, perhaps the error's message, but leave
    # the inclusion of the stack trace up to your appenders and their
    # formatters.
    #
    # Examples
    #
    #   error { "A fine grained message" }
    #   error "A message relating to an exception", e
    #
    # Returns nothing.
    #
    def error(message = nil, error = nil, &block)
      add_to_appenders(:error, message, error, &block)
    end

    # Public: Returns true if any of the appenders will log messages for the
    # current context at error level, otherwise returns false.
    #
    # Writes messages to STDOUT if any appender fails to complete the check.
    #
    def error?
      enabled? :error
    end

    # Public: Logs a message at fatal level.
    #
    # message - An already evaluated message, usually a String (default: nil).
    # error   - An error which is associated with the message (default: nil).
    # block   - An optional block which will provide a message when invoked.
    #
    # One of message or block must be provided. If both are provided then the
    # block is preferred as it is assumed to provide more detail.
    #
    # In general, you should use the block style for any message not related
    # to an error. This is because any unneccessary String interpolation is
    # avoided making unwritten fatal calls, for example, less expensive.
    #
    # When logging errors it is advised that you include some details of the
    # error within the regular message, perhaps the error's message, but leave
    # the inclusion of the stack trace up to your appenders and their
    # formatters.
    #
    # Examples
    #
    #   fatal { "A fine grained message" }
    #   fatal "A message relating to an exception", e
    #
    # Returns nothing.
    #
    def fatal(message = nil, error = nil, &block)
      add_to_appenders(:fatal, message, error, &block)
    end

    # Public: Returns true if any of the appenders will log messages for the
    # current context at fatal level, otherwise returns false.
    #
    # Writes messages to STDOUT if any appender fails to complete the check.
    #
    def fatal?
      enabled? :fatal
    end

    # Public: Returns the default level of the logger's configuration.
    #
    def level
      @configuration.default_level
    end

    # Public: Set the lowest level of message to log by default.
    #
    # level - The lowest level of message to log by default.
    #
    # The use of this method is not recommended as it affects the performance of
    # the logging. It is only provided for compatibility.
    #
    # Returns nothing.
    #
    def level=(level)
      level = case level
              when Symbol
                level
              else
                STANDARD_TO_SYMBOL[level] || :info
              end

      @configuration.level level
    end

    # Public: Returns nil, exists for greater compatibility with things
    # expecting a standard Logger.
    #
    def formatter
      nil
    end

    # Public: No-op, exists for greater compatibility with things expecting a
    # standard Logger.
    #
    def formatter=(formatter)
      # no-op for Logger protocol compatibility
    end

    # Public: Adds a message to each appender at the specified level.
    #
    # level   - The level of the message. One of, in decreasing order of
    #           severity:
    #
    #             * Logger::FATAL
    #             * Logger::ERROR
    #             * Logger::WARN
    #             * Logger::INFO
    #             * Logger::DEBUG
    #             * :fatal
    #             * :error
    #             * :warn
    #             * :info
    #             * :debug
    #
    # message - The message that will be logged by an appender when it is
    #           configured to log at the given level or lower.
    # block   - An optional block which will provide a message when invoked.
    #
    # Writes messages to STDOUT if any appender fails to complete the enabled
    # check or log the message.
    #
    # Also aliased as log.
    #
    # Returns nothing.
    #
    def add(severity, message = nil, progname = nil, &block)
      level = STANDARD_TO_SYMBOL[severity] || severity
      add_to_appenders(level, message, nil, &block)
    end

    alias log add

    # Public: No-op, exists for greater compatibility with things expecting a
    # standard Logger.
    #
    def <<(msg)
      nil
    end

    # Internal: Specifies the instance variables to be serialized when
    # converting the logger to YAML.
    #
    def to_yaml_properties
      [:@context]
    end

    private

    # Private: Adds a message to each appender at the specified level.
    #
    # level   - The level of the message. One of, in decreasing order of
    #           severity:
    #
    #             * fatal
    #             * error
    #             * warn
    #             * info
    #             * debug
    #
    # message - The message that will be logged by an appender when it is
    #           configured to log at the given level or lower.
    # error   - An error which is associated with the message.
    # block   - An optional block which will provide a message when invoked.
    #
    #
    # Writes messages to STDOUT if any appender fails to complete the enabled
    # check or log the message.
    #
    # Returns nothing.
    #
    def add_to_appenders(level, message, error, &block)
      return unless message or block

      # Ensure configuration and context set - can be lost by marshalling and
      # unmarshalling the logger.
      @configuration ||= Hatchet.configuration
      @ndc ||= Hatchet::NestedDiagnosticContext.current

      msg = Message.new(ndc: @ndc.context.clone, message: message, error: error, backtrace_filters: @configuration.backtrace_filters, &block)

      @configuration.appenders.each do |appender|
        if appender.enabled?(level, @context)
          begin
            appender.add(level, @context, msg)
          rescue => e
            STDERR.puts "Failed to log message for #{@context} with appender #{appender} - #{level} - #{msg}\n"
            STDERR.puts "#{e}\n"
          end
        end
      end
      nil
    end

    # Private: Returns true if any of the appenders will log messages for the
    # current context at the given level, otherwise returns false.
    #
    # level   - The level of the message. One of, in decreasing order of
    #           severity:
    #
    #             * fatal
    #             * error
    #             * warn
    #             * info
    #             * debug
    #
    # Writes messages to STDOUT if any appender fails to complete the check.
    #
    def enabled?(level)
      @configuration.appenders.any? do |appender|
        begin
          appender.enabled? level, @context
        rescue
          puts "Failed to check if level #{level} enabled for #{context} with appender #{appender}\n"
          false
        end
      end
    end

    # Private: Determines the contextual name of the host object.
    #
    # host - The object hosting this logger.
    #
    # Returns the String 'main' if this is the initial execution context of
    # Ruby, the host itself when the host is a module, otherwise the object's
    # class.
    #
    def host_name(host)
      if host.inspect == 'main'
        'main'
      elsif [Module, Class].include? host.class
        host
      else
        host.class
      end
    end

  end

end