From 6b50b8020f77529dca2f67a77ce5725f2520e0a2 Mon Sep 17 00:00:00 2001 From: steffyP Date: Mon, 14 Nov 2022 12:41:42 +0100 Subject: [PATCH] CloudWatch: add support for put-metric-data with values list (#5658) --- moto/cloudwatch/models.py | 62 ++++++++-- .../test_cloudwatch/test_cloudwatch_boto3.py | 109 ++++++++++++++++++ 2 files changed, 162 insertions(+), 9 deletions(-) diff --git a/moto/cloudwatch/models.py b/moto/cloudwatch/models.py index 6ba95fc4b..83603d103 100644 --- a/moto/cloudwatch/models.py +++ b/moto/cloudwatch/models.py @@ -473,22 +473,66 @@ class CloudWatchBackend(BaseBackend): raise InvalidParameterValue( f"The value NaN for parameter MetricData.member.{i + 1}.Value is invalid." ) + if metric.get("Values.member"): + if "Value" in metric: + raise InvalidParameterValue( + f"The parameters MetricData.member.{i+1}.Value and MetricData.member.{i+1}.Values are mutually exclusive and you have specified both." + ) + if metric.get("Counts.member"): + if len(metric["Counts.member"]) != len(metric["Values.member"]): + raise InvalidParameterValue( + f"The parameters MetricData.member.{i+1}.Values and MetricData.member.{i+1}.Counts must be of the same size." + ) + for value in metric["Values.member"]: + if value.lower() == "nan": + raise InvalidParameterValue( + f"The value {value} for parameter MetricData.member.{i + 1}.Values is invalid." + ) for metric_member in metric_data: # Preserve "datetime" for get_metric_statistics comparisons timestamp = metric_member.get("Timestamp") if timestamp is not None and type(timestamp) != datetime: timestamp = parser.parse(timestamp) - self.metric_data.append( - MetricDatum( - namespace, - metric_member["MetricName"], - float(metric_member.get("Value", 0)), - metric_member.get("Dimensions.member", _EMPTY_LIST), - timestamp, - metric_member.get("Unit"), + metric_name = metric_member["MetricName"] + dimension = metric_member.get("Dimensions.member", _EMPTY_LIST) + unit = metric_member.get("Unit") + + # put_metric_data can include "value" as single value or "values" as a list + if metric_member.get("Values.member"): + values = metric_member["Values.member"] + # value[i] should be added count[i] times (with default count 1) + counts = metric_member.get("Counts.member") or ["1"] * len(values) + for i in range(0, len(values)): + value = values[i] + timestamp = metric_member.get("Timestamp") + if timestamp is not None and type(timestamp) != datetime: + timestamp = parser.parse(timestamp) + + # add the value count[i] times + for _ in range(0, int(float(counts[i]))): + self.metric_data.append( + MetricDatum( + namespace, + metric_name, + float(value), + dimension, + timestamp, + unit, + ) + ) + else: + # there is only a single value + self.metric_data.append( + MetricDatum( + namespace, + metric_name, + float(metric_member.get("Value", 0)), + dimension, + timestamp, + unit, + ) ) - ) def get_metric_data( self, diff --git a/tests/test_cloudwatch/test_cloudwatch_boto3.py b/tests/test_cloudwatch/test_cloudwatch_boto3.py index 2e4a85ba2..79e90c17e 100644 --- a/tests/test_cloudwatch/test_cloudwatch_boto3.py +++ b/tests/test_cloudwatch/test_cloudwatch_boto3.py @@ -52,6 +52,115 @@ def test_put_metric_data_can_not_have_nan(): ) +@mock_cloudwatch +def test_put_metric_data_can_not_have_value_and_values(): + client = boto3.client("cloudwatch", region_name="us-west-2") + utc_now = datetime.now(tz=pytz.utc) + with pytest.raises(ClientError) as exc: + client.put_metric_data( + Namespace="mynamespace", + MetricData=[ + { + "MetricName": "mymetric", + "Timestamp": utc_now, + "Value": 1.5, + "Values": [1.0, 10.0], + "Unit": "Count", + } + ], + ) + err = exc.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.equal( + "The parameters MetricData.member.1.Value and MetricData.member.1.Values are mutually exclusive and you have specified both." + ) + + +@mock_cloudwatch +def test_put_metric_data_can_not_have_and_values_mismatched_counts(): + client = boto3.client("cloudwatch", region_name="us-west-2") + utc_now = datetime.now(tz=pytz.utc) + with pytest.raises(ClientError) as exc: + client.put_metric_data( + Namespace="mynamespace", + MetricData=[ + { + "MetricName": "mymetric", + "Timestamp": utc_now, + "Values": [1.0, 10.0], + "Counts": [2, 4, 1], + "Unit": "Count", + } + ], + ) + err = exc.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.equal( + "The parameters MetricData.member.1.Values and MetricData.member.1.Counts must be of the same size." + ) + + +@mock_cloudwatch +def test_put_metric_data_values_and_counts(): + client = boto3.client("cloudwatch", region_name="us-west-2") + utc_now = datetime.now(tz=pytz.utc) + namespace = "values" + metric = "mymetric" + client.put_metric_data( + Namespace=namespace, + MetricData=[ + { + "MetricName": metric, + "Timestamp": utc_now, + "Values": [1.0, 10.0], + "Counts": [2, 4], + } + ], + ) + stats = client.get_metric_statistics( + Namespace=namespace, + MetricName=metric, + StartTime=utc_now - timedelta(seconds=60), + EndTime=utc_now + timedelta(seconds=60), + Period=60, + Statistics=["SampleCount", "Sum", "Maximum"], + ) + datapoint = stats["Datapoints"][0] + datapoint["SampleCount"].should.equal(6.0) + datapoint["Sum"].should.equal(42.0) + datapoint["Maximum"].should.equal(10.0) + + +@mock_cloudwatch +def test_put_metric_data_values_without_counts(): + client = boto3.client("cloudwatch", region_name="us-west-2") + utc_now = datetime.now(tz=pytz.utc) + namespace = "values" + metric = "mymetric" + client.put_metric_data( + Namespace=namespace, + MetricData=[ + { + "MetricName": metric, + "Timestamp": utc_now, + "Values": [1.0, 10.0, 23.45], + } + ], + ) + stats = client.get_metric_statistics( + Namespace=namespace, + MetricName=metric, + StartTime=utc_now - timedelta(seconds=60), + EndTime=utc_now + timedelta(seconds=60), + Period=60, + Statistics=["SampleCount", "Sum", "Maximum"], + ) + datapoint = stats["Datapoints"][0] + datapoint["SampleCount"].should.equal(3.0) + datapoint["Sum"].should.equal(34.45) + datapoint["Maximum"].should.equal(23.45) + + @mock_cloudwatch def test_put_metric_data_with_statistics(): conn = boto3.client("cloudwatch", region_name="us-east-1")