lib/roda/plugins/common_logger.rb



# frozen-string-literal: true

#
class Roda
  module RodaPlugins
    # The common_logger plugin adds common logger support to Roda
    # applications, similar to Rack::CommonLogger, with the following
    # differences:
    #
    # * Better performance
    # * Doesn't include middleware timing
    # * Doesn't proxy the body
    # * Doesn't support different capitalization of the Content-Length response header
    # * Logs to +$stderr+ instead of <tt>env['rack.errors']</tt> if explicit logger not passed
    #
    # Example:
    #
    #   plugin :common_logger
    #   plugin :common_logger, $stdout
    #   plugin :common_logger, Logger.new('filename')
    #   plugin :common_logger, Logger.new('filename'), method: :debug
    module CommonLogger
      MUTATE_LINE = RUBY_VERSION < '2.3' || RUBY_VERSION >= '3'
      private_constant :MUTATE_LINE

      def self.configure(app, logger=nil, opts=OPTS)
        app.opts[:common_logger] = logger || app.opts[:common_logger] || $stderr
        app.opts[:common_logger_meth] = app.opts[:common_logger].method(opts.fetch(:method){logger.respond_to?(:write) ? :write : :<<})
      end

      if RUBY_VERSION >= '2.1'
        # A timer object for calculating elapsed time.
        def self.start_timer
          Process.clock_gettime(Process::CLOCK_MONOTONIC)
        end
      else
        # :nocov:
        def self.start_timer # :nodoc:
          Time.now
        end
        # :nocov:
      end

      module InstanceMethods
        private

        # Log request/response information in common log format to logger.
        def _roda_after_90__common_logger(result)
          return unless result && (status = result[0]) && (headers = result[1])

          elapsed_time = if timer = @_request_timer
            '%0.4f' % (CommonLogger.start_timer - timer)
          else 
            '-'
          end

          env = @_request.env

          line = "#{env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-"} - #{env["REMOTE_USER"] || "-"} [#{Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")}] \"#{env["REQUEST_METHOD"]} #{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}#{"?#{env["QUERY_STRING"]}" if ((qs = env["QUERY_STRING"]) && !qs.empty?)} #{@_request.http_version}\" #{status} #{((length = headers[RodaResponseHeaders::CONTENT_LENGTH]) && (length unless length == '0')) || '-'} #{elapsed_time}\n"
          if MUTATE_LINE
            line.gsub!(/[^[:print:]\n]/){|c| sprintf("\\x%x", c.ord)}
          # :nocov:
          else
            line = line.gsub(/[^[:print:]\n]/){|c| sprintf("\\x%x", c.ord)}
          # :nocov:
          end
          opts[:common_logger_meth].call(line)
        end

        # Create timer instance used for timing
        def _roda_before_05__common_logger
          @_request_timer = CommonLogger.start_timer
        end
      end
    end

    register_plugin(:common_logger, CommonLogger)
  end
end