diff --git a/ext/syslog/lib/syslog/logger.rb b/ext/syslog/lib/syslog/logger.rb new file mode 100644 index 0000000..52ad842 --- /dev/null +++ b/ext/syslog/lib/syslog/logger.rb @@ -0,0 +1,177 @@ +require 'syslog' +require 'logger' +require 'rbconfig' + +## +# SyslogLogger is a Logger work-alike that logs via syslog instead of to a +# file. You can add SyslogLogger to your Rails production environment to +# aggregate logs between multiple machines. +# +# By default, SyslogLogger uses the program name 'rails', but this can be +# changed via the first argument to SyslogLogger.new. +# +# NOTE! You can only set the SyslogLogger program name when you initialize +# SyslogLogger for the first time. This is a limitation of the way +# SyslogLogger uses syslog (and in some ways, a limitation of the way +# syslog(3) works). Attempts to change SyslogLogger's program name after the +# first initialization will be ignored. +# +# = Sample usage with Rails +# +# == config/environment/production.rb +# +# Add the following lines: +# +# require 'syslog_logger' +# RAILS_DEFAULT_LOGGER = SyslogLogger.new +# +# == config/environment.rb +# +# In 0.10.0, change this line: +# +# RAILS_DEFAULT_LOGGER = Logger.new("#{RAILS_ROOT}/log/#{RAILS_ENV}.log") +# +# to: +# +# RAILS_DEFAULT_LOGGER ||= Logger.new("#{RAILS_ROOT}/log/#{RAILS_ENV}.log") +# +# Other versions of Rails should have a similar change. +# +# == BSD syslog setup +# +# === /etc/syslog.conf +# +# Add the following lines: +# +# !rails +# *.* /var/log/production.log +# +# Then touch /var/log/production.log and signal syslogd with a HUP +# (killall -HUP syslogd, on FreeBSD). +# +# === /etc/newsyslog.conf +# +# Add the following line: +# +# /var/log/production.log 640 7 * @T00 Z +# +# This creates a log file that is rotated every day at midnight, gzip'd, then +# kept for 7 days. Consult newsyslog.conf(5) for more details. +# +# == syslog-ng setup +# +# === syslog-ng.conf +# +# destination rails_log { file("/var/log/production.log"); }; +# filter f_rails { program("rails.*"); }; +# log { source(src); filter(f_rails); destination(rails_log); }; +# +# == Starting +# +# Now restart your Rails app. Your production logs should now be showing up +# in /var/log/production.log. If you have mulitple machines, you can log them +# all to a central machine with remote syslog logging for analysis. Consult +# your syslogd(8) manpage for further details. + +module Syslog + class Logger + + ## + # The version of SyslogLogger you are using. + + VERSION = '2.0.0' + + ## + # Maps Logger warning types to syslog(3) warning types. + + LOGGER_MAP = { + :debug => :debug, + :info => :info, + :warn => :notice, + :unknown => :alert, + :fatal => :err, + :error => :warning, + } + + ## + # Maps Logger log levels to their values so we can silence. + + LOGGER_LEVEL_MAP = {} + + LOGGER_MAP.each_key do |key| + LOGGER_LEVEL_MAP[key] = ::Logger.const_get key.to_s.upcase + end + + ## + # Maps Logger log level values to syslog log levels. + + LEVEL_LOGGER_MAP = {} + + LOGGER_LEVEL_MAP.invert.each do |level, severity| + LEVEL_LOGGER_MAP[level] = LOGGER_MAP[severity] + end + + ## + # Builds a methods for level +meth+. + + def self.make_methods(meth) + eval <<-EOM, nil, __FILE__, __LINE__ + 1 + def #{meth}(message = nil) + return true if #{LOGGER_LEVEL_MAP[meth]} < @level + SYSLOG.#{LOGGER_MAP[meth]} clean(message || yield) + return true + end + + def #{meth}? + @level <= ::Logger::#{meth.to_s.upcase} + end + EOM + end + + LOGGER_MAP.each_key do |level| + make_methods level + end + + ## + # Log level for Logger compatibility. + + attr_accessor :level + + ## + # Fills in variables for Logger compatibility. If this is the first + # instance of SyslogLogger, +program_name+ may be set to change the logged + # program name. + # + # Due to the way syslog works, only one program name may be chosen. + + def initialize(program_name = RbConfig::CONFIG["ruby_install_name"]) + @level = ::Logger::DEBUG + + return if defined? SYSLOG + self.class.const_set :SYSLOG, Syslog.open(program_name) + end + + ## + # Almost duplicates Logger#add. +progname+ is ignored. + + def add(severity, message = nil, progname = nil, &block) + severity ||= ::Logger::UNKNOWN + return true if severity < @level + SYSLOG.send LEVEL_LOGGER_MAP[severity], clean(message || block.call) + return true + end + + private + + ## + # Clean up messages so they're nice and pretty. + + def clean(message) + message = message.to_s.dup + message.strip! + message.gsub!(/%/, '%%') # syslog(3) freaks on % (printf) + message.gsub!(/\e\[[^m]*m/, '') # remove useless ansi color codes + return message + end + end +end diff --git a/test/syslog/test_syslog_logger.rb b/test/syslog/test_syslog_logger.rb new file mode 100644 index 0000000..70d4c38 --- /dev/null +++ b/test/syslog/test_syslog_logger.rb @@ -0,0 +1,492 @@ +require 'minitest/autorun' +require 'tempfile' +require 'syslog/logger' + +module Syslog +module MockSyslog; end + +class << MockSyslog + + @line = nil + + Syslog::Logger::LOGGER_MAP.values.uniq.each do |level| + eval <<-EOM + def #{level}(message) + @line = "#{level.to_s.upcase} - \#{message}" + end + EOM + end + + attr_reader :line + attr_reader :program_name + + def open(program_name) + @program_name = program_name + end + + def reset + @line = '' + end + +end + +Syslog::Logger.const_set :SYSLOG, MockSyslog + +class TestLogger < MiniTest::Unit::TestCase + + LEVEL_LABEL_MAP = { + ::Logger::DEBUG => 'DEBUG', + ::Logger::INFO => 'INFO', + ::Logger::WARN => 'WARN', + ::Logger::ERROR => 'ERROR', + ::Logger::FATAL => 'FATAL', + ::Logger::UNKNOWN => 'ANY', + } + + def setup + @logger = ::Logger.new(nil) + end + + class Log + attr_reader :line, :label, :datetime, :pid, :severity, :progname, :msg + def initialize(line) + @line = line + /\A(\w+), \[([^#]*)#(\d+)\]\s+(\w+) -- (\w*): ([\x0-\xff]*)/ =~ @line + @label, @datetime, @pid, @severity, @progname, @msg = $1, $2, $3, $4, $5, $6 + end + end + + def log_add(severity, msg, progname = nil, &block) + log(:add, severity, msg, progname, &block) + end + + def log(msg_id, *arg, &block) + Log.new(log_raw(msg_id, *arg, &block)) + end + + def log_raw(msg_id, *arg, &block) + logdev = Tempfile.new(File.basename(__FILE__) + '.log') + @logger.instance_eval { @logdev = ::Logger::LogDevice.new(logdev) } + assert_equal true, @logger.__send__(msg_id, *arg, &block) + logdev.open + msg = logdev.read + logdev.close + msg + end + + def test_initialize + assert_equal ::Logger::DEBUG, @logger.level + end + + def test_add + msg = log_add nil, 'unknown level message' # nil == unknown + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + msg = log_add ::Logger::FATAL, 'fatal level message' + assert_equal LEVEL_LABEL_MAP[::Logger::FATAL], msg.severity + + msg = log_add ::Logger::ERROR, 'error level message' + assert_equal LEVEL_LABEL_MAP[::Logger::ERROR], msg.severity + + msg = log_add ::Logger::WARN, 'warn level message' + assert_equal LEVEL_LABEL_MAP[::Logger::WARN], msg.severity + + msg = log_add ::Logger::INFO, 'info level message' + assert_equal LEVEL_LABEL_MAP[::Logger::INFO], msg.severity + + msg = log_add ::Logger::DEBUG, 'debug level message' + assert_equal LEVEL_LABEL_MAP[::Logger::DEBUG], msg.severity + end + + def test_add_level_unknown + @logger.level = ::Logger::UNKNOWN + + msg = log_add nil, 'unknown level message' # nil == unknown + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + msg = log_add ::Logger::FATAL, 'fatal level message' + assert_equal '', msg.line + + msg = log_add ::Logger::ERROR, 'error level message' + assert_equal '', msg.line + + msg = log_add ::Logger::WARN, 'warn level message' + assert_equal '', msg.line + + msg = log_add ::Logger::INFO, 'info level message' + assert_equal '', msg.line + + msg = log_add ::Logger::DEBUG, 'debug level message' + assert_equal '', msg.line + end + + def test_add_level_fatal + @logger.level = ::Logger::FATAL + + msg = log_add nil, 'unknown level message' # nil == unknown + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + msg = log_add ::Logger::FATAL, 'fatal level message' + assert_equal LEVEL_LABEL_MAP[::Logger::FATAL], msg.severity + + msg = log_add ::Logger::ERROR, 'error level message' + assert_equal '', msg.line + + msg = log_add ::Logger::WARN, 'warn level message' + assert_equal '', msg.line + + msg = log_add ::Logger::INFO, 'info level message' + assert_equal '', msg.line + + msg = log_add ::Logger::DEBUG, 'debug level message' + assert_equal '', msg.line + end + + def test_add_level_error + @logger.level = ::Logger::ERROR + + msg = log_add nil, 'unknown level message' # nil == unknown + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + msg = log_add ::Logger::FATAL, 'fatal level message' + assert_equal LEVEL_LABEL_MAP[::Logger::FATAL], msg.severity + + msg = log_add ::Logger::ERROR, 'error level message' + assert_equal LEVEL_LABEL_MAP[::Logger::ERROR], msg.severity + + msg = log_add ::Logger::WARN, 'warn level message' + assert_equal '', msg.line + + msg = log_add ::Logger::INFO, 'info level message' + assert_equal '', msg.line + + msg = log_add ::Logger::DEBUG, 'debug level message' + assert_equal '', msg.line + end + + def test_add_level_warn + @logger.level = ::Logger::WARN + + msg = log_add nil, 'unknown level message' # nil == unknown + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + msg = log_add ::Logger::FATAL, 'fatal level message' + assert_equal LEVEL_LABEL_MAP[::Logger::FATAL], msg.severity + + msg = log_add ::Logger::ERROR, 'error level message' + assert_equal LEVEL_LABEL_MAP[::Logger::ERROR], msg.severity + + msg = log_add ::Logger::WARN, 'warn level message' + assert_equal LEVEL_LABEL_MAP[::Logger::WARN], msg.severity + + msg = log_add ::Logger::INFO, 'info level message' + assert_equal '', msg.line + + msg = log_add ::Logger::DEBUG, 'debug level message' + assert_equal '', msg.line + end + + def test_add_level_info + @logger.level = ::Logger::INFO + + msg = log_add nil, 'unknown level message' # nil == unknown + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + msg = log_add ::Logger::FATAL, 'fatal level message' + assert_equal LEVEL_LABEL_MAP[::Logger::FATAL], msg.severity + + msg = log_add ::Logger::ERROR, 'error level message' + assert_equal LEVEL_LABEL_MAP[::Logger::ERROR], msg.severity + + msg = log_add ::Logger::WARN, 'warn level message' + assert_equal LEVEL_LABEL_MAP[::Logger::WARN], msg.severity + + msg = log_add ::Logger::INFO, 'info level message' + assert_equal LEVEL_LABEL_MAP[::Logger::INFO], msg.severity + + msg = log_add ::Logger::DEBUG, 'debug level message' + assert_equal '', msg.line + end + + def test_add_level_debug + @logger.level = ::Logger::DEBUG + + msg = log_add nil, 'unknown level message' # nil == unknown + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + msg = log_add ::Logger::FATAL, 'fatal level message' + assert_equal LEVEL_LABEL_MAP[::Logger::FATAL], msg.severity + + msg = log_add ::Logger::ERROR, 'error level message' + assert_equal LEVEL_LABEL_MAP[::Logger::ERROR], msg.severity + + msg = log_add ::Logger::WARN, 'warn level message' + assert_equal LEVEL_LABEL_MAP[::Logger::WARN], msg.severity + + msg = log_add ::Logger::INFO, 'info level message' + assert_equal LEVEL_LABEL_MAP[::Logger::INFO], msg.severity + + msg = log_add ::Logger::DEBUG, 'debug level message' + assert_equal LEVEL_LABEL_MAP[::Logger::DEBUG], msg.severity + end + + def test_unknown + msg = log :unknown, 'unknown level message' + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + @logger.level = ::Logger::UNKNOWN + msg = log :unknown, 'unknown level message' + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + @logger.level = ::Logger::FATAL + msg = log :unknown, 'unknown level message' + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + @logger.level = ::Logger::ERROR + msg = log :unknown, 'unknown level message' + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + @logger.level = ::Logger::WARN + msg = log :unknown, 'unknown level message' + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + @logger.level = ::Logger::INFO + msg = log :unknown, 'unknown level message' + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + + @logger.level = ::Logger::DEBUG + msg = log :unknown, 'unknown level message' + assert_equal LEVEL_LABEL_MAP[::Logger::UNKNOWN], msg.severity + end + + def test_fatal + msg = log :fatal, 'fatal level message' + assert_equal LEVEL_LABEL_MAP[::Logger::FATAL], msg.severity + + @logger.level = ::Logger::UNKNOWN + msg = log :fatal, 'fatal level message' + assert_equal '', msg.line + + @logger.level = ::Logger::FATAL + msg = log :fatal, 'fatal level message' + assert_equal LEVEL_LABEL_MAP[::Logger::FATAL], msg.severity + + @logger.level = ::Logger::ERROR + msg = log :fatal, 'fatal level message' + assert_equal LEVEL_LABEL_MAP[::Logger::FATAL], msg.severity + + @logger.level = ::Logger::WARN + msg = log :fatal, 'fatal level message' + assert_equal LEVEL_LABEL_MAP[::Logger::FATAL], msg.severity + + @logger.level = ::Logger::INFO + msg = log :fatal, 'fatal level message' + assert_equal LEVEL_LABEL_MAP[::Logger::FATAL], msg.severity + + @logger.level = ::Logger::DEBUG + msg = log :fatal, 'fatal level message' + assert_equal LEVEL_LABEL_MAP[::Logger::FATAL], msg.severity + end + + def test_fatal_eh + @logger.level = ::Logger::FATAL + assert_equal true, @logger.fatal? + + @logger.level = ::Logger::UNKNOWN + assert_equal false, @logger.fatal? + end + + def test_error + msg = log :error, 'error level message' + assert_equal LEVEL_LABEL_MAP[::Logger::ERROR], msg.severity + + @logger.level = ::Logger::UNKNOWN + msg = log :error, 'error level message' + assert_equal '', msg.line + + @logger.level = ::Logger::FATAL + msg = log :error, 'error level message' + assert_equal '', msg.line + + @logger.level = ::Logger::ERROR + msg = log :error, 'error level message' + assert_equal LEVEL_LABEL_MAP[::Logger::ERROR], msg.severity + + @logger.level = ::Logger::WARN + msg = log :error, 'error level message' + assert_equal LEVEL_LABEL_MAP[::Logger::ERROR], msg.severity + + @logger.level = ::Logger::INFO + msg = log :error, 'error level message' + assert_equal LEVEL_LABEL_MAP[::Logger::ERROR], msg.severity + + @logger.level = ::Logger::DEBUG + msg = log :error, 'error level message' + assert_equal LEVEL_LABEL_MAP[::Logger::ERROR], msg.severity + end + + def test_error_eh + @logger.level = ::Logger::ERROR + assert_equal true, @logger.error? + + @logger.level = ::Logger::FATAL + assert_equal false, @logger.error? + end + + def test_warn + msg = log :warn, 'warn level message' + assert_equal LEVEL_LABEL_MAP[::Logger::WARN], msg.severity + + @logger.level = ::Logger::UNKNOWN + msg = log :warn, 'warn level message' + assert_equal '', msg.line + + @logger.level = ::Logger::FATAL + msg = log :warn, 'warn level message' + assert_equal '', msg.line + + @logger.level = ::Logger::ERROR + msg = log :warn, 'warn level message' + assert_equal '', msg.line + + @logger.level = ::Logger::WARN + msg = log :warn, 'warn level message' + assert_equal LEVEL_LABEL_MAP[::Logger::WARN], msg.severity + + @logger.level = ::Logger::INFO + msg = log :warn, 'warn level message' + assert_equal LEVEL_LABEL_MAP[::Logger::WARN], msg.severity + + @logger.level = ::Logger::DEBUG + msg = log :warn, 'warn level message' + assert_equal LEVEL_LABEL_MAP[::Logger::WARN], msg.severity + end + + def test_warn_eh + @logger.level = ::Logger::WARN + assert_equal true, @logger.warn? + + @logger.level = ::Logger::ERROR + assert_equal false, @logger.warn? + end + + def test_info + msg = log :info, 'info level message' + assert_equal LEVEL_LABEL_MAP[::Logger::INFO], msg.severity + + @logger.level = ::Logger::UNKNOWN + msg = log :info, 'info level message' + assert_equal '', msg.line + + @logger.level = ::Logger::FATAL + msg = log :info, 'info level message' + assert_equal '', msg.line + + @logger.level = ::Logger::ERROR + msg = log :info, 'info level message' + assert_equal '', msg.line + + @logger.level = ::Logger::WARN + msg = log :info, 'info level message' + assert_equal '', msg.line + + @logger.level = ::Logger::INFO + msg = log :info, 'info level message' + assert_equal LEVEL_LABEL_MAP[::Logger::INFO], msg.severity + + @logger.level = ::Logger::DEBUG + msg = log :info, 'info level message' + assert_equal LEVEL_LABEL_MAP[::Logger::INFO], msg.severity + end + + def test_info_eh + @logger.level = ::Logger::INFO + assert_equal true, @logger.info? + + @logger.level = ::Logger::WARN + assert_equal false, @logger.info? + end + + def test_debug + msg = log :debug, 'debug level message' + assert_equal LEVEL_LABEL_MAP[::Logger::DEBUG], msg.severity + + @logger.level = ::Logger::UNKNOWN + msg = log :debug, 'debug level message' + assert_equal '', msg.line + + @logger.level = ::Logger::FATAL + msg = log :debug, 'debug level message' + assert_equal '', msg.line + + @logger.level = ::Logger::ERROR + msg = log :debug, 'debug level message' + assert_equal '', msg.line + + @logger.level = ::Logger::WARN + msg = log :debug, 'debug level message' + assert_equal '', msg.line + + @logger.level = ::Logger::INFO + msg = log :debug, 'debug level message' + assert_equal '', msg.line + + @logger.level = ::Logger::DEBUG + msg = log :debug, 'debug level message' + assert_equal LEVEL_LABEL_MAP[::Logger::DEBUG], msg.severity + end + + def test_debug_eh + @logger.level = ::Logger::DEBUG + assert_equal true, @logger.debug? + + @logger.level = ::Logger::INFO + assert_equal false, @logger.debug? + end + +end + +class TestSyslogLogger < TestLogger + + def setup + super + @logger = Syslog::Logger.new + end + + class Log + attr_reader :line, :label, :datetime, :pid, :severity, :progname, :msg + def initialize(line) + @line = line + return unless /\A(\w+) - (.*)\Z/ =~ @line + severity, @msg = $1, $2 + severity = Syslog::Logger::LOGGER_MAP.invert[severity.downcase.intern] + @severity = severity.to_s.upcase + @severity = 'ANY' if @severity == 'UNKNOWN' + end + end + + def log_add(severity, msg, progname = nil, &block) + log(:add, severity, msg, progname, &block) + end + + def log(msg_id, *arg, &block) + Log.new(log_raw(msg_id, *arg, &block)) + end + + def log_raw(msg_id, *arg, &block) + assert_equal true, @logger.__send__(msg_id, *arg, &block) + msg = MockSyslog.line + MockSyslog.reset + return msg + end + + def test_unknown_eh + @logger.level = ::Logger::UNKNOWN + assert_equal true, @logger.unknown? + + @logger.level = ::Logger::UNKNOWN + 1 + assert_equal false, @logger.unknown? + end + +end +end