From f8c2b621dbb281a2025bcc8db9231ad89ea459d8 Mon Sep 17 00:00:00 2001 From: steffyP Date: Thu, 21 Apr 2022 13:32:57 +0200 Subject: [PATCH] cloudwatch: filter 'dimensions' for get_metric_data (#5041) --- moto/cloudwatch/models.py | 29 ++++- .../test_cloudwatch/test_cloudwatch_boto3.py | 108 ++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/moto/cloudwatch/models.py b/moto/cloudwatch/models.py index 868c01cb6..6a247c754 100644 --- a/moto/cloudwatch/models.py +++ b/moto/cloudwatch/models.py @@ -41,6 +41,9 @@ class Dimension(object): def __ne__(self, item): # Only needed on Py2; Py3 defines it implicitly return self != item + def __lt__(self, other): + return self.name < other.name and self.value < other.name + class Metric(object): def __init__(self, metric_name, namespace, dimensions): @@ -478,6 +481,7 @@ class CloudWatchBackend(BaseBackend): query_ns = query["metric_stat._metric._namespace"] query_name = query["metric_stat._metric._metric_name"] delta = timedelta(seconds=int(query["metric_stat._period"])) + dimensions = self._extract_dimensions_from_get_metric_data_query(query) result_vals = [] timestamps = [] stat = query["metric_stat._stat"] @@ -494,11 +498,19 @@ class CloudWatchBackend(BaseBackend): for md in period_md if md.namespace == query_ns and md.name == query_name ] + if dimensions: + query_period_data = [ + md + for md in period_md + if sorted(md.dimensions) == sorted(dimensions) + ] metric_values = [m.value for m in query_period_data] if len(metric_values) > 0: - if stat == "Average": + if stat == "SampleCount": + result_vals.append(len(metric_values)) + elif stat == "Average": result_vals.append(sum(metric_values) / len(metric_values)) elif stat == "Minimum": result_vals.append(min(metric_values)) @@ -679,5 +691,20 @@ class CloudWatchBackend(BaseBackend): else: return None, metrics + def _extract_dimensions_from_get_metric_data_query(self, query): + dimensions = [] + prefix = "metric_stat._metric._dimensions.member." + suffix_name = "._name" + suffix_value = "._value" + counter = 1 + + while query.get(f"{prefix}{counter}{suffix_name}") and counter <= 10: + name = query.get(f"{prefix}{counter}{suffix_name}") + value = query.get(f"{prefix}{counter}{suffix_value}") + dimensions.append(Dimension(name=name, value=value)) + counter = counter + 1 + + return dimensions + cloudwatch_backends = BackendDict(CloudWatchBackend, "cloudwatch") diff --git a/tests/test_cloudwatch/test_cloudwatch_boto3.py b/tests/test_cloudwatch/test_cloudwatch_boto3.py index 05799619f..d8ee2aad7 100644 --- a/tests/test_cloudwatch/test_cloudwatch_boto3.py +++ b/tests/test_cloudwatch/test_cloudwatch_boto3.py @@ -785,6 +785,114 @@ def test_get_metric_data_for_multiple_metrics(): res2["Values"].should.equal([25.0]) +@mock_cloudwatch +def test_get_metric_data_for_dimensions(): + utc_now = datetime.now(tz=pytz.utc) + cloudwatch = boto3.client("cloudwatch", "eu-west-1") + namespace = "my_namespace/" + + # If the metric is created with multiple dimensions, then the data points for that metric can be retrieved only by specifying all the configured dimensions. + # https://aws.amazon.com/premiumsupport/knowledge-center/cloudwatch-getmetricstatistics-data/ + server_prod = {"Name": "Server", "Value": "Prod"} + dimension_berlin = [server_prod, {"Name": "Domain", "Value": "Berlin"}] + dimension_frankfurt = [server_prod, {"Name": "Domain", "Value": "Frankfurt"}] + + # put metric data + cloudwatch.put_metric_data( + Namespace=namespace, + MetricData=[ + { + "MetricName": "metric1", + "Value": 50, + "Dimensions": dimension_berlin, + "Unit": "Seconds", + "Timestamp": utc_now, + } + ], + ) + cloudwatch.put_metric_data( + Namespace=namespace, + MetricData=[ + { + "MetricName": "metric1", + "Value": 25, + "Unit": "Seconds", + "Dimensions": dimension_frankfurt, + "Timestamp": utc_now, + } + ], + ) + # get_metric_data + response = cloudwatch.get_metric_data( + MetricDataQueries=[ + { + "Id": "result1", + "MetricStat": { + "Metric": { + "Namespace": namespace, + "MetricName": "metric1", + "Dimensions": dimension_frankfurt, + }, + "Period": 60, + "Stat": "SampleCount", + }, + }, + { + "Id": "result2", + "MetricStat": { + "Metric": { + "Namespace": namespace, + "MetricName": "metric1", + "Dimensions": dimension_berlin, + }, + "Period": 60, + "Stat": "Sum", + }, + }, + { + "Id": "result3", + "MetricStat": { + "Metric": { + "Namespace": namespace, + "MetricName": "metric1", + "Dimensions": [server_prod], + }, + "Period": 60, + "Stat": "Sum", + }, + }, + { + "Id": "result4", + "MetricStat": { + "Metric": {"Namespace": namespace, "MetricName": "metric1"}, + "Period": 60, + "Stat": "Sum", + }, + }, + ], + StartTime=utc_now - timedelta(seconds=60), + EndTime=utc_now + timedelta(seconds=60), + ) + # + len(response["MetricDataResults"]).should.equal(4) + + res1 = [res for res in response["MetricDataResults"] if res["Id"] == "result1"][0] + # expect sample count for dimension_frankfurt + res1["Values"].should.equal([1.0]) + + res2 = [res for res in response["MetricDataResults"] if res["Id"] == "result2"][0] + # expect sum for dimension_berlin + res2["Values"].should.equal([50.0]) + + res3 = [res for res in response["MetricDataResults"] if res["Id"] == "result3"][0] + # expect no result, as server_prod is only a part of other dimensions, e.g. there is no match + res3["Values"].should.equal([]) + + res4 = [res for res in response["MetricDataResults"] if res["Id"] == "result4"][0] + # expect sum of both metrics, as we did not filter for dimensions + res4["Values"].should.equal([75.0]) + + @mock_cloudwatch @mock_s3 def test_cloudwatch_return_s3_metrics():