# # Copyright (c) 2011 RightScale Inc # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. require 'logger' module RightSupport::Log if require_succeeds?('syslog') # A logger that forwards log entries to the Unix syslog facility, but complies # with the interface of the Ruby Logger object and faithfully translates log # severities and other concepts. Provides optional cleanup/filtering in order # to keep the syslog from having weird characters or being susceptible to log # forgery. class SystemLogger < Logger SEVERITY_MAP = { UNKNOWN => :alert, FATAL => :err, ERROR => :warning, WARN => :notice, INFO => :info, DEBUG => :debug } # Translation table that maps human-readable syslog facility names to # integer constants used by the Syslog module. Compiled from the # Linux header file 'sys/syslog.h' # (see http://linux.die.net/include/sys/syslog.h) FACILITY_MAP = { 'kern' => (0<<3), 'user' => (1<<3), 'mail' => (2<<3), 'daemon' => (3<<3), 'auth' => (4<<3), 'syslog' => (5<<3), 'lpr' => (6<<3), 'news' => (7<<3), 'uucp' => (8<<3), 'cron' => (9<<3), 'authpriv' => (10<<3), 'ftp' => (11<<3), 'local0' => (16<<3), 'local1' => (17<<3), 'local2' => (18<<3), 'local3' => (19<<3), 'local4' => (20<<3), 'local5' => (21<<3), 'local6' => (22<<3), 'local7' => (23<<3), } DEFAULT_OPTIONS = { :split=>false, :color=>false, :facility=>'local0', :connection=>:autodetect } @@syslog = nil @@mutex = Mutex.new @@program_name = nil # Initialize this process's syslog facilities and construct a new syslog # logger object. # # === Parameters # program_name(String):: the syslog program name, 'ruby' by default # options(Hash):: (optional) configuration options to use, see below # # === Options # facility:: the syslog facility to use for messages, 'local0' by default # split(true|false):: if true, splits multi-line messages into separate syslog entries # color(true|false):: if true, passes ANSI escape sequences through to syslog # connection(type):: if :local, connects using local Unix socket. if :autodetect, # it may choose a remote connection if the 'syslog' host is known. otherwise # a URI such as 'tcp://127.0.0.1:514' can be passed explicitly. # error_handler(callback):: callback for write failures in order to give # the application a change to gracefully shutdown, etc. # default is to dump errors to STDERR and continue. # parameters = [exception] # currently only called by remote syslog # # === Raise # ArgumentError:: if an invalid facility is given def initialize(program_name='ruby', options={}) @options = DEFAULT_OPTIONS.merge(options) @level = Logger::DEBUG facility = FACILITY_MAP[@options[:facility].to_s] if facility.nil? raise ArgumentError, "Invalid facility '#{@options[:facility]}'" end @@mutex.synchronize do if @@syslog && (@@program_name != program_name) @@syslog.close ; @@syslog = nil end unless @@syslog if @options[:connection] == :autodetect # don't even want to know if this code is supported on older Ruby. # this logic is intended to help dockerization, which for us # begins at Ruby v2.1+ @options[:connection] = :local # local if all attempts fail if RUBY_VERSION >= '2.1' # only choose the 'syslog' host when explicitly declared in # '/etc/hosts'. use built-in Resolv module to check. begin require 'resolv' host = ::Resolv::Hosts.new.getaddress('syslog') begin ::TCPSocket.new(host, 514).close @options[:connection] = 'tcp://syslog:514' rescue ::Errno::ECONNREFUSED, Errno::EHOSTUNREACH # not listening on TCP port 514 end rescue ::Resolv::ResolvError # not set end end end case @options[:connection] when :local @@syslog = ::Syslog.open(program_name, nil, facility) else require ::File.expand_path('../syslog/remote', __FILE__) syslogger = ::RightSupport::Log::Syslog::Remote.new( @options[:connection], program_name, facility, :error_handler => @options[:error_handler]) syslogger.connect @@syslog = syslogger end end @@program_name = program_name end @@syslog.info("Connected to syslog: #{@options[:connection].inspect}") end # Log a message if the given severity is high enough. This is the generic # logging method. Users will be more inclined to use #debug, #info, #warn, # #error, and #fatal. # # === Parameters # severity(Integer):: one of the severity constants defined by Logger # message(Object):: the message to be logged # progname(String):: ignored, the program name is fixed at initialization # # === Block # If message is nil and a block is supplied, this method will yield to # obtain the log message. # # === Return # true:: always returns true # def add(severity, message = nil, progname = nil, &block) severity ||= UNKNOWN if @@syslog.nil? or severity < @level return true end progname ||= @progname if message.nil? if block_given? message = yield else message = progname progname = @progname end end parts = clean(message) parts.each { |part| emit_syslog(severity, part) } return true end # Emit a log entry at INFO severity. # # === Parameters # msg(Object):: the message to log # # === Return # true:: always returns true # def <<(msg) info(msg) end # Do nothing. This method is provided for Logger interface compatibility. # # === Return # true:: always returns true # def close return true end private # Call the syslog function to emit a syslog entry. # # === Parameters # severity(Integer):: one of the Logger severity constants # message(String):: the log message # # === Return # true:: always returns true def emit_syslog(severity, message) level = SEVERITY_MAP[severity] || SEVERITY_MAP[UNKNOWN] @@syslog.__send__(level, message) return true end # Perform cleanup, output escaping and splitting on message. # The operations that it performs can vary, depending on the # options that were passed to this logger at initialization # time. # # === Parameters # message(String):: raw log message # # === Return # log_lines([String]):: an array of String messages that should be logged separately to syslog def clean(message) message = message.to_s.dup message.strip! message.gsub!(/%/, '%%') # syslog(3) freaks on % (printf) unless @options[:color] message.gsub!(/\e\[[^m]*m/, '') # remove useless ansi color codes end if @options[:split] bits = message.split(/[\n\r]+/) else bits = [message] end return bits end end end end