Metric data query alarms (#3419)

* Add support for metric data query alarms (Metrics=[..])

* Fix trailing whitespace

* Allow for unordered metrics in Python 2.7

* Add describe_alarm assertions and support DatapointsToAlarm
This commit is contained in:
Eoin Shanaghy 2020-10-31 15:56:24 +00:00 committed by GitHub
parent f8d2ce2e6a
commit a3880c4c35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 216 additions and 9 deletions

View File

@ -31,6 +31,33 @@ class Dimension(object):
return self != item
class Metric(object):
def __init__(self, metric_name, namespace, dimensions):
self.metric_name = metric_name
self.namespace = namespace
self.dimensions = dimensions
class MetricStat(object):
def __init__(self, metric, period, stat, unit):
self.metric = metric
self.period = period
self.stat = stat
self.unit = unit
class MetricDataQuery(object):
def __init__(
self, id, label, period, return_data, expression=None, metric_stat=None
):
self.id = id
self.label = label
self.period = period
self.return_data = return_data
self.expression = expression
self.metric_stat = metric_stat
def daterange(start, stop, step=timedelta(days=1), inclusive=False):
"""
This method will iterate from `start` to `stop` datetimes with a timedelta step of `step`
@ -65,8 +92,10 @@ class FakeAlarm(BaseModel):
name,
namespace,
metric_name,
metric_data_queries,
comparison_operator,
evaluation_periods,
datapoints_to_alarm,
period,
threshold,
statistic,
@ -81,8 +110,10 @@ class FakeAlarm(BaseModel):
self.name = name
self.namespace = namespace
self.metric_name = metric_name
self.metric_data_queries = metric_data_queries
self.comparison_operator = comparison_operator
self.evaluation_periods = evaluation_periods
self.datapoints_to_alarm = datapoints_to_alarm
self.period = period
self.threshold = threshold
self.statistic = statistic
@ -235,8 +266,10 @@ class CloudWatchBackend(BaseBackend):
name,
namespace,
metric_name,
metric_data_queries,
comparison_operator,
evaluation_periods,
datapoints_to_alarm,
period,
threshold,
statistic,
@ -252,8 +285,10 @@ class CloudWatchBackend(BaseBackend):
name,
namespace,
metric_name,
metric_data_queries,
comparison_operator,
evaluation_periods,
datapoints_to_alarm,
period,
threshold,
statistic,

View File

@ -1,7 +1,7 @@
import json
from moto.core.utils import amzn_request_id
from moto.core.responses import BaseResponse
from .models import cloudwatch_backends
from .models import cloudwatch_backends, MetricDataQuery, MetricStat, Metric, Dimension
from dateutil.parser import parse as dtparse
@ -19,8 +19,37 @@ class CloudWatchResponse(BaseResponse):
name = self._get_param("AlarmName")
namespace = self._get_param("Namespace")
metric_name = self._get_param("MetricName")
metrics = self._get_multi_param("Metrics.member")
metric_data_queries = None
if metrics:
metric_data_queries = [
MetricDataQuery(
id=metric.get("Id"),
label=metric.get("Label"),
period=metric.get("Period"),
return_data=metric.get("ReturnData"),
expression=metric.get("Expression"),
metric_stat=MetricStat(
metric=Metric(
metric_name=metric.get("MetricStat.Metric.MetricName"),
namespace=metric.get("MetricStat.Metric.Namespace"),
dimensions=[
Dimension(name=dim["Name"], value=dim["Value"])
for dim in metric["MetricStat.Metric.Dimensions.member"]
],
),
period=metric.get("MetricStat.Period"),
stat=metric.get("MetricStat.Stat"),
unit=metric.get("MetricStat.Unit"),
)
if "MetricStat.Metric.MetricName" in metric
else None,
)
for metric in metrics
]
comparison_operator = self._get_param("ComparisonOperator")
evaluation_periods = self._get_param("EvaluationPeriods")
datapoints_to_alarm = self._get_param("DatapointsToAlarm")
period = self._get_param("Period")
threshold = self._get_param("Threshold")
statistic = self._get_param("Statistic")
@ -37,8 +66,10 @@ class CloudWatchResponse(BaseResponse):
name,
namespace,
metric_name,
metric_data_queries,
comparison_operator,
evaluation_periods,
datapoints_to_alarm,
period,
threshold,
statistic,
@ -261,35 +292,92 @@ DESCRIBE_ALARMS_TEMPLATE = """<DescribeAlarmsResponse xmlns="http://monitoring.a
<AlarmDescription>{{ alarm.description }}</AlarmDescription>
<AlarmName>{{ alarm.name }}</AlarmName>
<ComparisonOperator>{{ alarm.comparison_operator }}</ComparisonOperator>
<Dimensions>
{% for dimension in alarm.dimensions %}
<member>
<Name>{{ dimension.name }}</Name>
<Value>{{ dimension.value }}</Value>
</member>
{% endfor %}
</Dimensions>
{% if alarm.dimensions is not none %}
<Dimensions>
{% for dimension in alarm.dimensions %}
<member>
<Name>{{ dimension.name }}</Name>
<Value>{{ dimension.value }}</Value>
</member>
{% endfor %}
</Dimensions>
{% endif %}
<EvaluationPeriods>{{ alarm.evaluation_periods }}</EvaluationPeriods>
{% if alarm.datapoints_to_alarm is not none %}
<DatapointsToAlarm>{{ alarm.datapoints_to_alarm }}</DatapointsToAlarm>
{% endif %}
<InsufficientDataActions>
{% for action in alarm.insufficient_data_actions %}
<member>{{ action }}</member>
{% endfor %}
</InsufficientDataActions>
{% if alarm.metric_name is not none %}
<MetricName>{{ alarm.metric_name }}</MetricName>
{% endif %}
{% if alarm.metric_data_queries is not none %}
<Metrics>
{% for metric in alarm.metric_data_queries %}
<member>
<Id>{{ metric.id }}</Id>
{% if metric.label is not none %}
<Label>{{ metric.label }}</Label>
{% endif %}
{% if metric.expression is not none %}
<Expression>{{ metric.expression }}</Expression>
{% endif %}
{% if metric.metric_stat is not none %}
<MetricStat>
<Metric>
<Namespace>{{ metric.metric_stat.metric.namespace }}</Namespace>
<MetricName>{{ metric.metric_stat.metric.metric_name }}</MetricName>
<Dimensions>
{% for dim in metric.metric_stat.metric.dimensions %}
<member>
<Name>{{ dim.name }}</Name>
<Value>{{ dim.value }}</Value>
</member>
{% endfor %}
</Dimensions>
</Metric>
{% if metric.metric_stat.period is not none %}
<Period>{{ metric.metric_stat.period }}</Period>
{% endif %}
<Stat>{{ metric.metric_stat.stat }}</Stat>
{% if metric.metric_stat.unit is not none %}
<Unit>{{ metric.metric_stat.unit }}</Unit>
{% endif %}
</MetricStat>
{% endif %}
{% if metric.period is not none %}
<Period>{{ metric.period }}</Period>
{% endif %}
<ReturnData>{{ metric.return_data }}</ReturnData>
</member>
{% endfor %}
</Metrics>
{% endif %}
{% if alarm.namespace is not none %}
<Namespace>{{ alarm.namespace }}</Namespace>
{% endif %}
<OKActions>
{% for action in alarm.ok_actions %}
<member>{{ action }}</member>
{% endfor %}
</OKActions>
{% if alarm.period is not none %}
<Period>{{ alarm.period }}</Period>
{% endif %}
<StateReason>{{ alarm.state_reason }}</StateReason>
<StateReasonData>{{ alarm.state_reason_data }}</StateReasonData>
<StateUpdatedTimestamp>{{ alarm.state_updated_timestamp }}</StateUpdatedTimestamp>
<StateValue>{{ alarm.state_value }}</StateValue>
{% if alarm.statistic is not none %}
<Statistic>{{ alarm.statistic }}</Statistic>
{% endif %}
<Threshold>{{ alarm.threshold }}</Threshold>
{% if alarm.unit is not none %}
<Unit>{{ alarm.unit }}</Unit>
{% endif %}
</member>
{% endfor %}
</MetricAlarms>

View File

@ -141,6 +141,90 @@ def test_describe_alarms_for_metric():
alarms.get("MetricAlarms").should.have.length_of(1)
@mock_cloudwatch
def test_describe_alarms():
conn = boto3.client("cloudwatch", region_name="eu-central-1")
conn.put_metric_alarm(
AlarmName="testalarm1",
MetricName="cpu",
Namespace="blah",
Period=10,
EvaluationPeriods=5,
Statistic="Average",
Threshold=2,
ComparisonOperator="GreaterThanThreshold",
ActionsEnabled=True,
)
metric_data_queries = [
{
"Id": "metricA",
"Expression": "metricB + metricC",
"Label": "metricA",
"ReturnData": True,
},
{
"Id": "metricB",
"MetricStat": {
"Metric": {
"Namespace": "ns",
"MetricName": "metricB",
"Dimensions": [{"Name": "Name", "Value": "B"}],
},
"Period": 60,
"Stat": "Sum",
},
"ReturnData": False,
},
{
"Id": "metricC",
"MetricStat": {
"Metric": {
"Namespace": "AWS/Lambda",
"MetricName": "metricC",
"Dimensions": [{"Name": "Name", "Value": "C"}],
},
"Period": 60,
"Stat": "Sum",
"Unit": "Seconds",
},
"ReturnData": False,
},
]
conn.put_metric_alarm(
AlarmName="testalarm2",
EvaluationPeriods=1,
DatapointsToAlarm=1,
Metrics=metric_data_queries,
ComparisonOperator="GreaterThanThreshold",
Threshold=1.0,
)
alarms = conn.describe_alarms()
metric_alarms = alarms.get("MetricAlarms")
metric_alarms.should.have.length_of(2)
single_metric_alarm = [
alarm for alarm in metric_alarms if alarm["AlarmName"] == "testalarm1"
][0]
multiple_metric_alarm = [
alarm for alarm in metric_alarms if alarm["AlarmName"] == "testalarm2"
][0]
single_metric_alarm["MetricName"].should.equal("cpu")
single_metric_alarm.shouldnt.have.property("Metrics")
single_metric_alarm["Namespace"].should.equal("blah")
single_metric_alarm["Period"].should.equal(10)
single_metric_alarm["EvaluationPeriods"].should.equal(5)
single_metric_alarm["Statistic"].should.equal("Average")
single_metric_alarm["ComparisonOperator"].should.equal("GreaterThanThreshold")
single_metric_alarm["Threshold"].should.equal(2)
multiple_metric_alarm.shouldnt.have.property("MetricName")
multiple_metric_alarm["EvaluationPeriods"].should.equal(1)
multiple_metric_alarm["DatapointsToAlarm"].should.equal(1)
multiple_metric_alarm["Metrics"].should.equal(metric_data_queries)
multiple_metric_alarm["ComparisonOperator"].should.equal("GreaterThanThreshold")
multiple_metric_alarm["Threshold"].should.equal(1.0)
@mock_cloudwatch
def test_alarm_state():
client = boto3.client("cloudwatch", region_name="eu-central-1")