piotrmurach/tty-logger

View on GitHub
lib/tty/logger/handlers/console.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# frozen_string_literal: true

require "pastel"

require_relative "base"

module TTY
  class Logger
    module Handlers
      class Console
        include Base

        ARROW = "›"

        STYLES = {
          debug: {
            label: "debug",
            symbol: "•",
            color: :cyan,
            levelpad: 2
          },
          info: {
            label: "info",
            symbol: "ℹ",
            color: :green,
            levelpad: 3
          },
          warn: {
            label: "warning",
            symbol: "⚠",
            color: :yellow,
            levelpad: 0
          },
          error: {
            label: "error",
            symbol: "⨯",
            color: :red,
            levelpad: 2
          },
          fatal: {
            label: "fatal",
            symbol: "!",
            color: :red,
            levelpad: 2
          },
          success: {
            label: "success",
            symbol: "✔",
            color: :green,
            levelpad: 0
          },
          wait: {
            label: "waiting",
            symbol: "…",
            color: :cyan,
            levelpad: 0
          }
        }.freeze

        TEXT_REGEXP = /([{}()\[\]])?(["']?)(\S+?)(["']?=)/.freeze
        JSON_REGEXP = /\"([^,]+?)\"(?=:)/.freeze

        COLOR_PATTERNS = {
          text: [TEXT_REGEXP, ->(c) { "\\1\\2" + c.("\\3") + "\\4" }],
          json: [JSON_REGEXP, ->(c) { "\"" + c.("\\1") + "\"" }]
        }.freeze

        # The output stream
        # @api private
        attr_reader :output

        # The configuration options
        # @api private
        attr_reader :config

        # The logging level
        # @api private
        attr_reader :level

        # The format for the message
        # @api private
        attr_reader :message_format

        def initialize(output: $stderr, formatter: nil, config: nil, level: nil,
                       styles: {}, enable_color: nil, message_format: "%-25s")
          @output = Array[output].flatten
          @formatter = coerce_formatter(formatter || config.formatter).new
          @formatter_name = @formatter.class.name.split("::").last.downcase
          @color_pattern = COLOR_PATTERNS[@formatter_name.to_sym]
          @config = config
          @styles = styles
          @level = level || @config.level
          @mutex = Mutex.new
          @pastel = Pastel.new(enabled: enable_color)
          @message_format = message_format
        end

        # Handle log event output in format
        #
        # @param [Event] event
        #   the current event logged
        #
        # @api public
        def call(event)
          @mutex.lock

          style = configure_styles(event)
          color = configure_color(style)

          fmt = []
          metadata.each do |meta|
            case meta
            when :date
              fmt << @pastel.white("[" + event.metadata[:time].
                                   strftime(config.date_format) + "]")
            when :time
              fmt << @pastel.white("[" + event.metadata[:time].
                                   strftime(config.time_format) + "]")
            when :file
              fmt << @pastel.white("[#{format_filepath(event)}]")
            when :pid
              fmt << @pastel.white("[%d]" % event.metadata[:pid])
            else
              raise "Unknown metadata `#{meta}`"
            end
          end
          fmt << ARROW unless config.metadata.empty?
          unless style.empty?
            fmt << color.(style[:symbol])
            fmt << color.(style[:label]) + (" " * style[:levelpad])
          end
          fmt << message_format % event.message.join(" ")
          unless event.fields.empty?
            pattern, replacement = *@color_pattern
            fmt << @formatter.dump(event.fields, max_bytes: config.max_bytes,
                                                 max_depth: config.max_depth)
                             .gsub(pattern, replacement.(color))
          end
          unless event.backtrace.empty?
            fmt << "\n" + format_backtrace(event)
          end

          output.each { |out| out.puts fmt.join(" ") }
        ensure
          @mutex.unlock
        end

        private

        def format_backtrace(event)
          indent = " " * 4
          event.backtrace.map do |bktrace|
            indent + bktrace.to_s
          end.join("\n")
        end

        # Merge default styles with custom style overrides
        #
        # @return [Hash[String]]
        #   the style matching log type
        #
        # @api private
        def configure_styles(event)
          return {} if event.metadata[:name].nil?

          STYLES.fetch(event.metadata[:name].to_sym, {})
                .dup
                .merge!(@styles[event.metadata[:name].to_sym] || {})
        end

        def configure_color(style)
          color = style.fetch(:color) { :cyan }
          @pastel.send(color).detach
        end
      end # Console
    end # Handlers
  end # Logger
end # TTY