|
| 1 | +""" |
| 2 | +Tests for multiple dimension sets feature |
| 3 | +""" |
| 4 | + |
| 5 | +from __future__ import annotations |
| 6 | + |
| 7 | +import json |
| 8 | + |
| 9 | +import pytest |
| 10 | + |
| 11 | +from aws_lambda_powertools.metrics import Metrics, MetricUnit, SchemaValidationError |
| 12 | +from aws_lambda_powertools.metrics.provider.cloudwatch_emf.cloudwatch import AmazonCloudWatchEMFProvider |
| 13 | + |
| 14 | + |
| 15 | +def test_add_dimensions_creates_multiple_dimension_sets(capsys): |
| 16 | + # GIVEN a metrics instance |
| 17 | + metrics = AmazonCloudWatchEMFProvider(namespace="TestApp") |
| 18 | + |
| 19 | + # WHEN we add multiple dimension sets |
| 20 | + metrics.add_dimension(name="service", value="booking") |
| 21 | + metrics.add_dimensions({"environment": "prod", "region": "us-east-1"}) |
| 22 | + metrics.add_dimensions({"environment": "prod"}) |
| 23 | + metrics.add_dimensions({"region": "us-east-1"}) |
| 24 | + metrics.add_metric(name="SuccessfulRequests", unit=MetricUnit.Count, value=10) |
| 25 | + |
| 26 | + # THEN the serialized output should contain multiple dimension arrays |
| 27 | + output = metrics.serialize_metric_set() |
| 28 | + |
| 29 | + assert len(output["_aws"]["CloudWatchMetrics"]) == 1 |
| 30 | + dimensions = output["_aws"]["CloudWatchMetrics"][0]["Dimensions"] |
| 31 | + |
| 32 | + # Should have 4 dimension sets: primary + 3 added |
| 33 | + assert len(dimensions) == 4 |
| 34 | + assert dimensions[0] == ["service"] # Primary dimension set |
| 35 | + assert set(dimensions[1]) == {"environment", "region"} |
| 36 | + assert dimensions[2] == ["environment"] |
| 37 | + assert dimensions[3] == ["region"] |
| 38 | + |
| 39 | + # All dimension values should be in the root |
| 40 | + assert output["service"] == "booking" |
| 41 | + assert output["environment"] == "prod" |
| 42 | + assert output["region"] == "us-east-1" |
| 43 | + assert output["SuccessfulRequests"] == [10.0] |
| 44 | + |
| 45 | + |
| 46 | +def test_add_dimensions_with_metrics_wrapper(capsys): |
| 47 | + # GIVEN a Metrics instance (not provider directly) |
| 48 | + metrics = Metrics(namespace="TestApp", service="payment") |
| 49 | + |
| 50 | + # WHEN we use add_dimensions through the Metrics wrapper |
| 51 | + @metrics.log_metrics |
| 52 | + def handler(event, context): |
| 53 | + metrics.add_dimensions({"environment": "staging", "region": "us-west-2"}) |
| 54 | + metrics.add_dimensions({"environment": "staging"}) |
| 55 | + metrics.add_metric(name="PaymentProcessed", unit=MetricUnit.Count, value=1) |
| 56 | + |
| 57 | + handler({}, {}) |
| 58 | + |
| 59 | + # THEN the output should contain multiple dimension sets |
| 60 | + output = json.loads(capsys.readouterr().out.strip()) |
| 61 | + |
| 62 | + dimensions = output["_aws"]["CloudWatchMetrics"][0]["Dimensions"] |
| 63 | + assert len(dimensions) == 3 # Primary (service) + 2 added |
| 64 | + |
| 65 | + # Primary dimension from service parameter |
| 66 | + assert "service" in dimensions[0] |
| 67 | + |
| 68 | + # Check added dimension sets - they don't include service unless it's a default dimension |
| 69 | + assert set(dimensions[1]) == {"environment", "region"} |
| 70 | + assert set(dimensions[2]) == {"environment"} |
| 71 | + |
| 72 | + |
| 73 | +def test_add_dimensions_with_default_dimensions(): |
| 74 | + # GIVEN metrics with default dimensions |
| 75 | + metrics = AmazonCloudWatchEMFProvider(namespace="TestApp") |
| 76 | + metrics.set_default_dimensions(tenant_id="123", application="api") |
| 77 | + |
| 78 | + # WHEN we add dimension sets after setting defaults |
| 79 | + metrics.add_dimensions({"environment": "prod"}) |
| 80 | + metrics.add_dimensions({"region": "eu-west-1"}) |
| 81 | + metrics.add_metric(name="ApiCalls", unit=MetricUnit.Count, value=5) |
| 82 | + |
| 83 | + # THEN default dimensions should be included in all dimension sets |
| 84 | + output = metrics.serialize_metric_set() |
| 85 | + dimensions = output["_aws"]["CloudWatchMetrics"][0]["Dimensions"] |
| 86 | + |
| 87 | + # Each dimension set should include default dimensions |
| 88 | + assert set(dimensions[1]) == {"tenant_id", "application", "environment"} |
| 89 | + assert set(dimensions[2]) == {"tenant_id", "application", "region"} |
| 90 | + |
| 91 | + # Values should be in root |
| 92 | + assert output["tenant_id"] == "123" |
| 93 | + assert output["application"] == "api" |
| 94 | + assert output["environment"] == "prod" |
| 95 | + assert output["region"] == "eu-west-1" |
| 96 | + |
| 97 | + |
| 98 | +def test_add_dimensions_duplicate_keys_last_value_wins(): |
| 99 | + # GIVEN metrics with overlapping dimension keys |
| 100 | + metrics = AmazonCloudWatchEMFProvider(namespace="TestApp") |
| 101 | + |
| 102 | + # WHEN we add dimension sets with duplicate keys |
| 103 | + metrics.add_dimensions({"environment": "dev", "region": "us-east-1"}) |
| 104 | + metrics.add_dimensions({"environment": "staging", "region": "us-west-2"}) |
| 105 | + metrics.add_dimensions({"environment": "prod"}) # Last value for environment |
| 106 | + metrics.add_metric(name="TestMetric", unit=MetricUnit.Count, value=1) |
| 107 | + |
| 108 | + # THEN the last value should be used in the root |
| 109 | + output = metrics.serialize_metric_set() |
| 110 | + |
| 111 | + # Last values should win |
| 112 | + assert output["environment"] == "prod" |
| 113 | + assert output["region"] == "us-west-2" |
| 114 | + |
| 115 | + |
| 116 | +def test_add_dimensions_empty_dict_warns(): |
| 117 | + # GIVEN metrics instance |
| 118 | + metrics = AmazonCloudWatchEMFProvider(namespace="TestApp") |
| 119 | + |
| 120 | + # WHEN we add an empty dimensions dict |
| 121 | + with pytest.warns(UserWarning, match="Empty dimensions dictionary"): |
| 122 | + metrics.add_dimensions({}) |
| 123 | + |
| 124 | + # THEN no dimension set should be added |
| 125 | + assert len(metrics.dimension_sets) == 0 |
| 126 | + |
| 127 | + |
| 128 | +def test_add_dimensions_invalid_dimensions_skipped(): |
| 129 | + # GIVEN metrics instance |
| 130 | + metrics = AmazonCloudWatchEMFProvider(namespace="TestApp") |
| 131 | + |
| 132 | + # WHEN we add dimensions with empty names or values |
| 133 | + with pytest.warns(UserWarning, match="empty name or value"): |
| 134 | + metrics.add_dimensions({"": "value", "key": ""}) |
| 135 | + |
| 136 | + # THEN no dimension set should be added |
| 137 | + assert len(metrics.dimension_sets) == 0 |
| 138 | + |
| 139 | + |
| 140 | +def test_add_dimensions_exceeds_max_dimensions(): |
| 141 | + # GIVEN metrics with many dimensions |
| 142 | + metrics = AmazonCloudWatchEMFProvider(namespace="TestApp") |
| 143 | + |
| 144 | + # Add 29 dimensions to primary set (max is 30) |
| 145 | + for i in range(29): |
| 146 | + metrics.add_dimension(name=f"dim{i}", value=f"val{i}") |
| 147 | + |
| 148 | + # WHEN we try to add dimension set that would exceed max |
| 149 | + # THEN it should raise SchemaValidationError |
| 150 | + with pytest.raises(SchemaValidationError, match="Maximum dimensions"): |
| 151 | + metrics.add_dimensions({"extra1": "val1", "extra2": "val2"}) |
| 152 | + |
| 153 | + |
| 154 | +def test_add_dimensions_converts_values_to_strings(): |
| 155 | + # GIVEN metrics instance |
| 156 | + metrics = AmazonCloudWatchEMFProvider(namespace="TestApp") |
| 157 | + |
| 158 | + # WHEN we add dimensions with non-string values |
| 159 | + metrics.add_dimensions({"count": 123, "is_active": True, "ratio": 3.14}) |
| 160 | + metrics.add_metric(name="TestMetric", unit=MetricUnit.Count, value=1) |
| 161 | + |
| 162 | + # THEN values should be converted to strings |
| 163 | + output = metrics.serialize_metric_set() |
| 164 | + assert output["count"] == "123" |
| 165 | + assert output["is_active"] == "True" |
| 166 | + assert output["ratio"] == "3.14" |
| 167 | + |
| 168 | + |
| 169 | +def test_clear_metrics_clears_dimension_sets(capsys): |
| 170 | + # GIVEN metrics with dimension sets |
| 171 | + metrics = Metrics(namespace="TestApp", service="api") |
| 172 | + |
| 173 | + @metrics.log_metrics |
| 174 | + def handler(event, context): |
| 175 | + metrics.add_dimensions({"environment": "prod"}) |
| 176 | + metrics.add_dimensions({"region": "us-east-1"}) |
| 177 | + metrics.add_metric(name="Requests", unit=MetricUnit.Count, value=1) |
| 178 | + |
| 179 | + handler({}, {}) |
| 180 | + |
| 181 | + # WHEN we call clear_metrics (done automatically by decorator) |
| 182 | + # THEN dimension_sets should be cleared |
| 183 | + assert len(metrics.provider.dimension_sets) == 0 |
| 184 | + |
| 185 | + |
| 186 | +def test_add_dimensions_order_preserved(): |
| 187 | + # GIVEN metrics instance |
| 188 | + metrics = AmazonCloudWatchEMFProvider(namespace="TestApp") |
| 189 | + |
| 190 | + # WHEN we add dimension sets in specific order |
| 191 | + metrics.add_dimension(name="service", value="api") |
| 192 | + metrics.add_dimensions({"environment": "prod", "region": "us-east-1"}) |
| 193 | + metrics.add_dimensions({"environment": "prod"}) |
| 194 | + metrics.add_dimensions({"region": "us-east-1"}) |
| 195 | + metrics.add_metric(name="TestMetric", unit=MetricUnit.Count, value=1) |
| 196 | + |
| 197 | + # THEN dimension sets should appear in order added |
| 198 | + output = metrics.serialize_metric_set() |
| 199 | + dimensions = output["_aws"]["CloudWatchMetrics"][0]["Dimensions"] |
| 200 | + |
| 201 | + assert dimensions[0] == ["service"] |
| 202 | + assert set(dimensions[1]) == {"environment", "region"} |
| 203 | + assert dimensions[2] == ["environment"] |
| 204 | + assert dimensions[3] == ["region"] |
| 205 | + |
| 206 | + |
| 207 | +def test_add_dimensions_with_metadata(): |
| 208 | + # GIVEN metrics with metadata |
| 209 | + metrics = AmazonCloudWatchEMFProvider(namespace="TestApp") |
| 210 | + |
| 211 | + # WHEN we add dimension sets and metadata |
| 212 | + metrics.add_dimensions({"environment": "prod"}) |
| 213 | + metrics.add_metadata(key="request_id", value="abc-123") |
| 214 | + metrics.add_metric(name="ApiLatency", unit=MetricUnit.Milliseconds, value=150) |
| 215 | + |
| 216 | + # THEN both should be in output |
| 217 | + output = metrics.serialize_metric_set() |
| 218 | + |
| 219 | + assert "environment" in output |
| 220 | + assert output["request_id"] == "abc-123" |
| 221 | + # Primary dimension_set + 1 additional dimension set |
| 222 | + assert len(output["_aws"]["CloudWatchMetrics"][0]["Dimensions"]) == 2 |
| 223 | + |
| 224 | + |
| 225 | +def test_multiple_metrics_with_dimension_sets(): |
| 226 | + # GIVEN metrics with multiple metrics and dimension sets |
| 227 | + metrics = AmazonCloudWatchEMFProvider(namespace="TestApp") |
| 228 | + |
| 229 | + # WHEN we add multiple metrics with dimension sets |
| 230 | + metrics.add_dimensions({"environment": "prod", "region": "us-east-1"}) |
| 231 | + metrics.add_dimensions({"environment": "prod"}) |
| 232 | + metrics.add_metric(name="SuccessCount", unit=MetricUnit.Count, value=100) |
| 233 | + metrics.add_metric(name="ErrorCount", unit=MetricUnit.Count, value=5) |
| 234 | + metrics.add_metric(name="Latency", unit=MetricUnit.Milliseconds, value=250) |
| 235 | + |
| 236 | + # THEN all metrics should share the same dimension sets |
| 237 | + output = metrics.serialize_metric_set() |
| 238 | + |
| 239 | + assert len(output["_aws"]["CloudWatchMetrics"][0]["Metrics"]) == 3 |
| 240 | + # Primary (empty) + 2 added dimension sets |
| 241 | + assert len(output["_aws"]["CloudWatchMetrics"][0]["Dimensions"]) == 3 |
| 242 | + assert output["SuccessCount"] == [100.0] |
| 243 | + assert output["ErrorCount"] == [5.0] |
| 244 | + assert output["Latency"] == [250.0] |
| 245 | + |
| 246 | + |
| 247 | +def test_add_dimensions_with_high_resolution_metrics(): |
| 248 | + # GIVEN metrics with high resolution |
| 249 | + metrics = AmazonCloudWatchEMFProvider(namespace="TestApp") |
| 250 | + |
| 251 | + # WHEN we add dimension sets with high-resolution metrics |
| 252 | + metrics.add_dimensions({"function": "process_order"}) |
| 253 | + metrics.add_metric( |
| 254 | + name="ProcessingTime", |
| 255 | + unit=MetricUnit.Milliseconds, |
| 256 | + value=45, |
| 257 | + resolution=1, # High resolution |
| 258 | + ) |
| 259 | + |
| 260 | + # THEN dimension sets should work with high-resolution metrics |
| 261 | + output = metrics.serialize_metric_set() |
| 262 | + |
| 263 | + # Primary (empty) + 1 added dimension set |
| 264 | + assert len(output["_aws"]["CloudWatchMetrics"][0]["Dimensions"]) == 2 |
| 265 | + assert output["_aws"]["CloudWatchMetrics"][0]["Metrics"][0]["StorageResolution"] == 1 |
0 commit comments