Skip to content

Commit

Permalink
Multiple endpoints with multiple Dogapi::Client
Browse files Browse the repository at this point in the history
The handler handles the multiple endpoints issues, since it has all the
information.

Require dogapi >= 1.23.0 for the endpoint configuration.
  • Loading branch information
degemer committed Aug 24, 2016
1 parent 7fdd782 commit b2922d9
Show file tree
Hide file tree
Showing 9 changed files with 1,728 additions and 155 deletions.
2 changes: 1 addition & 1 deletion chef-handler-datadog.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Gem::Specification.new do |gem|
gem.require_paths = ['lib']
gem.extra_rdoc_files = ['README.md', 'LICENSE.txt']

gem.add_dependency 'dogapi', '>= 1.6'
gem.add_dependency 'dogapi', '>= 1.23.0'

gem.add_development_dependency 'appraisal', '~> 2.0.1'
gem.add_development_dependency 'bundler'
Expand Down
120 changes: 74 additions & 46 deletions lib/chef/handler/datadog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,7 @@ class Datadog < Chef::Handler
def initialize(config = {})
@config = Mash.new(config)

# If *any* api_key is not provided, this will fail immediately.
@dog = Dogapi::Client.new(
@config[:api_key],
@config[:application_key],
nil, # host
nil, # device
true, # silent
nil, # timeout
extra_endpoints
)
@dogs = prepare_the_pack
end

def report
Expand All @@ -37,8 +28,11 @@ def report

# prepare the metrics, event, and tags information to be reported
prepare_report_for_datadog
# post the report information to the datadog service
send_report_to_datadog

@dogs.each do |dog|
# post the report information to the datadog service
send_report_to_datadog dog
end
ensure
# restore the env proxy settings before leaving to avoid downstream side-effects
restore_env_proxies unless ENV['DATADOG_PROXY'].nil?
Expand All @@ -53,35 +47,33 @@ def prepare_report_for_datadog
# prepare chef run metrics
@metrics =
DatadogChefMetrics.new
.with_dogapi_client(@dog)
.with_hostname(hostname)
.with_run_status(run_status)

# Collect and prepare tags
@tags =
DatadogChefTags.new
.with_dogapi_client(@dog)
.with_hostname(hostname)
.with_run_status(run_status)
.with_application_key(config[:application_key])
.with_tag_prefix(config[:tag_prefix])
.with_retries(config[:tags_submission_retries])

# Build the chef event information
@event =
DatadogChefEvents.new
.with_dogapi_client(@dog)
.with_hostname(hostname)
.with_run_status(run_status)
.with_failure_notifications(@config['notify_on_failure'])
.with_tags(@tags.combined_host_tags)
end

# Submit metrics, event, and tags information to datadog
def send_report_to_datadog
@metrics.emit_to_datadog
@event.emit_to_datadog
@tags.send_update_to_datadog
#
# @param dog [Dogapi::Client] Dogapi Client to be used
def send_report_to_datadog(dog)
@metrics.emit_to_datadog dog
@event.emit_to_datadog dog
@tags.send_update_to_datadog dog
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
Chef::Log.error("Could not connect to Datadog. Connection error:\n" + e)
Chef::Log.error('Data to be submitted was:')
Expand Down Expand Up @@ -128,45 +120,81 @@ def restore_env_proxies
ENV['https_proxy'] = @env_https_proxy
end

def extra_endpoints
urls = @config[:other_dd_urls]
api_keys = @config[:other_api_keys]
app_keys = @config[:other_application_keys]
# create and configure all the Dogapi Clients to be used
#
# @return [Array] all Dogapi::Client to be used
def prepare_the_pack
dogs = []
endpoints.each do |url, api_key, app_key|
dogs.push(Dogapi::Client.new(
api_key,
app_key,
nil, # host
nil, # device
true, # silent
nil, # timeout
url
))
end
dogs
end

def convert_to_array(value_or_array)
return value_or_array if value_or_array.nil?
value_or_array.is_a?(Array) ? value_or_array : [value_or_array]
end

# return all endpoints as a list of triplets [url, api_key, application_key]
def endpoints
urls = convert_to_array @config[:url]
api_keys = convert_to_array @config[:api_key]
app_keys = convert_to_array @config[:application_key]

return nil unless validate_extra_endpoints(urls, api_keys, app_keys)
validate_extra_endpoints(urls, api_keys, app_keys)

if urls.nil?
keys = []
api_keys.each_with_index do |api_key, index|
keys.push([api_key, app_keys[index]])
endpoints = []
if urls.nil? || urls.length == 1
url = urls.nil? ? nil : urls[0]
[api_keys.length, app_keys.length].min.times do |index|
endpoints.push [url, api_keys[index], app_keys[index]]
end
return keys
else
endpoints = Hash.new []
urls.each_with_index do |url, index|
endpoints[url] = endpoints[url] + [[api_keys[index], app_keys[index]]]
[urls.length, api_keys.length, app_keys.length].min.times do |index|
endpoints.push [urls[index], api_keys[index], app_keys[index]]
end
return endpoints
end
endpoints
end

# Validate endpoints config (urls, api_keys and application keys)
# fails if incorrect and nothing can be done
# Doesn't fail if at least one endpoint can be used
def validate_extra_endpoints(urls, api_keys, app_keys)
return false if api_keys.nil?
if api_keys.nil?
Chef::Log.warn('You need an API key to communicate with Datadog')
fail ArgumentError, 'Missing Datadog Api Key'
end
if app_keys.nil?
Chef::Log.warn('You need an application key to let Chef tag your nodes ' \
'in Datadog. Visit https://app.datadoghq.com/account/settings#api to ' \
'create one and update your datadog attributes in the datadog cookbook.')
fail ArgumentError, 'Missing Datadog Application Key'
end

# If not enough app_keys compared to api_keys
if app_keys.nil? || app_keys.length != api_keys.length
Chef::Log.error('Bad number of other_application_keys given:')
Chef::Log.error("#{api_keys.length} other_api_keys, " \
"#{app_keys.nil? ? 0 : app_keys.length} other_application_keys")
return false
Chef::Log.warn('Bad number of application_key given,')
Chef::Log.warn('You have to provide one application_key per api_key:')
Chef::Log.warn("#{api_keys.length} api_key for " \
"#{app_keys.nil? ? 0 : app_keys.length} application_key")
end
# If not enough api_keys compared to dd_urls
if !urls.nil? && urls.length != api_keys.length
Chef::Log.error('Bad number of other_api_keys given:')
Chef::Log.error("#{urls.length} other_dd_urls, " \
"#{api_keys.length} other_api_keys")
return false
# If not enough api_keys compared to urls
if !urls.nil? && urls.length > 1 && urls.length != api_keys.length
Chef::Log.warn('Bad number of api_key given:')
Chef::Log.warn('You have to provide one api_key per dd_url:')
Chef::Log.warn("#{urls.length} url for " \
"#{api_keys.length} api_key")
end
true
end
end # end class Datadog
end # end class Handler
Expand Down
32 changes: 12 additions & 20 deletions lib/chef/handler/datadog_chef_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
# helper class for sending events about chef runs
class DatadogChefEvents
def initialize
@dog = nil
@hostname = nil
@run_status = nil
@failure_notfications = nil
Expand All @@ -21,15 +20,6 @@ def initialize
@event_body = ''
end

# set the dogapi client handle
#
# @param dogapi_client [Dogapi::Client] datadog api client handle
# @return [DatadogChefTags] instance reference to self enabling method chaining
def with_dogapi_client(dogapi_client)
@dog = dogapi_client
self
end

# set the target hostname (chef node name)
#
# @param hostname [String] hostname to use for the handler report
Expand Down Expand Up @@ -67,18 +57,20 @@ def with_tags(tags)
end

# Emit Chef event to Datadog
def emit_to_datadog
#
# @param dog [Dogapi::Client] Dogapi Client to be used
def emit_to_datadog(dog)
@event_body = ''
build_event_data
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)
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)

begin
# FIXME: nice-to-have: abstract format of return value away a bit
Expand Down
22 changes: 7 additions & 15 deletions lib/chef/handler/datadog_chef_metrics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,10 @@
# helper class for sending datadog metrics from a chef run
class DatadogChefMetrics
def initialize
@dog = nil
@hostname = ''
@run_status = nil
end

# set the dogapi client handle
#
# @param dogapi_client [Dogapi::Client] datadog api client
# @return [DatadogChefMetrics] instance reference to self enabling method chaining
def with_dogapi_client(dogapi_client)
@dog = dogapi_client
self
end

# set the target hostname (chef node name)
#
# @param hostname [String] hostname used for reporting metrics
Expand All @@ -37,18 +27,20 @@ def with_run_status(run_status)
end

# Emit Chef metrics to Datadog
def emit_to_datadog
#
# @param dog [Dogapi::Client] Dogapi Client to be used
def emit_to_datadog(dog)
# Send base success/failure metric
@dog.emit_point(@run_status.success? ? 'chef.run.success' : 'chef.run.failure', 1, host: @hostname, type: 'counter')
dog.emit_point(@run_status.success? ? 'chef.run.success' : 'chef.run.failure', 1, host: @hostname, type: 'counter')

# If there is a failure during compile phase, a large portion of
# run_status may be unavailable. Bail out here
warn_msg = 'Error during compile phase, no Datadog metrics available.'
return Chef::Log.warn(warn_msg) if compile_error?

@dog.emit_point('chef.resources.total', @run_status.all_resources.length, host: @hostname)
@dog.emit_point('chef.resources.updated', @run_status.updated_resources.length, host: @hostname)
@dog.emit_point('chef.resources.elapsed_time', @run_status.elapsed_time, host: @hostname)
dog.emit_point('chef.resources.total', @run_status.all_resources.length, host: @hostname)
dog.emit_point('chef.resources.updated', @run_status.updated_resources.length, host: @hostname)
dog.emit_point('chef.resources.elapsed_time', @run_status.elapsed_time, host: @hostname)
Chef::Log.debug('Submitted Chef metrics back to Datadog')
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
Chef::Log.error("Could not send metrics to Datadog. Connection error:\n" + e)
Expand Down
35 changes: 4 additions & 31 deletions lib/chef/handler/datadog_chef_tags.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,6 @@ def initialize
@combined_host_tags = nil
end

# set the dogapi client handle
#
# @param dogapi_client [Dogapi::Client] datadog api client handle
# @return [DatadogChefTags] instance reference to self enabling method chaining
def with_dogapi_client(dogapi_client)
@dog = dogapi_client
self
end

# set the chef run status used for the report
#
# @param run_status [Chef::RunStatus] current chef run status
Expand All @@ -46,26 +37,6 @@ def with_hostname(hostname)
self
end

# set the datadog application key
#
# TODO: the application key is only needed for error checking, e.g. an app key exists
# would be cleaner to push this check up to the data prep method in the
# calling handler class
#
# @param application_key [String] datadog application key used for chef reports
# @return [DatadogChefTags] instance reference to self enabling method chaining
def with_application_key(application_key)
@application_key = application_key
if @application_key.nil?
Chef::Log.warn('You need an application key to let Chef tag your nodes ' \
'in Datadog. Visit https://app.datadoghq.com/account/settings#api to ' \
'create one and update your datadog attributes in the datadog cookbook.'
)
fail ArgumentError, 'Missing Datadog Application Key'
end
self
end

# set the prefix to be added to all Chef tags
#
# @param tag_prefix [String] prefix to be added to all Chef tags
Expand All @@ -86,13 +57,15 @@ def with_retries(retries)
end

# send updated chef run generated tags to Datadog
def send_update_to_datadog
#
# @param dog [Dogapi::Client] Dogapi Client to be used
def send_update_to_datadog(dog)
tags = combined_host_tags
retries = @retries
begin
loop do
should_retry = false
rc = @dog.update_tags(@hostname, tags, 'chef')
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}")
Expand Down
Loading

0 comments on commit b2922d9

Please sign in to comment.