Skip to content

Commit ef3da99

Browse files
feat(metrics): add support for multiple dimension sets (#7848)
* feat(metrics): add support for multiple dimension sets Implements add_dimensions() method to create multiple dimension sets in CloudWatch EMF output, enabling metric aggregation across different dimension combinations. - Add dimension_sets list to track multiple dimension arrays - Implement add_dimensions() in AmazonCloudWatchEMFProvider - Update serialize_metric_set to output all dimension sets - Add add_dimensions() to Metrics wrapper class - Update clear_metrics to clear dimension_sets - Add comprehensive test suite (13 tests) - Handle dimension key conflicts (last value wins) - Include default dimensions in all dimension sets Resolves #6198 * adding docs + small refactor --------- Co-authored-by: Leandro Damascena <lcdama@amazon.pt>
1 parent c4631a9 commit ef3da99

File tree

6 files changed

+423
-13
lines changed

6 files changed

+423
-13
lines changed

aws_lambda_powertools/metrics/metrics.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ def add_metric(
123123
def add_dimension(self, name: str, value: str) -> None:
124124
self.provider.add_dimension(name=name, value=value)
125125

126+
def add_dimensions(self, **dimensions: str) -> None:
127+
"""Add a new set of dimensions creating an additional dimension array.
128+
129+
Creates a new dimension set in the CloudWatch EMF Dimensions array.
130+
"""
131+
self.provider.add_dimensions(**dimensions)
132+
126133
def serialize_metric_set(
127134
self,
128135
metrics: dict | None = None,

aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def __init__(
9494

9595
self.metadata_set = metadata_set if metadata_set is not None else {}
9696
self.timestamp: int | None = None
97+
self.dimension_sets: list[dict[str, str]] = [] # Store multiple dimension sets
9798

9899
self._metric_units = [unit.value for unit in MetricUnit]
99100
self._metric_unit_valid_options = list(MetricUnit.__members__)
@@ -256,21 +257,30 @@ def serialize_metric_set(
256257

257258
metric_names_and_values.update({metric_name: metric_value})
258259

260+
# Build Dimensions array: primary set + additional dimension sets
261+
dimension_arrays: list[list[str]] = [list(dimensions.keys())]
262+
all_dimensions: dict[str, str] = dict(dimensions)
263+
264+
# Add each additional dimension set
265+
for dim_set in self.dimension_sets:
266+
all_dimensions.update(dim_set)
267+
dimension_arrays.append(list(dim_set.keys()))
268+
259269
return {
260270
"_aws": {
261271
"Timestamp": self.timestamp or int(datetime.datetime.now().timestamp() * 1000), # epoch
262272
"CloudWatchMetrics": [
263273
{
264274
"Namespace": self.namespace, # "test_namespace"
265-
"Dimensions": [list(dimensions.keys())], # [ "service" ]
275+
"Dimensions": dimension_arrays, # [["service"], ["env", "region"]]
266276
"Metrics": metric_definition,
267277
},
268278
],
269279
},
270280
# NOTE: Mypy doesn't recognize splats '** syntax' in TypedDict
271-
**dimensions, # "service": "test_service"
272-
**metadata, # type: ignore[typeddict-item] # "username": "test"
273-
**metric_names_and_values, # "single_metric": 1.0
281+
**all_dimensions, # type: ignore[typeddict-item] # All dimension key-value pairs
282+
**metadata, # type: ignore[typeddict-item]
283+
**metric_names_and_values,
274284
}
275285

276286
def add_dimension(self, name: str, value: str) -> None:
@@ -316,6 +326,70 @@ def add_dimension(self, name: str, value: str) -> None:
316326

317327
self.dimension_set[name] = value
318328

329+
def add_dimensions(self, **dimensions: str) -> None:
330+
"""Add a new set of dimensions creating an additional dimension array.
331+
332+
Creates a new dimension set in the CloudWatch EMF Dimensions array.
333+
334+
Example
335+
-------
336+
**Add multiple dimension sets**
337+
338+
metrics.add_dimensions(environment="prod", region="us-east-1")
339+
340+
Parameters
341+
----------
342+
dimensions : str
343+
Dimension key-value pairs as keyword arguments
344+
"""
345+
logger.debug(f"Adding dimension set: {dimensions}")
346+
347+
if not dimensions:
348+
warnings.warn(
349+
"Empty dimensions dictionary provided",
350+
category=PowertoolsUserWarning,
351+
stacklevel=2,
352+
)
353+
return
354+
355+
sanitized = self._sanitize_dimensions(dimensions)
356+
if not sanitized:
357+
return
358+
359+
self._validate_dimension_limit(sanitized)
360+
361+
self.dimension_sets.append({**self.default_dimensions, **sanitized})
362+
363+
def _sanitize_dimensions(self, dimensions: dict[str, str]) -> dict[str, str]:
364+
"""Convert dimension values to strings and filter out empty ones."""
365+
sanitized: dict[str, str] = {}
366+
367+
for name, value in dimensions.items():
368+
str_name = str(name)
369+
str_value = str(value)
370+
371+
if not str_name.strip() or not str_value.strip():
372+
warnings.warn(
373+
f"Dimension {str_name} has empty name or value",
374+
category=PowertoolsUserWarning,
375+
stacklevel=2,
376+
)
377+
continue
378+
379+
sanitized[str_name] = str_value
380+
381+
return sanitized
382+
383+
def _validate_dimension_limit(self, new_dimensions: dict[str, str]) -> None:
384+
"""Validate that adding new dimensions won't exceed CloudWatch limits."""
385+
all_keys = set(self.dimension_set.keys())
386+
for ds in self.dimension_sets:
387+
all_keys.update(ds.keys())
388+
all_keys.update(new_dimensions.keys())
389+
390+
if len(all_keys) > MAX_DIMENSIONS:
391+
raise SchemaValidationError(f"Maximum dimensions ({MAX_DIMENSIONS}) exceeded")
392+
319393
def add_metadata(self, key: str, value: Any) -> None:
320394
"""Adds high cardinal metadata for metrics object
321395
@@ -377,6 +451,7 @@ def clear_metrics(self) -> None:
377451
logger.debug("Clearing out existing metric set from memory")
378452
self.metric_set.clear()
379453
self.dimension_set.clear()
454+
self.dimension_sets.clear()
380455
self.metadata_set.clear()
381456
self.set_default_dimensions(**self.default_dimensions)
382457

docs/core/metrics.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ If you're new to Amazon CloudWatch, there are five terminologies you must be awa
2222
* **Dimensions**. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example `ColdStart` metric by Payment `service`.
2323
* **Metric**. It's the name of the metric, for example: `SuccessfulBooking` or `UpdatedBooking`.
2424
* **Unit**. It's a value representing the unit of measure for the corresponding metric, for example: `Count` or `Seconds`.
25-
* **Resolution**. It's a value representing the storage resolution for the corresponding metric. Metrics can be either Standard or High resolution. Read more [here](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/publishingMetrics.html#high-resolution-metrics){target="_blank"}.
25+
* **Resolution**. It's a value representing the storage resolution for the corresponding metric. Metrics can be either Standard or High resolution. Read more in the [high-resolution metrics documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/publishingMetrics.html#high-resolution-metrics){target="_blank"}.
2626

2727
<figure>
2828
<img src="../../media/metrics_terminology.png" alt="Terminology" />
@@ -136,6 +136,27 @@ If you'd like to remove them at some point, you can use `clear_default_dimension
136136

137137
**Note:** Dimensions with empty values will not be included.
138138

139+
### Adding multiple dimension sets
140+
141+
You can use `add_dimensions` method to create multiple dimension sets in a single EMF blob. This allows you to aggregate metrics across different dimension combinations without emitting separate metric blobs.
142+
143+
Each call to `add_dimensions` creates a new dimension array in the CloudWatch EMF output, enabling different views of the same metric data.
144+
145+
=== "add_dimensions.py"
146+
147+
```python hl_lines="12-13"
148+
--8<-- "examples/metrics/src/add_dimensions.py"
149+
```
150+
151+
=== "add_dimensions_output.json"
152+
153+
```json hl_lines="8-12"
154+
--8<-- "examples/metrics/src/add_dimensions_output.json"
155+
```
156+
157+
???+ tip "When to use multiple dimension sets"
158+
Use `add_dimensions` when you need to query the same metric with different dimension combinations. For example, you might want to see `SuccessfulBooking` aggregated by `environment` alone, or by both `environment` and `region`.
159+
139160
### Changing default timestamp
140161

141162
When creating metrics, we use the current timestamp. If you want to change the timestamp of all the metrics you create, utilize the `set_timestamp` function. You can specify a datetime object or an integer representing an epoch timestamp in milliseconds.
@@ -233,12 +254,12 @@ The priority of the `function_name` dimension value is defined as:
233254

234255
The following environment variable is available to configure Metrics at a global scope:
235256

236-
| Setting | Description | Environment variable | Default |
237-
| ------------------ | ------------------------------------------------------------ | ---------------------------------- | ------- |
238-
| **Namespace Name** | Sets **namespace** used for metrics. | `POWERTOOLS_METRICS_NAMESPACE` | `None` |
239-
| **Service** | Sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `None` |
240-
| **Function Name** | Function name used as dimension for the **ColdStart** metric. | `POWERTOOLS_METRICS_FUNCTION_NAME` | `None` |
241-
| **Disable Powertools Metrics** | **Disables** all metrics emitted by Powertools. | `POWERTOOLS_METRICS_DISABLED` | `None` |
257+
| Setting | Description | Environment variable | Default |
258+
| ------------------------------ | ------------------------------------------------------------------- | ---------------------------------- | ------- |
259+
| **Namespace Name** | Sets **namespace** used for metrics. | `POWERTOOLS_METRICS_NAMESPACE` | `None` |
260+
| **Service** | Sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `None` |
261+
| **Function Name** | Function name used as dimension for the **ColdStart** metric. | `POWERTOOLS_METRICS_FUNCTION_NAME` | `None` |
262+
| **Disable Powertools Metrics** | **Disables** all metrics emitted by Powertools. | `POWERTOOLS_METRICS_DISABLED` | `None` |
242263

243264
`POWERTOOLS_METRICS_NAMESPACE` is also available on a per-instance basis with the `namespace` parameter, which will consequently override the environment variable value.
244265

@@ -393,8 +414,8 @@ We provide a thin-wrapper on top of the most requested observability providers.
393414

394415
Current providers:
395416

396-
| Provider | Notes |
397-
| ------------------------------------- | -------------------------------------------------------- |
417+
| Provider | Notes |
418+
| ---------------------------------------- | -------------------------------------------------------- |
398419
| [Datadog](./datadog.md){target="_blank"} | Uses Datadog SDK and Datadog Lambda Extension by default |
399420

400421
## Testing your code
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from aws_lambda_powertools import Metrics
2+
from aws_lambda_powertools.metrics import MetricUnit
3+
from aws_lambda_powertools.utilities.typing import LambdaContext
4+
5+
metrics = Metrics()
6+
7+
8+
@metrics.log_metrics
9+
def lambda_handler(event: dict, context: LambdaContext):
10+
# Add primary dimension
11+
metrics.add_dimension(name="service", value="booking")
12+
13+
# Add multiple dimension sets for different aggregation views
14+
metrics.add_dimensions(environment="prod", region="us-east-1")
15+
metrics.add_dimensions(environment="prod")
16+
17+
metrics.add_metric(name="SuccessfulBooking", unit=MetricUnit.Count, value=1)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"_aws": {
3+
"Timestamp": 1656620400000,
4+
"CloudWatchMetrics": [
5+
{
6+
"Namespace": "ServerlessAirline",
7+
"Dimensions": [
8+
["service"],
9+
["environment", "region"],
10+
["environment"]
11+
],
12+
"Metrics": [
13+
{
14+
"Name": "SuccessfulBooking",
15+
"Unit": "Count"
16+
}
17+
]
18+
}
19+
]
20+
},
21+
"service": "booking",
22+
"environment": "prod",
23+
"region": "us-east-1",
24+
"SuccessfulBooking": [1.0]
25+
}

0 commit comments

Comments
 (0)