banshee is under active development: collectors will keep changing, and a version you publish will likely need adjustments. For stable use, wait for version 1.0.0.

Cloud

banshee/cloud-collection/cloud/aws/https/

cloudwatch-alarms

The CloudWatch alarms of an AWS region in the state ALARM, how many and their names, by the CloudWatch API.

  • cloud
  • aws
  • amazon
  • cloudwatch
  • alarms
  • https
  • api

by banshee0 installations1 version

v1Latest

Published on October 27, 2026. First version.

Keen 1.0.0 · 99 lines
// The CloudWatch alarms of an AWS region that are in the state ALARM, by the CloudWatch API with an access key:
// how many, and their names. The alarms are those the account defines in CloudWatch; this collector reads their
// state, it does not judge the metrics again.
// Reads GET https://monitoring.<region>.amazonaws.com/?Action=DescribeAlarms&StateValue=ALARM, page after page,
// signed by Signature Version 4.
need('protocol:http');
need('protocol:aws');
$region = variable('aws:region', 'text', array(
    'label' => 'Region',
    'help'  => 'The AWS region of the alarms, as AWS writes it in an API: eu-west-3, us-east-1.',
));
$prefix = variable('aws:alarm-prefix', 'text', array(
    'label'    => 'Alarm name prefix',
    'help'     => 'Only the alarms whose name starts with this text, for instance prod-; empty, every alarm of the region.',
    'required' => false,
));

output('alarms', 'number', array('label' => 'Alarms in ALARM'));
output('alarm-names', 'text', array('label' => 'Names of the alarms in ALARM'));

watch.above('alarm', 'CloudWatch alarm raised', 'alarms', array('critical' => 0));

// One signed GET on an AWS API of the Query protocol (EC2, RDS, CloudWatch): the parameters in the address, the
// answer in XML. aws.sign gives the headers that carry the signature: the keys never leave. A refusal is told
// by the error code AWS writes in its answer; a throttled request or a fault of AWS is not one.
function aws_get($service, $region, $query, $what) {
    $url = 'https://' . $service . '.' . $region . '.amazonaws.com/?' . url.query($query);
    $answer = http.get($url, headers=aws.sign('GET', $url, array(), '', $region, $service));
    if ($answer['status'] != 200) {
        $code = '';
        $found = text.match($answer['body'], '<Code>([^<]+)</Code>');
        if (list.count($found) > 0) {
            $code = $found[1];
        }
        if ($code == 'Throttling' || $code == 'RequestLimitExceeded' || $answer['status'] >= 500) {
            fail('unreachable', 'AWS answers the reading of ' . $what . ' with code ' . $answer['status'] . ' ' . $code);
        }
        if ($answer['status'] == 400 || $answer['status'] == 401 || $answer['status'] == 403) {
            fail('misconfigured', 'AWS refuses to read ' . $what . ', code ' . $answer['status'] . ' ' . $code,
                'Check the access key, the permissions of its user or role, and the region.');
        }
        fail('unreachable', 'AWS answers the reading of ' . $what . ' with code ' . $answer['status']);
    }
    return xml.read($answer['body']);
}

// The first child of that name, array() when it is absent or empty. An empty element, such as a list without
// members written <name/>, reads as the text '' and not as an array.
function element($parent, $name) {
    $children = list.get($parent, $name, array());
    if (list.count($children) == 0 || !list.is($children[0])) {
        return array();
    }
    return $children[0];
}

// The text of the first child of that name, '' when it is absent.
function child_text($parent, $name) {
    $children = list.get($parent, $name, array());
    if (list.count($children) == 0) {
        return '';
    }
    return $children[0];
}

// The region goes into the name of the host: it must be a name.
if (list.count(text.match($region, '^[a-z0-9-]+$')) == 0) {
    fail('misconfigured', 'The region holds other characters than lower-case letters, digits and hyphens');
}

// <DescribeAlarmsResponse><DescribeAlarmsResult><MetricAlarms><member><AlarmName>cpu-high</AlarmName>
// <StateValue>ALARM</StateValue>…</member></MetricAlarms><CompositeAlarms/><LogAlarms/><NextToken>…</NextToken>
// </DescribeAlarmsResult><ResponseMetadata>…</ResponseMetadata></DescribeAlarmsResponse>. Without AlarmTypes, AWS
// lists the metric alarms only: the three kinds are asked for.
$names = array();
$token = '';
while (true) {
    $query = array('Action' => 'DescribeAlarms', 'Version' => '2010-08-01', 'StateValue' => 'ALARM', 'MaxRecords' => 100,
        'AlarmTypes.member.1' => 'MetricAlarm', 'AlarmTypes.member.2' => 'CompositeAlarm', 'AlarmTypes.member.3' => 'LogAlarm');
    if ($prefix != '') {
        $query['AlarmNamePrefix'] = $prefix;
    }
    if ($token != '') {
        $query['NextToken'] = $token;
    }
    $result = element(element(aws_get('monitoring', $region, $query, 'the CloudWatch alarms'), 'DescribeAlarmsResponse'),
        'DescribeAlarmsResult');
    foreach (array('MetricAlarms', 'CompositeAlarms', 'LogAlarms') as $kind) {
        foreach (list.get(element($result, $kind), 'member', array()) as $alarm) {
            $names = list.add($names, child_text($alarm, 'AlarmName'));
        }
    }
    $token = child_text($result, 'NextToken');
    if ($token == '') {
        break;
    }
}
value('alarms', list.count($names));
value('alarm-names', text.join($names, ' | '));