Skip to content

Commit 323d246

Browse files
zbintliffmajormoses
authored andcommitted
Feature/allow zero numerator (#266)
* Add ability for numerator to be empty CloudWatch returns empty data if there is no metric for it. This allows us to set a default (0) to replace for the numeratort. Remove the --no-data-ok flag because it doesn't make sense with the above option. Instead we chang eit to --no-denominator-data-ok. If there is no denominator data we will either `unknown` or `ok`. Also refactored some to reuse more code. May refactor more. * Allow backwards compatibility and add some tests * Move #composite_check out of library, and update tests * Split out some logic for easier legibility * Fix rubo by disabling and adding changelog
1 parent 2f86c56 commit 323d246

8 files changed

Lines changed: 569 additions & 48 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44
This CHANGELOG follows the format listed [here](https://github.com/sensu-plugins/community/blob/master/HOW_WE_CHANGELOG.md)
55

66
## [Unreleased]
7+
- check-cloudwatch-composite-metric.rb: add flags `zero_denominator_data_ok`, `no_denominator_data_ok`, and `numerator_default` to add ability to allow numerator in composite to be 0. While leaving the functionality of `no_data_ok` the same, this change allows us to check to alert if the numerator has no data since 0/X is a valid alert case.
8+
- lib/cloudwatch-common.rb: added tests for majority of functions
79

810
## [10.0.3] - 2017-12-03
911
### Fixed

bin/check-cloudwatch-composite-metric.rb

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,14 @@ class CloudWatchCompositeMetricCheck < Sensu::Plugin::Check::CLI
8585
description: 'CloudWatch metric unit'
8686

8787
option :critical,
88-
description: 'Trigger a critical when value is over VALUE',
88+
description: 'Trigger a critical when value is over VALUE as a Percent',
8989
short: '-c VALUE',
9090
long: '--critical VALUE',
9191
proc: proc(&:to_f),
9292
required: true
9393

9494
option :warning,
95-
description: 'Trigger a warning when value is over VALUE',
95+
description: 'Trigger a warning when value is over VALUE as a Percent',
9696
short: '-w VALUE',
9797
long: '--warning VALUE',
9898
proc: proc(&:to_f)
@@ -103,20 +103,97 @@ class CloudWatchCompositeMetricCheck < Sensu::Plugin::Check::CLI
103103
long: '--operator OPERATION',
104104
default: 'greater'
105105

106+
option :numerator_default,
107+
long: '--numerator-default DEFAULT',
108+
description: 'Default for numerator if no data is returned for metric',
109+
proc: proc(&:to_f)
110+
111+
option :no_denominator_data_ok,
112+
long: '--allow-no-denominator-data',
113+
description: 'Returns ok if no data is returned from denominator metric',
114+
boolean: true,
115+
default: false
116+
117+
option :zero_denominator_data_ok,
118+
long: '--allow-zero-denominator-data',
119+
description: 'Returns ok if denominator metric is zero',
120+
boolean: true,
121+
default: false
122+
106123
option :no_data_ok,
107124
short: '-O',
108125
long: '--allow-no-data',
109-
description: 'Returns ok if no data is returned from the metric',
126+
description: 'Returns ok if no data is returned from either metric',
110127
boolean: true,
111128
default: false
112-
113129
include CloudwatchCommon
114130

115131
def metric_desc
116132
"#{config[:namespace]}-#{config[:numerator_metric_name]}/#{config[:denominator_metric_name]}(#{dimension_string})"
117133
end
118134

135+
def numerator_data(metric_payload)
136+
if resp_has_no_data(metric_payload, config[:statistics])
137+
# If the numerator response has no data in it, see if there was a predefined default.
138+
# If there is no predefined default it will return nil
139+
config[:numerator_default]
140+
else
141+
read_value(metric_payload, config[:statistics]).to_f
142+
end
143+
end
144+
145+
# rubocop:disable Style/GuardClause
146+
def composite_check
147+
numerator_metric_resp = get_metric(config[:numerator_metric_name])
148+
denominator_metric_resp = get_metric(config[:denominator_metric_name])
149+
150+
## If the numerator is empty, then we see if there is a default. If there is a default
151+
## then we will pretend the numerator _isnt_ empty. That is
152+
## if empty but there is no default this will be true. If it is empty and there is a default
153+
## this will be false (i.e. there is data, following standard of dealing in the negative here)
154+
no_num_data = numerator_data(numerator_metric_resp).nil?
155+
no_den_data = resp_has_no_data(denominator_metric_resp, config[:statistics])
156+
no_data = no_num_data || no_den_data
157+
158+
# no data in numerator or denominator this is to keep backwards compatibility
159+
if no_data && config[:no_data_ok]
160+
return :ok, "#{metric_desc} returned no data but that's ok"
161+
elsif no_den_data && config[:no_denominator_data_ok]
162+
return :ok, "#{config[:denominator_metric_name]} returned no data but that's ok"
163+
elsif no_data ## This is legacy case
164+
return :unknown, "#{metric_desc} could not be retrieved"
165+
end
166+
167+
## Now both the denominator and numerator have data (or a valid default)
168+
denominator_value = read_value(denominator_metric_resp, config[:statistics]).to_f
169+
if denominator_value.zero? && config[:zero_denominator_data_ok]
170+
return :ok, "#{metric_desc}: denominator value is zero but that's ok"
171+
elsif denominator_value.zero?
172+
return :unknown, "#{metric_desc}: denominator value is zero"
173+
end
174+
175+
## We already checked if this value is nil so we know its not
176+
numerator_value = numerator_data(numerator_metric_resp)
177+
value = (numerator_value / denominator_value * 100).to_i
178+
base_msg = "#{metric_desc} is #{value}: comparison=#{config[:compare]}"
179+
180+
if compare(value, config[:critical], config[:compare])
181+
return :critical, "#{base_msg} threshold=#{config[:critical]}"
182+
elsif config[:warning] && compare(value, config[:warning], config[:compare])
183+
return :warning, "#{base_msg} threshold=#{config[:warning]}"
184+
else
185+
threshold = config[:warning] || config[:critical]
186+
return :ok, "#{base_msg}, will alarm at #{threshold}"
187+
end
188+
end
189+
# rubocop:enable Style/GuardClause
190+
119191
def run
120-
composite_check config
192+
status, msg = composite_check
193+
if respond_to?(status)
194+
send(status, msg)
195+
else
196+
unknown 'unknown exit status called'
197+
end
121198
end
122199
end

lib/sensu-plugins-aws/cloudwatch-common.rb

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module CloudwatchCommon
33
include Common
44

55
def client
6-
Aws::CloudWatch::Client.new
6+
@client ||= Aws::CloudWatch::Client.new
77
end
88

99
def read_value(resp, stats)
@@ -40,47 +40,13 @@ def metrics_request(config)
4040
}
4141
end
4242

43-
def composite_metrics_request(config, metric, fixed_time_now = Time.now)
44-
{
45-
namespace: config[:namespace],
46-
metric_name: config[metric],
47-
dimensions: config[:dimensions],
48-
start_time: fixed_time_now - config[:period] * 10,
49-
end_time: fixed_time_now,
50-
period: config[:period],
51-
statistics: [config[:statistics]],
52-
unit: config[:unit]
53-
}
43+
def get_metric(metric)
44+
client.get_metric_statistics(composite_metrics_request(metric))
5445
end
5546

56-
def composite_check(config)
57-
fixed_time_now = Time.now
58-
numerator_metric_resp = client.get_metric_statistics(composite_metrics_request(config, :numerator_metric_name, fixed_time_now))
59-
denominator_metric_resp = client.get_metric_statistics(composite_metrics_request(config, :denominator_metric_name, fixed_time_now))
60-
61-
no_data = resp_has_no_data(numerator_metric_resp, config[:statistics]) || \
62-
resp_has_no_data(denominator_metric_resp, config[:statistics])
63-
if no_data && config[:no_data_ok]
64-
ok "#{metric_desc} returned no data but that's ok"
65-
elsif no_data && !config[:no_data_ok]
66-
unknown "#{metric_desc} could not be retrieved"
67-
end
68-
69-
denominator_value = read_value(denominator_metric_resp, config[:statistics]).to_f
70-
if denominator_value.zero?
71-
ok "#{metric_desc} denominator value is zero but that's ok"
72-
end
73-
numerator_value = read_value(numerator_metric_resp, config[:statistics]).to_f
74-
value = (numerator_value / denominator_value * 100).to_i
75-
base_msg = "#{metric_desc} is #{value}: comparison=#{config[:compare]}"
76-
77-
if compare(value, config[:critical], config[:compare])
78-
critical "#{base_msg} threshold=#{config[:critical]}"
79-
elsif config[:warning] && compare(value, config[:warning], config[:compare])
80-
warning "#{base_msg} threshold=#{config[:warning]}"
81-
else
82-
ok "#{base_msg}, will alarm at #{!config[:warning].nil? ? config[:warning] : config[:critical]}"
83-
end
47+
def composite_metrics_request(metric)
48+
## config is a class variable but don't want to change signature
49+
metrics_request(config).merge(metric_name: metric)
8450
end
8551

8652
def self.parse_dimensions(dimension_string)

lib/sensu-plugins-aws/common.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
#
1818

1919
module Common
20-
def initialize
21-
super()
20+
def initialize(argv = ARGV)
21+
super(argv)
2222
aws_config
2323
end
2424

0 commit comments

Comments
 (0)