require 'fileutils' require 'logger' module Polytrix class Logger include ::Logger::Severity # @return [IO] the log device attr_reader :logdev # Constructs a new logger. # # @param options [Hash] configuration for a new logger # @option options [Symbol] :color color to use when when outputting # messages # @option options [Integer] :level the logging severity threshold # (default: `Polytrix::DEFAULT_LOG_LEVEL`) # @option options [String,IO] :logdev filepath String or IO object to be # used for logging (default: `nil`) # @option options [String] :progname program name to include in log # messages (default: `"Polytrix"`) # @option options [IO] :stdout a standard out IO object to use # (default: `$stdout`) def initialize(options = {}) color = options[:color] @loggers = [] @loggers << @logdev = logdev_logger(options[:logdev]) if options[:logdev] @loggers << stdout_logger(options[:stdout], color) if options[:stdout] @loggers << stdout_logger($stdout, color) if @loggers.empty? self.progname = options[:progname] || 'Polytrix' self.level = options[:level] || default_log_level end def self.new_logger(implementor) # (test, implementor, index) name = implementor.name # instance_name(test, implementor) index = Polytrix.implementors.index(implementor) || 0 Logger.new( stdout: STDOUT, color: Color::COLORS[index % Color::COLORS.size].to_sym, logdev: File.join(Polytrix.configuration.log_root, "#{name}.log"), level: Polytrix::Util.to_logger_level(Polytrix.configuration.log_level), progname: name ) end class << self private # @api private # @!macro delegate_to_first_logger # @method $1() def delegate_to_first_logger(meth) define_method(meth) { |*args| @loggers.first.public_send(meth, *args) } end # @api private # @!macro delegate_to_all_loggers # @method $1() def delegate_to_all_loggers(meth) define_method(meth) do |*args| result = nil @loggers.each { |l| result = l.public_send(meth, *args) } result end end end # @return [Integer] the logging severity threshold # @see http://is.gd/Okuy5p delegate_to_first_logger :level # Sets the logging severity threshold. # # @param level [Integer] the logging severity threshold # @see http://is.gd/H1VBFH delegate_to_all_loggers :level= # @return [String] program name to include in log messages # @see http://is.gd/5uHGK0 delegate_to_first_logger :progname # Sets the program name to include in log messages. # # @param progname [String] the program name to include in log messages # @see http://is.gd/f2U5Xj delegate_to_all_loggers :progname= # @return [String] the date format being used # @see http://is.gd/btmFWJ delegate_to_first_logger :datetime_format # Sets the date format being used. # # @param format [String] the date format # @see http://is.gd/M36ml8 delegate_to_all_loggers :datetime_format= # Log a message if the given severity is high enough. # # @see http://is.gd/5opBW0 delegate_to_all_loggers :add # Dump one or more messages to info. # # @param message [#to_s] the message to log # @see http://is.gd/BCp5KV delegate_to_all_loggers :<< # Log a message with severity of banner (high level). # # @param message_or_progname [#to_s] the message to log. In the block # form, this is the progname to use in the log message. # @yield evaluates to the message to log. This is not evaluated unless the # logger's level is sufficient to log the message. This allows you to # create potentially expensive logging messages that are only called when # the logger is configured to show them. # @return [nil,true] when the given severity is not high enough (for this # particular logger), log no message, and return true # @see http://is.gd/pYUCYU delegate_to_all_loggers :banner # Log a message with severity of debug. # # @param message_or_progname [#to_s] the message to log. In the block # form, this is the progname to use in the log message. # @yield evaluates to the message to log. This is not evaluated unless the # logger's level is sufficient to log the message. This allows you to # create potentially expensive logging messages that are only called when # the logger is configured to show them. # @return [nil,true] when the given severity is not high enough (for this # particular logger), log no message, and return true # @see http://is.gd/Re97Zp delegate_to_all_loggers :debug # @return [true,false] whether or not the current severity level # allows for the printing of debug messages # @see http://is.gd/Iq08xB delegate_to_first_logger :debug? # Log a message with severity of info. # # @param message_or_progname [#to_s] the message to log. In the block # form, this is the progname to use in the log message. # @yield evaluates to the message to log. This is not evaluated unless the # logger's level is sufficient to log the message. This allows you to # create potentially expensive logging messages that are only called when # the logger is configured to show them. # @return [nil,true] when the given severity is not high enough (for this # particular logger), log no message, and return true # @see http://is.gd/pYUCYU delegate_to_all_loggers :info # @return [true,false] whether or not the current severity level # allows for the printing of info messages # @see http://is.gd/lBtJkT delegate_to_first_logger :info? # Log a message with severity of error. # # @param message_or_progname [#to_s] the message to log. In the block # form, this is the progname to use in the log message. # @yield evaluates to the message to log. This is not evaluated unless the # logger's level is sufficient to log the message. This allows you to # create potentially expensive logging messages that are only called when # the logger is configured to show them. # @return [nil,true] when the given severity is not high enough (for this # particular logger), log no message, and return true # @see http://is.gd/mLwYMl delegate_to_all_loggers :error # @return [true,false] whether or not the current severity level # allows for the printing of error messages # @see http://is.gd/QY19JL delegate_to_first_logger :error? # Log a message with severity of warn. # # @param message_or_progname [#to_s] the message to log. In the block # form, this is the progname to use in the log message. # @yield evaluates to the message to log. This is not evaluated unless the # logger's level is sufficient to log the message. This allows you to # create potentially expensive logging messages that are only called when # the logger is configured to show them. # @return [nil,true] when the given severity is not high enough (for this # particular logger), log no message, and return true # @see http://is.gd/PX9AIS delegate_to_all_loggers :warn # @return [true,false] whether or not the current severity level # allows for the printing of warn messages # @see http://is.gd/Gdr4lD delegate_to_first_logger :warn? # Log a message with severity of fatal. # # @param message_or_progname [#to_s] the message to log. In the block # form, this is the progname to use in the log message. # @yield evaluates to the message to log. This is not evaluated unless the # logger's level is sufficient to log the message. This allows you to # create potentially expensive logging messages that are only called when # the logger is configured to show them. # @return [nil,true] when the given severity is not high enough (for this # particular logger), log no message, and return true # @see http://is.gd/5ElFPK delegate_to_all_loggers :fatal # @return [true,false] whether or not the current severity level # allows for the printing of fatal messages # @see http://is.gd/7PgwRl delegate_to_first_logger :fatal? # Log a message with severity of unknown. # # @param message_or_progname [#to_s] the message to log. In the block # form, this is the progname to use in the log message. # @yield evaluates to the message to log. This is not evaluated unless the # logger's level is sufficient to log the message. This allows you to # create potentially expensive logging messages that are only called when # the logger is configured to show them. # @return [nil,true] when the given severity is not high enough (for this # particular logger), log no message, and return true # @see http://is.gd/Y4hqpf delegate_to_all_loggers :unknown # Close the logging devices. # # @see http://is.gd/b13cVn delegate_to_all_loggers :close private # @return [Integer] the default logger level # @api private def default_log_level Polytrix::Util.to_logger_level(Polytrix.configuration.log_level) end # Construct a new standard out logger. # # @param stdout [IO] the IO object that represents stdout (or similar) # @param color [Symbol] color to use when outputing messages # @return [StdoutLogger] a new logger # @api private def stdout_logger(stdout, color) logger = StdoutLogger.new(stdout) if Polytrix.tty? logger.formatter = proc do |_severity, _datetime, _progname, msg| Color.colorize("#{msg}", color).concat("\n") end else logger.formatter = proc do |_severity, _datetime, _progname, msg| msg.concat("\n") end end logger end # Construct a new logdev logger. # # @param filepath_or_logdev [String,IO] a filepath String or IO object # @return [LogdevLogger] a new logger # @api private def logdev_logger(filepath_or_logdev) LogdevLogger.new(resolve_logdev(filepath_or_logdev)) end # Return an IO object from a filepath String or the IO object itself. # # @param filepath_or_logdev [String,IO] a filepath String or IO object # @return [IO] an IO object # @api private def resolve_logdev(filepath_or_logdev) if filepath_or_logdev.is_a? String FileUtils.mkdir_p(File.dirname(filepath_or_logdev)) file = File.open(File.expand_path(filepath_or_logdev), 'ab') file.sync = true file else filepath_or_logdev end end # Internal class which adds a #banner method call that displays the # message with a callout arrow. class LogdevLogger < ::Logger alias_method :super_info, :info # Dump one or more messages to info. # # @param msg [String] a message def <<(msg) @buffer ||= '' lines, _, remainder = msg.rpartition("\n") if lines.empty? @buffer << remainder else lines.insert(0, @buffer) lines.split("\n").each { |l| format_line(l.chomp) } @buffer = '' end end # Log a banner message. # # @param msg [String] a message def banner(msg = nil, &block) super_info("-----> #{msg}", &block) end private # Reformat a line if it already contains log formatting. # # @param line [String] a message line # @api private def format_line(line) case line when /^-----> / then banner(line.gsub(/^[ >-]{6} /, '')) when /^>>>>>> / then error(line.gsub(/^[ >-]{6} /, '')) when /^ / then info(line.gsub(/^[ >-]{6} /, '')) else info(line) end end end # Internal class which reformats logging methods for display as console # output. class StdoutLogger < LogdevLogger # Log a debug message # # @param msg [String] a message def debug(msg = nil, &block) super("D #{msg}", &block) end # Log an info message # # @param msg [String] a message def info(msg = nil, &block) super(" #{msg}", &block) end # Log a warn message # # @param msg [String] a message def warn(msg = nil, &block) super("$$$$$$ #{msg}", &block) end # Log an error message # # @param msg [String] a message def error(msg = nil, &block) super(">>>>>> #{msg}", &block) end # Log a fatal message # # @param msg [String] a message def fatal(msg = nil, &block) super("!!!!!! #{msg}", &block) end # Log an unknown message # # @param msg [String] a message def unknown(msg = nil, &block) super("?????? #{msg}", &block) end end end end