sensu-plugins/sensu-plugins-aws

View on GitHub
bin/check-cloudwatch-composite-metric.rb

Summary

Maintainability
B
4 hrs
Test Coverage
#! /usr/bin/env ruby
#
# check-cloudwatch-composite-metric
#
# DESCRIPTION:
#   This plugin retrieves a couple of values of two cloudwatch metrics,
#   computes a percentage value based on the numerator metric and the denomicator metric
#   and triggers alarms based on the thresholds specified.
#   This plugin is an extension to the Andrew Matheny's check-cloudwatch-metric plugin
#   and uses the CloudwatchCommon lib, extended as well.
#
# OUTPUT:
#   plain-text
#
# PLATFORMS:
#   Linux
#
# DEPENDENCIES:
#   gem: aws-sdk
#   gem: sensu-plugin
#
# USAGE:
#   ./check-cloudwatch-composite-metric.rb --namespace AWS/ELB -N HTTPCode_Backend_4XX -D RequestCount --dimensions LoadBalancerName=test-elb --period 60 --statistics Maximum --operator equal --critical 0
#
# NOTES:
#
# LICENSE:
#   Cornel Foltea
#   Released under the same terms as Sensu (the MIT license); see LICENSE
#   for details.
#

require 'sensu-plugins-aws'
require 'sensu-plugin/check/cli'
require 'aws-sdk'

class CloudWatchCompositeMetricCheck < Sensu::Plugin::Check::CLI
  option :aws_region,
         short: '-r AWS_REGION',
         long: '--aws-region REGION',
         description: 'AWS Region (defaults to us-east-1).',
         default: 'us-east-1'

  option :namespace,
         description: 'CloudWatch namespace for metric',
         short: '-n NAME',
         long: '--namespace NAME',
         default: 'AWS/EC2'

  option :numerator_metric_name,
         description: 'Numerator metric name',
         short: '-N NAME',
         long: '--numerator-metric NAME',
         required: true

  option :denominator_metric_name,
         description: 'Denominator metric name',
         short: '-D NAME',
         long: '--denominator-metric NAME',
         required: true

  option :dimensions,
         description: 'Comma delimited list of DimName=Value',
         short: '-d DIMENSIONS',
         long: '--dimensions DIMENSIONS',
         proc: proc { |d| CloudwatchCommon.parse_dimensions d },
         default: []

  option :period,
         description: 'CloudWatch metric statistics period. Must be a multiple of 60',
         short: '-p N',
         long: '--period SECONDS',
         default: 60,
         proc: proc(&:to_i)

  option :statistics,
         short: '-s N',
         long: '--statistics NAME',
         default: 'Average',
         description: 'CloudWatch statistics method'

  option :unit,
         short: '-u UNIT',
         long: '--unit UNIT',
         description: 'CloudWatch metric unit'

  option :critical,
         description: 'Trigger a critical when value is over VALUE as a Percent',
         short: '-c VALUE',
         long: '--critical VALUE',
         proc: proc(&:to_f),
         required: true

  option :warning,
         description: 'Trigger a warning when value is over VALUE as a Percent',
         short: '-w VALUE',
         long: '--warning VALUE',
         proc: proc(&:to_f)

  option :compare,
         description: 'Comparision operator for threshold: equal, not, greater, less',
         short: '-o OPERATION',
         long: '--operator OPERATION',
         default: 'greater'

  option :numerator_default,
         long: '--numerator-default DEFAULT',
         description: 'Default for numerator if no data is returned for metric',
         proc: proc(&:to_f)

  option :no_denominator_data_ok,
         long: '--allow-no-denominator-data',
         description: 'Returns ok if no data is returned from denominator metric',
         boolean: true,
         default: false

  option :zero_denominator_data_ok,
         long: '--allow-zero-denominator-data',
         description: 'Returns ok if denominator metric is zero',
         boolean: true,
         default: false

  option :no_data_ok,
         short: '-O',
         long: '--allow-no-data',
         description: 'Returns ok if no data is returned from either metric',
         boolean: true,
         default: false
  include CloudwatchCommon

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

  def numerator_data(metric_payload)
    if resp_has_no_data(metric_payload, config[:statistics])
      # If the numerator response has no data in it, see if there was a predefined default.
      # If there is no predefined default it will return nil
      config[:numerator_default]
    else
      read_value(metric_payload, config[:statistics]).to_f
    end
  end

  # rubocop:disable Style/GuardClause
  def composite_check
    numerator_metric_resp = get_metric(config[:numerator_metric_name])
    denominator_metric_resp = get_metric(config[:denominator_metric_name])

    ## If the numerator is empty, then we see if there is a default. If there is a default
    ## then we will pretend the numerator _isnt_ empty. That is
    ## if empty but there is no default this will be true. If it is empty and there is a default
    ## this will be false (i.e. there is data, following standard of dealing in the negative here)
    no_num_data = numerator_data(numerator_metric_resp).nil?
    no_den_data = resp_has_no_data(denominator_metric_resp, config[:statistics])
    no_data = no_num_data || no_den_data

    # no data in numerator or denominator this is to keep backwards compatibility
    if no_data && config[:no_data_ok]
      return :ok, "#{metric_desc} returned no data but that's ok"
    elsif no_den_data && config[:no_denominator_data_ok]
      return :ok, "#{config[:denominator_metric_name]} returned no data but that's ok"
    elsif no_data ## This is legacy case
      return :unknown, "#{metric_desc} could not be retrieved"
    end

    ## Now both the denominator and numerator have data (or a valid default)
    denominator_value = read_value(denominator_metric_resp, config[:statistics]).to_f
    if denominator_value.zero? && config[:zero_denominator_data_ok]
      return :ok, "#{metric_desc}: denominator value is zero but that's ok"
    elsif denominator_value.zero?
      return :unknown, "#{metric_desc}: denominator value is zero"
    end

    ## We already checked if this value is nil so we know its not
    numerator_value = numerator_data(numerator_metric_resp)
    value = (numerator_value / denominator_value * 100).to_i
    base_msg = "#{metric_desc} is #{value}: comparison=#{config[:compare]}"

    if compare(value, config[:critical], config[:compare])
      return :critical, "#{base_msg} threshold=#{config[:critical]}"
    elsif config[:warning] && compare(value, config[:warning], config[:compare])
      return :warning,  "#{base_msg} threshold=#{config[:warning]}"
    else
      threshold = config[:warning] || config[:critical]
      return :ok, "#{base_msg}, will alarm at #{threshold}"
    end
  end
  # rubocop:enable Style/GuardClause

  def run
    status, msg = composite_check
    if respond_to?(status)
      send(status, msg)
    else
      unknown 'unknown exit status called'
    end
  end
end