diff --git a/moto/cloudwatch/models.py b/moto/cloudwatch/models.py index 772672e0e..94668f32f 100644 --- a/moto/cloudwatch/models.py +++ b/moto/cloudwatch/models.py @@ -155,6 +155,18 @@ class FakeAlarm(BaseModel): self.state_updated_timestamp = datetime.utcnow() +def are_dimensions_same(metric_dimensions, dimensions): + for dimension in metric_dimensions: + for new_dimension in dimensions: + if ( + dimension.name != new_dimension.name + or dimension.value != new_dimension.value + ): + return False + + return True + + class MetricDatum(BaseModel): def __init__(self, namespace, name, value, dimensions, timestamp): self.namespace = namespace @@ -165,11 +177,17 @@ class MetricDatum(BaseModel): Dimension(dimension["Name"], dimension["Value"]) for dimension in dimensions ] - def filter(self, namespace, name, dimensions): + def filter(self, namespace, name, dimensions, already_present_metrics): if namespace and namespace != self.namespace: return False if name and name != self.name: return False + for metric in already_present_metrics: + if self.dimensions and are_dimensions_same( + metric.dimensions, self.dimensions + ): + return False + if dimensions and any( Dimension(d["Name"], d["Value"]) not in self.dimensions for d in dimensions ): @@ -514,12 +532,16 @@ class CloudWatchBackend(BaseBackend): def get_filtered_metrics(self, metric_name, namespace, dimensions): metrics = self.get_all_metrics() - metrics = [ - md - for md in metrics - if md.filter(namespace=namespace, name=metric_name, dimensions=dimensions) - ] - return metrics + new_metrics = [] + for md in metrics: + if md.filter( + namespace=namespace, + name=metric_name, + dimensions=dimensions, + already_present_metrics=new_metrics, + ): + new_metrics.append(md) + return new_metrics def _get_paginated(self, metrics): if len(metrics) > 500: diff --git a/tests/test_cloudwatch/test_cloudwatch_boto3.py b/tests/test_cloudwatch/test_cloudwatch_boto3.py index d448b0c58..55f0878d4 100644 --- a/tests/test_cloudwatch/test_cloudwatch_boto3.py +++ b/tests/test_cloudwatch/test_cloudwatch_boto3.py @@ -349,6 +349,123 @@ def test_get_metric_statistics(): datapoint["Sum"].should.equal(1.5) +@mock_cloudwatch +def test_duplicate_put_metric_data(): + conn = boto3.client("cloudwatch", region_name="us-east-1") + utc_now = datetime.now(tz=pytz.utc) + + conn.put_metric_data( + Namespace="tester", + MetricData=[ + dict( + MetricName="metric", + Dimensions=[{"Name": "Name", "Value": "B"}], + Value=1.5, + Timestamp=utc_now, + ) + ], + ) + + result = conn.list_metrics( + Namespace="tester", Dimensions=[{"Name": "Name", "Value": "B"}] + )["Metrics"] + len(result).should.equal(1) + + conn.put_metric_data( + Namespace="tester", + MetricData=[ + dict( + MetricName="metric", + Dimensions=[{"Name": "Name", "Value": "B"}], + Value=1.5, + Timestamp=utc_now, + ) + ], + ) + + result = conn.list_metrics( + Namespace="tester", Dimensions=[{"Name": "Name", "Value": "B"}] + )["Metrics"] + len(result).should.equal(1) + + conn.put_metric_data( + Namespace="tester", + MetricData=[ + dict( + MetricName="metric", + Dimensions=[{"Name": "Name", "Value": "B"}], + Value=1.5, + Timestamp=utc_now, + ) + ], + ) + + result = conn.list_metrics( + Namespace="tester", Dimensions=[{"Name": "Name", "Value": "B"}] + )["Metrics"] + result.should.equal( + [ + { + "Namespace": "tester", + "MetricName": "metric", + "Dimensions": [{"Name": "Name", "Value": "B"}], + } + ] + ) + + conn.put_metric_data( + Namespace="tester", + MetricData=[ + dict( + MetricName="metric", + Dimensions=[ + {"Name": "Name", "Value": "B"}, + {"Name": "Name", "Value": "C"}, + ], + Value=1.5, + Timestamp=utc_now, + ) + ], + ) + + result = conn.list_metrics( + Namespace="tester", Dimensions=[{"Name": "Name", "Value": "B"}] + )["Metrics"] + result.should.equal( + [ + { + "Namespace": "tester", + "MetricName": "metric", + "Dimensions": [{"Name": "Name", "Value": "B"}], + }, + { + "Namespace": "tester", + "MetricName": "metric", + "Dimensions": [ + {"Name": "Name", "Value": "B"}, + {"Name": "Name", "Value": "C"}, + ], + }, + ] + ) + + result = conn.list_metrics( + Namespace="tester", Dimensions=[{"Name": "Name", "Value": "C"}] + )["Metrics"] + result.should.equal( + [ + { + "Namespace": "tester", + "MetricName": "metric", + "Dimensions": [ + {"Name": "Name", "Value": "B"}, + {"Name": "Name", "Value": "C"}, + ], + } + ] + ) + + @mock_cloudwatch @freeze_time("2020-02-10 18:44:05") def test_custom_timestamp():