require 'syslog'
require 'logger'

##
# Syslog::Logger is a Logger work-alike that logs via syslog instead of to a
# file.  You can use Syslog::Logger to aggregate logs between multiple
# machines.
#
# By default, Syslog::Logger uses the program name 'ruby', but this can be
# changed via the first argument to Syslog::Logger.new.
#
# NOTE! You can only set the Syslog::Logger program name when you initialize
# Syslog::Logger for the first time.  This is a limitation of the way
# Syslog::Logger uses syslog (and in some ways, a limitation of the way
# syslog(3) works).  Attempts to change Syslog::Logger's program name after
# the first initialization will be ignored.
#
# === Example
#
# The following will log to syslogd on your local machine:
#
#   require 'syslog/logger'
#
#   log = Syslog::Logger.new 'my_program'
#   log.info 'this line will be logged via syslog(3)'
#
# Also the facility may be set to specify the facility level which will be used:
#
#   log.info 'this line will be logged using Syslog default facility level'
#
#   log_local1 = Syslog::Logger.new 'my_program', Syslog::LOG_LOCAL1
#   log_local1.info 'this line will be logged using local1 facility level'
#
#
# You may need to perform some syslog.conf setup first.  For a BSD machine add
# the following lines to /etc/syslog.conf:
#
#  !my_program
#  *.*                                             /var/log/my_program.log
#
# Then touch /var/log/my_program.log and signal syslogd with a HUP
# (killall -HUP syslogd, on FreeBSD).
#
# If you wish to have logs automatically roll over and archive, see the
# newsyslog.conf(5) and newsyslog(8) man pages.

class Syslog::Logger
  # Default formatter for log messages.
  class Formatter
    def call severity, time, progname, msg
      clean msg
    end

    private

    ##
    # Clean up messages so they're nice and pretty.

    def clean message
      message = message.to_s.strip
      message.gsub!(/\e\[[0-9;]*m/, '') # remove useless ansi color codes
      return message
    end
  end

  ##
  # The version of Syslog::Logger you are using.

  VERSION = '2.0'

  ##
  # Maps Logger warning types to syslog(3) warning types.
  #
  # Messages from Ruby applications are not considered as critical as messages
  # from other system daemons using syslog(3), so most messages are reduced by
  # one level.  For example, a fatal message for Ruby's Logger is considered
  # an error for syslog(3).

  LEVEL_MAP = {
    ::Logger::UNKNOWN => Syslog::LOG_ALERT,
    ::Logger::FATAL   => Syslog::LOG_ERR,
    ::Logger::ERROR   => Syslog::LOG_WARNING,
    ::Logger::WARN    => Syslog::LOG_NOTICE,
    ::Logger::INFO    => Syslog::LOG_INFO,
    ::Logger::DEBUG   => Syslog::LOG_DEBUG,
  }

  ##
  # Returns the internal Syslog object that is initialized when the
  # first instance is created.

  def self.syslog
    @@syslog
  end

  ##
  # Specifies the internal Syslog object to be used.

  def self.syslog= syslog
    @@syslog = syslog
  end

  ##
  # Builds a methods for level +meth+.

  def self.make_methods meth
    level = ::Logger.const_get(meth.upcase)
    eval <<-EOM, nil, __FILE__, __LINE__ + 1
      def #{meth}(message = nil, &block)
        add(#{level}, message, &block)
      end

      def #{meth}?
        @level <= #{level}
      end
    EOM
  end

  ##
  # :method: unknown
  #
  # Logs a +message+ at the unknown (syslog alert) log level, or logs the
  # message returned from the block.

  ##
  # :method: fatal
  #
  # Logs a +message+ at the fatal (syslog err) log level, or logs the message
  # returned from the block.

  ##
  # :method: error
  #
  # Logs a +message+ at the error (syslog warning) log level, or logs the
  # message returned from the block.

  ##
  # :method: warn
  #
  # Logs a +message+ at the warn (syslog notice) log level, or logs the
  # message returned from the block.

  ##
  # :method: info
  #
  # Logs a +message+ at the info (syslog info) log level, or logs the message
  # returned from the block.

  ##
  # :method: debug
  #
  # Logs a +message+ at the debug (syslog debug) log level, or logs the
  # message returned from the block.

  Logger::Severity::constants.each do |severity|
    make_methods severity.downcase
  end

  ##
  # Log level for Logger compatibility.

  attr_accessor :level

  # Logging formatter, as a +Proc+ that will take four arguments and
  # return the formatted message. The arguments are:
  #
  # +severity+:: The Severity of the log message.
  # +time+:: A Time instance representing when the message was logged.
  # +progname+:: The #progname configured, or passed to the logger method.
  # +msg+:: The _Object_ the user passed to the log message; not necessarily a
  #         String.
  #
  # The block should return an Object that can be written to the logging
  # device via +write+.  The default formatter is used when no formatter is
  # set.
  attr_accessor :formatter

  ##
  # The facility argument is used to specify what type of program is logging the message.

  attr_accessor :facility

  ##
  # Fills in variables for Logger compatibility.  If this is the first
  # instance of Syslog::Logger, +program_name+ may be set to change the logged
  # program name. The +facility+ may be set to specify the facility level which will be used.
  #
  # Due to the way syslog works, only one program name may be chosen.

  def initialize program_name = 'ruby', facility = nil
    @level = ::Logger::DEBUG
    @formatter = Formatter.new

    @@syslog ||= Syslog.open(program_name)

    @facility = (facility || @@syslog.facility)
  end

  ##
  # Almost duplicates Logger#add.  +progname+ is ignored.

  def add severity, message = nil, progname = nil, &block
    severity ||= ::Logger::UNKNOWN
    @level <= severity and
      @@syslog.log( (LEVEL_MAP[severity] | @facility), '%s', formatter.call(severity, Time.now, progname, (message || block.call)) )
    true
  end
end
