lib/loog.rb



# frozen_string_literal: true

# SPDX-FileCopyrightText: Copyright (c) 2018-2025 Yegor Bugayenko
# SPDX-License-Identifier: MIT

require 'logger'
require 'time'

# Loog is an object-oriented wrapper around Ruby Logger:
#
#  require 'loog'
#  log = Loog::VERBOSE
#  log.info('Hello, world!')
#
# For more information read
# {README}[https://github.com/yegor256/loog/blob/master/README.md] file.
#
# Author:: Yegor Bugayenko (yegor256@gmail.com)
# Copyright:: Copyright (c) 2018-2025 Yegor Bugayenko
# License:: MIT
module Loog
  # Compact formatter
  COMPACT = proc do |severity, _time, _target, msg|
    prefix = ''
    case severity
    when 'ERROR', 'FATAL'
      prefix = 'E: '
    when 'DEBUG'
      prefix = 'D: '
    end
    message = msg.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
    "#{prefix}#{message.rstrip.gsub("\n", "\n#{' ' * prefix.length}")}\n"
  end

  # Short formatter
  SHORT = proc do |_severity, _time, _target, msg|
    message = msg.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
    "#{message.rstrip}\n"
  end

  # Full formatter
  FULL = proc do |severity, time, _target, msg|
    message = msg.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
    format(
      "%<time>s %<severity>5s %<msg>s\n",
      time: time.utc.iso8601,
      severity: severity,
      msg: message.rstrip
    )
  end

  # No logging at all
  NULL = Logger.new($stdout)
  NULL.level = Logger::UNKNOWN
  NULL.freeze

  # Everything, including debug
  VERBOSE = Logger.new($stdout)
  VERBOSE.level = Logger::DEBUG
  VERBOSE.formatter = COMPACT
  VERBOSE.freeze

  # Info and errors, no debug info
  REGULAR = Logger.new($stdout)
  REGULAR.level = Logger::INFO
  REGULAR.formatter = COMPACT
  REGULAR.freeze

  # Errors only
  ERRORS = Logger.new($stdout)
  ERRORS.level = Logger::ERROR
  ERRORS.formatter = COMPACT
  ERRORS.freeze

  # Accumulator of everything. This class may be used
  # for testing, when it's necessary to accumulate all log
  # messages in one place and then "assert" the presence
  # of certain strings inside them.
  class Buffer < Logger
    # Ctor
    # @param [String] level The level of logging
    # @param [String] formatter The formatter
    def initialize(level: Logger::DEBUG, formatter: Loog::SHORT)
      super(
        $stdout,
        level: level,
        formatter: proc do |severity, time, target, msg|
          @lines.push(formatter.call(severity, time, target, msg))
          ''
        end
      )
      @lines = []
    end

    def to_s
      @lines.map { |s| s.dup.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') }.join
    end
  end
end