smartinez87/exception_notification

View on GitHub
lib/exception_notifier/datadog_notifier.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

require 'action_dispatch'

module ExceptionNotifier
  class DatadogNotifier < BaseNotifier
    attr_reader :client,
                :default_options

    def initialize(options)
      super
      @client = options.fetch(:client)
      @default_options = options
    end

    def call(exception, options = {})
      client.emit_event(
        datadog_event(exception, options)
      )
    end

    def datadog_event(exception, options = {})
      DatadogExceptionEvent.new(
        exception,
        options.reverse_merge(default_options)
      ).event
    end

    class DatadogExceptionEvent
      include ExceptionNotifier::BacktraceCleaner

      MAX_TITLE_LENGTH = 120
      MAX_VALUE_LENGTH = 300
      MAX_BACKTRACE_SIZE = 3
      ALERT_TYPE = 'error'

      attr_reader :exception,
                  :options

      def initialize(exception, options)
        @exception = exception
        @options = options
      end

      def request
        @request ||= ActionDispatch::Request.new(options[:env]) if options[:env]
      end

      def controller
        @controller ||= options[:env] && options[:env]['action_controller.instance']
      end

      def backtrace
        @backtrace ||= exception.backtrace ? clean_backtrace(exception) : []
      end

      def tags
        options[:tags] || []
      end

      def title_prefix
        options[:title_prefix] || ''
      end

      def event
        title = formatted_title
        body = formatted_body

        Dogapi::Event.new(
          body,
          msg_title: title,
          alert_type: ALERT_TYPE,
          tags: tags,
          aggregation_key: [title]
        )
      end

      def formatted_title
        title =
          "#{title_prefix}#{controller_subtitle} (#{exception.class}) #{exception.message.inspect}"

        truncate(title, MAX_TITLE_LENGTH)
      end

      def formatted_body
        text = []

        text << '%%%'
        text << formatted_request if request
        text << formatted_session if request
        text << formatted_backtrace
        text << '%%%'

        text.join("\n")
      end

      def formatted_key_value(key, value)
        "**#{key}:** #{value}"
      end

      def formatted_request
        text = []
        text << '### **Request**'
        text << formatted_key_value('URL', request.url)
        text << formatted_key_value('HTTP Method', request.request_method)
        text << formatted_key_value('IP Address', request.remote_ip)
        text << formatted_key_value('Parameters', request.filtered_parameters.inspect)
        text << formatted_key_value('Timestamp', Time.current)
        text << formatted_key_value('Server', Socket.gethostname)
        text << formatted_key_value('Rails root', Rails.root) if defined?(Rails) && Rails.respond_to?(:root)
        text << formatted_key_value('Process', $PROCESS_ID)
        text << '___'
        text.join("\n")
      end

      def formatted_session
        text = []
        text << '### **Session**'
        text << formatted_key_value('Data', request.session.to_hash)
        text << '___'
        text.join("\n")
      end

      def formatted_backtrace
        size = [backtrace.size, MAX_BACKTRACE_SIZE].min

        text = []
        text << '### **Backtrace**'
        text << '````'
        size.times { |i| text << backtrace[i] }
        text << '````'
        text << '___'
        text.join("\n")
      end

      def truncate(string, max)
        string.length > max ? "#{string[0...max]}..." : string
      end

      def inspect_object(object)
        case object
        when Hash, Array
          truncate(object.inspect, MAX_VALUE_LENGTH)
        else
          object.to_s
        end
      end

      private

      def controller_subtitle
        "#{controller.controller_name} #{controller.action_name}" if controller
      end
    end
  end
end