# coding: utf-8 require 'logger' require 'delegate' module RDF; module Util ## # Helpers for logging errors, warnings and debug information. # # Modules must provide `@logger`, which returns an instance of `Logger`, or something responding to `#<<`. Logger may also be specified using an `@options` hash containing a `:logger` entry. # @since 2.0.0 module Logger # The IOWrapper class is used to store per-logger state while wrapping an IO such as $stderr. IOWrapper = DelegateClass(IO) ## # Logger instance, found using `options[:logger]`, `@logger`, or `@options[:logger]` # @param [Hash{Symbol => Object}] options # @option options [Logger, #<<] :logger # @return [Logger, #write, #<<] def logger(**options) logger = options.fetch(:logger, @logger) logger = @options[:logger] if logger.nil? && @options if logger.nil? # Unless otherwise specified, use $stderr logger = (@options || options)[:logger] = IOWrapper.new($stderr) # Reset log_statistics so that it's not inherited across different instances logger.log_statistics.clear if logger.respond_to?(:log_statistics) end logger = (@options || options)[:logger] = ::Logger.new(::File.open(::File::NULL, "w")) unless logger # Incase false was used, which is frozen logger.extend(LoggerBehavior) unless logger.is_a?(LoggerBehavior) logger end ## # Number of times logger has been called at each level # @param [Hash{Symbol => Object}] options # @option options [Logger, #<<] :logger # @return [Hash{Symbol => Integer}] def log_statistics(**options) logger(**options).log_statistics end ## # Used for fatal errors where processing cannot continue. If `logger` is not configured, it logs to `$stderr`. # # @overload log_fatal(*args, **options, &block) # @param [Array] args # @param [Array] args Messages # @param [:fatal, :error, :warn, :info, :debug] level (:fatal) # @param [Hash{Symbol => Object}] options # @option options [Integer] :depth # Recursion depth for indenting output # @option options [Integer] :lineno associated with message # @option options [Logger, #<<] :logger # @option options [Class] :exception, (StandardError) # Exception class used for raising error # @yieldreturn [String] added to message # @return [void] # @raise Raises the provided exception class using the first element from args as the message component. def log_fatal(*args, level: :fatal, **options, &block) logger_common(*args, "Called from #{Gem.location_of_caller.join(':')}", level: level, **options, &block) raise options.fetch(:exception, StandardError), args.first end ## # Used for non-fatal errors where processing can continue. If `logger` is not configured, it logs to `$stderr`. # # As a side-effect of setting `@logger_in_error`, which will suppress further error messages until cleared using {#log_recover}. # # @overload log_error(*args, **options, &block) # @param [Array] args # @param [Array] args Messages # @param [:fatal, :error, :warn, :info, :debug] level (:error) # @param [Hash{Symbol => Object}] options # @option options [Integer] :depth # Recursion depth for indenting output # @option options [:fatal, :error, :warn, :info, :debug] level (:<<) # @option options [Integer] :lineno associated with message # @option options [Logger, #<<] :logger # @option options [Class] :exception, (StandardError) # Exception class used for raising error # @yieldreturn [String] added to message # @return [void] # @raise Raises the provided exception class using the first element from args as the message component, if `:exception` option is provided. def log_error(*args, level: :error, **options, &block) logger = self.logger(**options) return if logger.recovering logger.recovering = true logger_common(*args, level: level, **options, &block) raise options[:exception], args.first if options[:exception] end # In recovery mode? When `log_error` is called, we enter recovery mode. This is cleared when `log_recover` is called. # @param [Hash{Symbol => Object}] options # @option options [Logger, #<<] :logger # @return [Boolean] def log_recovering?(**options) self.logger(**options).recovering end ## # Warning message. # # @overload log_warn(*args, **options, &block) # @param [Array] args # @param [Array] args Messages # @param [:fatal, :error, :warn, :info, :debug] level (:warn) # @param [Hash{Symbol => Object}] options # @option options [Integer] :depth # Recursion depth for indenting output # @option options [:fatal, :error, :warn, :info, :debug] level (:<<) # @option options [Integer] :lineno associated with message # @option options [Logger, #<<] :logger # @yieldreturn [String] added to message # @return [void] def log_warn(*args, level: :warn, **options, &block) logger_common(*args, level: level, **options, &block) end ## # Recovers from an error condition. If `args` are passed, sent as an informational message # # As a side-effect of clearing `@logger_in_error`. # # @overload log_recover(*args, **options, &block) # @param [Array] args # @param [Array] args Messages # @param [:fatal, :error, :warn, :info, :debug] level (:info) # @param [Hash{Symbol => Object}] options # @option options [Integer] :depth # Recursion depth for indenting output # @option options [Integer] :lineno associated with message # @option options [Logger, #<<] :logger # @yieldreturn [String] added to message # @return [void] def log_recover(*args, level: :info, **options, &block) logger = self.logger(**options) logger.recovering = false return if args.empty? && !block_given? logger_common(*args, level: level, **options, &block) end ## # Informational message. # # @overload log_info(*args, **options, &block) # @param [Array] args # @param [Array] args Messages # @param [:fatal, :error, :warn, :info, :debug] level (:info) # @param [Hash{Symbol => Object}] options # @option options [Integer] :depth # Recursion depth for indenting output # @option options [Integer] :lineno associated with message # @option options [Logger, #<<] :logger # @yieldreturn [String] added to message # @return [void] def log_info(*args, level: :info, **options, &block) logger_common(*args, level: level, **options, &block) end ## # Debug message. # # @overload log_debug(*args, **options, &block) # @param [Array] args # @param [Array] args Messages # @param [:fatal, :error, :warn, :info, :debug] level (:debug) # @param [Hash{Symbol => Object}] options # @option options [Integer] :depth # Recursion depth for indenting output # @option options [Integer] :lineno associated with message # @option options [Logger, #<<] :logger # @yieldreturn [String] added to message # @return [void] def log_debug(*args, level: :debug, **options, &block) logger_common(*args, level: level, **options, &block) end ## # @overload log_depth(options, &block) # Increase depth around a method invocation # @param [Hash{Symbol}] options (@options || {}) # @option options [Integer] :depth Additional recursion depth # @option options [Logger, #<<] :logger # @yield # Yields with no arguments # @yieldreturn [Object] returns the result of yielding # @return [Object] # # @overload log_depth # # Return the current log depth # @return [Integer] def log_depth(**options, &block) self.logger(**options).log_depth(&block) end private LOGGER_COMMON_LEVELS = { fatal: 4, error: 3, warn: 2, info: 1, debug: 0 }.freeze LOGGER_COMMON_LEVELS_REVERSE = LOGGER_COMMON_LEVELS.invert.freeze ## # Common method for logging messages # # The call is ignored, unless `@logger` or `@options[:logger]` is set, in which case it records tracing information as indicated. # # @overload logger_common(args, options) # @param [Array] args Messages # @param [:fatal, :error, :warn, :info, :debug] level # @param [Hash{Symbol => Object}] options # @option options [Integer] :depth # Recursion depth for indenting output # @option options [:fatal, :error, :warn, :info, :debug] level (:<<) # @option options [Integer] :lineno associated with message # @option options [Logger, #<<] :logger # @yieldreturn [String] added to message # @return [void] def logger_common(*args, level:, **options) logger = self.logger(**options) # Some older code uses integer level numbers level = LOGGER_COMMON_LEVELS_REVERSE.fetch(level) if level.is_a?(Integer) logger.log_statistics[level] = logger.log_statistics[level].to_i + 1 return if logger.level > LOGGER_COMMON_LEVELS.fetch(level) depth = options.fetch(:depth, logger.log_depth) args << yield if block_given? str = (depth > 100 ? ' ' * 100 + '+' : ' ' * depth) + args.join(": ") str = "[line #{options[:lineno]}] #{str}" if options[:lineno] logger.send(level, str) end ## # Module which is mixed-in to found logger to provide statistics and depth behavior module LoggerBehavior attr_accessor :recovering def log_statistics @logger_statistics ||= {} end ## # @overload log_depth(options, &block) # Increase depth around a method invocation # @param [Integer] depth (1) recursion depth # @param [Hash{Symbol}] options (@options || {}) # @option options [Logger, #<<] :logger # @yield # Yields with no arguments # @yieldreturn [Object] returns the result of yielding # @return [Object] # # @overload log_depth # # Return the current log depth # @return [Integer] def log_depth(depth: 1, **options) @log_depth ||= 0 if block_given? @log_depth += depth yield else @log_depth end ensure @log_depth -= depth if block_given? end # Give Logger like behavior to non-logger objects def method_missing(method, *args) case method.to_sym when :fatal, :error, :warn, :info, :debug if self.respond_to?(:write) self.write "#{method.to_s.upcase} #{(args.join(": "))}\n" elsif self.respond_to?(:<<) self << "#{method.to_s.upcase} #{args.join(": ")}" else # Silently eat the message end when :level, :sev_threshold then 2 else super end end def respond_to_missing?(name, include_private = false) return true if %i(fatal error warn info debug level sev_threshold) .include?(name.to_sym) super end end end # Logger end; end # RDF::Util