DataDog/chef-handler-datadog

View on GitHub
lib/chef/handler/datadog_chef_events.rb

Summary

Maintainability
A
45 mins
Test Coverage
# encoding: utf-8
require 'rubygems'
require 'chef/handler'
require 'chef/mash'
require 'dogapi'

require_relative 'datadog_util'

# helper class for sending events about chef runs
class DatadogChefEvents
  include DatadogUtil

  attr_reader :event_title
  attr_reader :event_body

  def initialize
    @hostname = nil
    @run_status = nil
    @failure_notfications = nil

    @alert_type = ''
    @event_priority = ''
    @event_title = ''
    # TODO: refactor how event_body is constructed in the class methods
    #       handling of the event_body is a bit clunky and depends on the order of
    #       method calls
    @event_body = ''
  end

  # set the target hostname (chef node name)
  #
  # @param hostname [String] hostname to use for the handler report
  # @return [DatadogChefTags] instance reference to self enabling method chaining
  def with_hostname(hostname)
    @hostname = hostname
    self
  end

  # set the chef run status used for the report
  #
  # @param run_status [Chef::RunStatus] current chef run status
  # @return [DatadogChefTags] instance reference to self enabling method chaining
  def with_run_status(run_status)
    @run_status = run_status
    self
  end

  # set the failure notification list
  #
  # @param failure_notifications [Array] set of datadog notification handles
  # @return [DatadogChefTags] instance reference to self enabling method chaining
  def with_failure_notifications(failure_notifications)
    @failure_notifications = failure_notifications
    self
  end

  # set the datadog host tags associated with the event
  #
  # @param [Array] the set of host tags
  # @return [DatadogChefTags] instance reference to self enabling method chaining
  def with_tags(tags)
    @tags = tags
    self
  end

  # Emit Chef event to Datadog
  #
  # @param dog [Dogapi::Client] Dogapi Client to be used
  def emit_to_datadog(dog)
    @event_body = ''
    build_event_data

    begin
      evt = dog.emit_event(Dogapi::Event.new(@event_body,
                                             msg_title: @event_title,
                                             event_type: 'config_management.run',
                                             event_object: @hostname,
                                             alert_type: @alert_type,
                                             priority: @event_priority,
                                             source_type_name: 'chef',
                                             tags: @tags
                                            ), host: @hostname)
      # FIXME: nice-to-have: abstract format of return value away a bit
      # in dogapi directly. See https://github.com/DataDog/dogapi-rb/issues/18
      if evt.length < 2
        Chef::Log.warn("Unexpected response from Datadog Event API: #{evt}")
      else
        # [http_response_code, {"event" => {"url" => "...", ...}}]
        # 2xx means ok
        if evt[0].to_i / 100 != 2
          Chef::Log.warn("Could not submit event to Datadog (HTTP call failed): #{evt[0]}")
        else
          Chef::Log.debug("Successfully submitted Chef event to Datadog for #{@hostname} at #{evt[1]['event']['url']}")
        end
      end
    rescue StandardError => e
      Chef::Log.warn("Could not determine whether Chef event was successfully submitted to Datadog: #{evt}. Error:\n#{e}")
    end
  end

  private

  def pluralize(number, noun)
    case number
    when 0..1
      "less than 1 #{noun}"
    else
      "#{number.round} #{noun}s"
    end
  rescue
    Chef::Log.warn("Cannot make #{number} more legible")
    "#{number} #{noun}s"
  end

  # Compose a list of resources updated during a run.
  def update_resource_list
    # No resources updated?
    return unless @run_status.updated_resources.length.to_i > 0

    if @run_status.failed?
      # Shorten the list when there is a failure for stacktrace debugging
      report_resources = @run_status.updated_resources.last(5)
    else
      report_resources = @run_status.updated_resources
    end

    @event_body = "\n$$$\n"
    report_resources.each do |r|
      @event_body << "- #{r} (#{r.defined_at})\n"
    end
    @event_body << "\n$$$\n"
  end

  # Marshal the Event data for submission
  def build_event_data
    # bail early in case of a compiletime failure
    # OPTIMIZE: Use better inspectors to handle failure scenarios, refactor needed.
    if compile_error?
      @alert_type = 'error'
      @event_title = "Chef failed during compile phase on #{@hostname} "
      @event_priority = 'normal'
      @event_body = 'Chef was unable to complete a run, an error during compilation may have occurred.'
    else
      run_time = pluralize(@run_status.elapsed_time, 'second')

      # This is the first line of the Event body, the rest is appended here.
      @event_body = "Chef updated #{@run_status.updated_resources.length} resources out of #{@run_status.all_resources.length} resources total."

      # Update resource list, truncated when failed to 5
      # update will add to the event_body
      update_resource_list

      if @run_status.success?
        @alert_type = 'success'
        @event_priority = 'low'
        @event_title = "Chef completed in #{run_time} on #{@hostname} "
      else
        @alert_type = 'error'
        @event_priority = 'normal'
        @event_title = "Chef failed in #{run_time} on #{@hostname} "

        if @failure_notifications
          handles = @failure_notifications
          # convert the notification handle array to a string
          @event_body << "\nAlerting: #{handles.join(' ')}\n"
        end

        @event_body << "\n$$$\n#{@run_status.formatted_exception}\n$$$\n"
        @event_body << "\n$$$\n#{@run_status.backtrace.join("\n")}\n$$$\n"
      end
    end
  end
end # end module DatadogChefEvent