# frozen_string_literal: true

require 'logger'
require 'delegate'

module Airbrake
  # Decorator for +Logger+ from stdlib. Endows loggers the ability to both log
  # and report errors to Airbrake.
  #
  # @example
  #   # Create a logger like you normally do and decorate it.
  #   logger = Airbrake::AirbrakeLogger.new(Logger.new($stdout))
  #
  #   # Just use the logger like you normally do.
  #   logger.fatal('oops')
  class AirbrakeLogger < SimpleDelegator
    # @example
    #   # Assign a custom Airbrake notifier
    #   logger.airbrake_notifier = Airbrake::NoticeNotifier.new
    # @return [Airbrake::Notifier] notifier to be used to send notices
    attr_accessor :airbrake_notifier

    # @return [Integer]
    attr_reader :airbrake_level

    def initialize(logger)
      super

      __setobj__(logger)
      @airbrake_notifier = Airbrake
      self.level = logger.level
    end

    # @see Logger#warn
    def warn(progname = nil, &block)
      notify_airbrake(Logger::WARN, progname)
      super
    end

    # @see Logger#error
    def error(progname = nil, &block)
      notify_airbrake(Logger::ERROR, progname)
      super
    end

    # @see Logger#fatal
    def fatal(progname = nil, &block)
      notify_airbrake(Logger::FATAL, progname)
      super
    end

    # @see Logger#unknown
    def unknown(progname = nil, &block)
      notify_airbrake(Logger::UNKNOWN, progname)
      super
    end

    # @see Logger#level=
    def level=(value)
      self.airbrake_level = value < Logger::WARN ? Logger::WARN : value
      super
    end

    # Sets airbrake severity level. Does not permit values below `Logger::WARN`.
    #
    # @example
    #   logger.airbrake_level = Logger::FATAL
    # @return [void]
    def airbrake_level=(level)
      if level < Logger::WARN
        raise "Airbrake severity level #{level} is not allowed. " \
              "Minimum allowed level is #{Logger::WARN}"
      end
      @airbrake_level = level
    end

    private

    def notify_airbrake(severity, progname)
      return if severity < @airbrake_level || !@airbrake_notifier

      @airbrake_notifier.notify(progname) do |notice|
        # Get rid of unwanted internal Logger frames. Examples:
        # * /ruby-2.4.0/lib/ruby/2.4.0/logger.rb
        # * /gems/activesupport-4.2.7.1/lib/active_support/logger.rb
        backtrace = notice[:errors].first[:backtrace]
        notice[:errors].first[:backtrace] =
          backtrace.drop_while { |frame| frame[:file] =~ %r{/logger.rb\z} }

        notice[:context][:component] = 'log'
        notice[:context][:severity] = normalize_severity(severity)
      end
    end

    def normalize_severity(severity)
      (case severity
       when Logger::WARN then 'warning'
       when Logger::ERROR, Logger::UNKNOWN then 'error'
       when Logger::FATAL then 'critical'
       else
         raise "Unknown airbrake severity: #{severity}"
       end).freeze
    end
  end
end
