Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions aws_lambda_powertools/metrics/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ def add_metric(
def add_dimension(self, name: str, value: str) -> None:
self.provider.add_dimension(name=name, value=value)

def add_dimensions(self, **dimensions: str) -> None:
"""Add a new set of dimensions creating an additional dimension array.

Creates a new dimension set in the CloudWatch EMF Dimensions array.
"""
self.provider.add_dimensions(**dimensions)

def serialize_metric_set(
self,
metrics: dict | None = None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def __init__(

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

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

metric_names_and_values.update({metric_name: metric_value})

# Build Dimensions array: primary set + additional dimension sets
dimension_arrays: list[list[str]] = [list(dimensions.keys())]
all_dimensions: dict[str, str] = dict(dimensions)

# Add each additional dimension set
for dim_set in self.dimension_sets:
all_dimensions.update(dim_set)
dimension_arrays.append(list(dim_set.keys()))

return {
"_aws": {
"Timestamp": self.timestamp or int(datetime.datetime.now().timestamp() * 1000), # epoch
"CloudWatchMetrics": [
{
"Namespace": self.namespace, # "test_namespace"
"Dimensions": [list(dimensions.keys())], # [ "service" ]
"Dimensions": dimension_arrays, # [["service"], ["env", "region"]]
"Metrics": metric_definition,
},
],
},
# NOTE: Mypy doesn't recognize splats '** syntax' in TypedDict
**dimensions, # "service": "test_service"
**metadata, # type: ignore[typeddict-item] # "username": "test"
**metric_names_and_values, # "single_metric": 1.0
**all_dimensions, # type: ignore[typeddict-item] # All dimension key-value pairs
**metadata, # type: ignore[typeddict-item]
**metric_names_and_values,
}

def add_dimension(self, name: str, value: str) -> None:
Expand Down Expand Up @@ -316,6 +326,70 @@ def add_dimension(self, name: str, value: str) -> None:

self.dimension_set[name] = value

def add_dimensions(self, **dimensions: str) -> None:
"""Add a new set of dimensions creating an additional dimension array.

Creates a new dimension set in the CloudWatch EMF Dimensions array.

Example
-------
**Add multiple dimension sets**

metrics.add_dimensions(environment="prod", region="us-east-1")

Parameters
----------
dimensions : str
Dimension key-value pairs as keyword arguments
"""
logger.debug(f"Adding dimension set: {dimensions}")

if not dimensions:
warnings.warn(
"Empty dimensions dictionary provided",
category=PowertoolsUserWarning,
stacklevel=2,
)
return

sanitized = self._sanitize_dimensions(dimensions)
if not sanitized:
return

self._validate_dimension_limit(sanitized)

self.dimension_sets.append({**self.default_dimensions, **sanitized})

def _sanitize_dimensions(self, dimensions: dict[str, str]) -> dict[str, str]:
"""Convert dimension values to strings and filter out empty ones."""
sanitized: dict[str, str] = {}

for name, value in dimensions.items():
str_name = str(name)
str_value = str(value)

if not str_name.strip() or not str_value.strip():
warnings.warn(
f"Dimension {str_name} has empty name or value",
category=PowertoolsUserWarning,
stacklevel=2,
)
continue

sanitized[str_name] = str_value

return sanitized

def _validate_dimension_limit(self, new_dimensions: dict[str, str]) -> None:
"""Validate that adding new dimensions won't exceed CloudWatch limits."""
all_keys = set(self.dimension_set.keys())
for ds in self.dimension_sets:
all_keys.update(ds.keys())
all_keys.update(new_dimensions.keys())

if len(all_keys) > MAX_DIMENSIONS:
raise SchemaValidationError(f"Maximum dimensions ({MAX_DIMENSIONS}) exceeded")

def add_metadata(self, key: str, value: Any) -> None:
"""Adds high cardinal metadata for metrics object

Expand Down Expand Up @@ -377,6 +451,7 @@ def clear_metrics(self) -> None:
logger.debug("Clearing out existing metric set from memory")
self.metric_set.clear()
self.dimension_set.clear()
self.dimension_sets.clear()
self.metadata_set.clear()
self.set_default_dimensions(**self.default_dimensions)

Expand Down
39 changes: 30 additions & 9 deletions docs/core/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ If you're new to Amazon CloudWatch, there are five terminologies you must be awa
* **Dimensions**. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example `ColdStart` metric by Payment `service`.
* **Metric**. It's the name of the metric, for example: `SuccessfulBooking` or `UpdatedBooking`.
* **Unit**. It's a value representing the unit of measure for the corresponding metric, for example: `Count` or `Seconds`.
* **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"}.
* **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"}.

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

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

### Adding multiple dimension sets

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.

Each call to `add_dimensions` creates a new dimension array in the CloudWatch EMF output, enabling different views of the same metric data.

=== "add_dimensions.py"

```python hl_lines="12-13"
--8<-- "examples/metrics/src/add_dimensions.py"
```

=== "add_dimensions_output.json"

```json hl_lines="8-12"
--8<-- "examples/metrics/src/add_dimensions_output.json"
```

???+ tip "When to use multiple dimension sets"
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`.

### Changing default timestamp

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.
Expand Down Expand Up @@ -233,12 +254,12 @@ The priority of the `function_name` dimension value is defined as:

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

| Setting | Description | Environment variable | Default |
| ------------------ | ------------------------------------------------------------ | ---------------------------------- | ------- |
| **Namespace Name** | Sets **namespace** used for metrics. | `POWERTOOLS_METRICS_NAMESPACE` | `None` |
| **Service** | Sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `None` |
| **Function Name** | Function name used as dimension for the **ColdStart** metric. | `POWERTOOLS_METRICS_FUNCTION_NAME` | `None` |
| **Disable Powertools Metrics** | **Disables** all metrics emitted by Powertools. | `POWERTOOLS_METRICS_DISABLED` | `None` |
| Setting | Description | Environment variable | Default |
| ------------------------------ | ------------------------------------------------------------------- | ---------------------------------- | ------- |
| **Namespace Name** | Sets **namespace** used for metrics. | `POWERTOOLS_METRICS_NAMESPACE` | `None` |
| **Service** | Sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `None` |
| **Function Name** | Function name used as dimension for the **ColdStart** metric. | `POWERTOOLS_METRICS_FUNCTION_NAME` | `None` |
| **Disable Powertools Metrics** | **Disables** all metrics emitted by Powertools. | `POWERTOOLS_METRICS_DISABLED` | `None` |

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

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

Current providers:

| Provider | Notes |
| ------------------------------------- | -------------------------------------------------------- |
| Provider | Notes |
| ---------------------------------------- | -------------------------------------------------------- |
| [Datadog](./datadog.md){target="_blank"} | Uses Datadog SDK and Datadog Lambda Extension by default |

## Testing your code
Expand Down
17 changes: 17 additions & 0 deletions examples/metrics/src/add_dimensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from aws_lambda_powertools import Metrics
from aws_lambda_powertools.metrics import MetricUnit
from aws_lambda_powertools.utilities.typing import LambdaContext

metrics = Metrics()


@metrics.log_metrics
def lambda_handler(event: dict, context: LambdaContext):
# Add primary dimension
metrics.add_dimension(name="service", value="booking")

# Add multiple dimension sets for different aggregation views
metrics.add_dimensions(environment="prod", region="us-east-1")
metrics.add_dimensions(environment="prod")

metrics.add_metric(name="SuccessfulBooking", unit=MetricUnit.Count, value=1)
25 changes: 25 additions & 0 deletions examples/metrics/src/add_dimensions_output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"_aws": {
"Timestamp": 1656620400000,
"CloudWatchMetrics": [
{
"Namespace": "ServerlessAirline",
"Dimensions": [
["service"],
["environment", "region"],
["environment"]
],
"Metrics": [
{
"Name": "SuccessfulBooking",
"Unit": "Count"
}
]
}
]
},
"service": "booking",
"environment": "prod",
"region": "us-east-1",
"SuccessfulBooking": [1.0]
}
Loading