DataDog/chef-handler-datadog

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

Summary

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

# helper class for sending datadog tags from chef runs
class DatadogChefTags
  def initialize
    @node = nil
    @run_status = nil
    @application_key = nil
    @tag_prefix = 'tag:'
    @scope_prefix = nil
    @retries = 0
    @combined_host_tags = nil
    @regex_black_list = nil
    @policy_tags_enabled = false
  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
    # Build up an array of Chef tags that will be sent back
    # Selects all [env, roles, tags] from the Node's object and reformats
    # them to `key:value` e.g. `role:database-master`.
    @node = run_status.node
    self
  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 prefix to be added to all Chef tags
  #
  # @param tag_prefix [String] prefix to be added to all Chef tags
  # @return [DatadogChefTags] instance reference to self enabling method chaining
  def with_tag_prefix(tag_prefix)
    @tag_prefix = tag_prefix unless tag_prefix.nil?
    self
  end

  # set the number of retries when sending tags, when the host is not yet present
  # on Datadog
  #
  # @param retries [Integer] number of retries
  # @return [DatadogChefTags] instance reference to self enabling method chaining
  def with_retries(retries)
    @retries = retries unless retries.nil?
    self
  end

  # set the blacklist regexp, node tags matching this regex won't be sent
  #
  # @param tags_blacklist_regex [String] regexp-formatted string
  # @return [DatadogChefTags] instance reference to self enabling method chaining
  def with_tag_blacklist(tags_blacklist_regex)
    @regex_black_list = Regexp.new(tags_blacklist_regex, Regexp::IGNORECASE) unless tags_blacklist_regex.nil? || tags_blacklist_regex.empty?
    self
  end

  # set the prefix to be added to Datadog tags (Role, Env)
  #
  # @param scope_prefix [String] prefix to be added to Datadog tags
  # @return [DatadogChefTags] instance reference to self enabling method chaining
  def with_scope_prefix(scope_prefix)
    @scope_prefix = scope_prefix unless scope_prefix.nil?
    self
  end

  # enable policy tags
  #
  # @param enabled [TrueClass,FalseClass] enable or disable policy tags
  # @return [DatadogChefTags] instance reference to self enabling method chaining
  def with_policy_tags_enabled(enabled)
    @policy_tags_enabled = enabled unless enabled.nil?
    self
  end

  # send updated chef run generated tags to Datadog
  #
  # @param dog [Dogapi::Client] Dogapi Client to be used
  def send_update_to_datadog(dog)
    tags = combined_host_tags
    retries = @retries
    rc = []
    begin
      loop do
        should_retry = false
        rc = dog.update_tags(@hostname, tags, 'chef')
        # See FIXME in DatadogChefEvents::emit_to_datadog about why I feel dirty repeating this code here
        if rc.length < 2
          Chef::Log.warn("Unexpected response from Datadog Tags API: #{rc}")
        else
          if retries > 0 && rc[0].to_i == 404
            Chef::Log.debug("Host #{@hostname} not yet present on Datadog, re-submitting tags in 2 seconds")
            sleep 2
            retries -= 1
            should_retry = true
          elsif rc[0].to_i / 100 != 2
            Chef::Log.warn("Could not submit #{tags} tags for #{@hostname} to Datadog: #{rc}")
          else
            Chef::Log.debug("Successfully updated #{@hostname}'s tags to #{tags.join(', ')}")
          end
        end
        break unless should_retry
      end
    rescue StandardError => e
      Chef::Log.warn("Could not determine whether #{@hostname}'s tags were successfully submitted to Datadog: #{rc.inspect}. Error:\n#{e}")
    end
  end

  # return a combined array of tags that should be sent to Datadog
  #
  # @return [Array] the set of host tags based off the chef run
  def combined_host_tags
    # Combine (union) all arrays. Removes duplicates if found.
    node_env.split | node_roles | node_policy_tags | node_tags
  end

  private

  def node_roles
    @node.run_list.roles.map! { |role| "#{@scope_prefix}role:#{role}" }
  end

  def node_env
    "#{@scope_prefix}env:#{@node.chef_environment}" if @node.respond_to?('chef_environment')
  end

  # Send the policy name and policy group as chef tags when using chef policyfiles feature
  # The policy_group and policy_name attributes exist only for chef >= 12.5.1
  def node_policy_tags
    policy_tags = []
    if @policy_tags_enabled
      if @node.respond_to?('policy_group') && !@node.policy_group.nil?
        policy_tags << "#{@scope_prefix}policy_group:#{@node.policy_group}"
      end
      if @node.respond_to?('policy_name') && !@node.policy_name.nil?
        policy_tags << "#{@scope_prefix}policy_name:#{@node.policy_name}"
      end
    end
    policy_tags
  end

  def node_tags
    return [] unless @node.tags
    output = @node.tags.map { |tag| "#{@tag_prefix}#{tag}" }

    # No blacklist, return all results
    return output if @regex_black_list.nil?

    # The blacklist is set, so return the items which are not filtered by it.
    output.select { |t| !@regex_black_list.match(t) }
  end
end # end class DatadogChefTags