Skip to content

feat(metrics) - add support for high resolution metrics #1915

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
11 changes: 9 additions & 2 deletions aws_lambda_powertools/metrics/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"""CloudWatch Embedded Metric Format utility
"""
from .base import MetricUnit
from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError
from .base import MetricResolution, MetricUnit
from .exceptions import (
MetricResolutionError,
MetricUnitError,
MetricValueError,
SchemaValidationError,
)
from .metric import single_metric
from .metrics import EphemeralMetrics, Metrics

Expand All @@ -11,6 +16,8 @@
"single_metric",
"MetricUnit",
"MetricUnitError",
"MetricResolution",
"MetricResolutionError",
"SchemaValidationError",
"MetricValueError",
]
109 changes: 96 additions & 13 deletions aws_lambda_powertools/metrics/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@

from ..shared import constants
from ..shared.functions import resolve_env_var_choice
from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError
from .exceptions import (
MetricResolutionError,
MetricUnitError,
MetricValueError,
SchemaValidationError,
)

logger = logging.getLogger(__name__)

Expand All @@ -22,6 +27,11 @@
is_cold_start = True


class MetricResolution(Enum):
Standard = 60
High = 1


class MetricUnit(Enum):
Seconds = "Seconds"
Microseconds = "Microseconds"
Expand Down Expand Up @@ -72,7 +82,9 @@ class MetricManager:
Raises
------
MetricUnitError
When metric metric isn't supported by CloudWatch
When metric unit isn't supported by CloudWatch
MetricResolutionError
When metric resolution isn't supported by CloudWatch
MetricValueError
When metric value isn't a number
SchemaValidationError
Expand All @@ -94,8 +106,16 @@ def __init__(
self.metadata_set = metadata_set if metadata_set is not None else {}
self._metric_units = [unit.value for unit in MetricUnit]
self._metric_unit_options = list(MetricUnit.__members__)
self._metric_resolutions = [resolution.value for resolution in MetricResolution]
self._metric_resolution_options = list(MetricResolution.__members__)

def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> None:
def add_metric(
self,
name: str,
unit: Union[MetricUnit, str],
value: float,
resolution: Union[MetricResolution, int] = 60,
) -> None:
"""Adds given metric

Example
Expand All @@ -108,6 +128,10 @@ def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> N

metric.add_metric(name="BookingConfirmation", unit="Count", value=1)

**Add given metric with MetricResolution non default value**

metric.add_metric(name="BookingConfirmation", unit="Count", value=1, resolution=MetricResolution.High)

Parameters
----------
name : str
Expand All @@ -116,18 +140,24 @@ def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> N
`aws_lambda_powertools.helper.models.MetricUnit`
value : float
Metric value
resolution : Union[MetricResolution, int]
`aws_lambda_powertools.helper.models.MetricResolution`

Raises
------
MetricUnitError
When metric unit is not supported by CloudWatch
MetricResolutionError
When metric resolution is not supported by CloudWatch
"""
if not isinstance(value, numbers.Number):
raise MetricValueError(f"{value} is not a valid number")

unit = self._extract_metric_unit_value(unit=unit)
resolution = self._extract_metric_resolution_value(resolution=resolution)
metric: Dict = self.metric_set.get(name, defaultdict(list))
metric["Unit"] = unit
metric["StorageResolution"] = resolution
metric["Value"].append(float(value))
logger.debug(f"Adding metric: {name} with {metric}")
self.metric_set[name] = metric
Expand Down Expand Up @@ -194,15 +224,20 @@ def serialize_metric_set(

logger.debug({"details": "Serializing metrics", "metrics": metrics, "dimensions": dimensions})

metric_names_and_units: List[Dict[str, str]] = [] # [ { "Name": "metric_name", "Unit": "Count" } ]
metric_names_and_units_and_resolution: List[
Dict[str, Union[str, int]]
] = [] # [ { "Name": "metric_name", "Unit": "Count", "StorageResolution": 60 } ]
metric_names_and_values: Dict[str, float] = {} # { "metric_name": 1.0 }

for metric_name in metrics:
metric: dict = metrics[metric_name]
metric_value: int = metric.get("Value", 0)
metric_unit: str = metric.get("Unit", "")
metric_resolution: int = metric.get("StorageResolution", 60)

metric_names_and_units.append({"Name": metric_name, "Unit": metric_unit})
metric_names_and_units_and_resolution.append(
{"Name": metric_name, "Unit": metric_unit, "StorageResolution": metric_resolution}
)
metric_names_and_values.update({metric_name: metric_value})

return {
Expand All @@ -212,7 +247,7 @@ def serialize_metric_set(
{
"Namespace": self.namespace, # "test_namespace"
"Dimensions": [list(dimensions.keys())], # [ "service" ]
"Metrics": metric_names_and_units,
"Metrics": metric_names_and_units_and_resolution,
}
],
},
Expand Down Expand Up @@ -358,6 +393,39 @@ def decorate(event, context):

return decorate

def _extract_metric_resolution_value(self, resolution: Union[int, MetricResolution]) -> int:
"""Return metric value from metric unit whether that's str or MetricResolution enum

Parameters
----------
unit : Union[int, MetricResolution]
Metric resolution

Returns
-------
int
Metric resolution value must be 1 or 60

Raises
------
MetricResolutionError
When metric resolution is not supported by CloudWatch
"""

if isinstance(resolution, int):
if resolution in self._metric_resolution_options:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: try renaming to _metric_resolution_valid_options or something along these lines so it's quicker to read code for the intent you have at hand (validate whether resolution falls within the valid options.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed

resolution = MetricResolution[str(resolution)].value

if resolution not in self._metric_resolutions:
raise MetricResolutionError(
f"Invalid metric resolution '{resolution}', expected either option: {self._metric_resolution_options}" # noqa: E501
)

if isinstance(resolution, MetricResolution):
resolution = resolution.value

return resolution

def _extract_metric_unit_value(self, unit: Union[str, MetricUnit]) -> str:
"""Return metric value from metric unit whether that's str or MetricUnit enum

Expand Down Expand Up @@ -429,10 +497,10 @@ class SingleMetric(MetricManager):
**Creates cold start metric with function_version as dimension**

import json
from aws_lambda_powertools.metrics import single_metric, MetricUnit
from aws_lambda_powertools.metrics import single_metric, MetricUnit, MetricResolution
metric = single_metric(namespace="ServerlessAirline")

metric.add_metric(name="ColdStart", unit=MetricUnit.Count, value=1)
metric.add_metric(name="ColdStart", unit=MetricUnit.Count, value=1, resolution=MetricResolution.Standard)
metric.add_dimension(name="function_version", value=47)

print(json.dumps(metric.serialize_metric_set(), indent=4))
Expand All @@ -443,7 +511,13 @@ class SingleMetric(MetricManager):
Inherits from `aws_lambda_powertools.metrics.base.MetricManager`
"""

def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> None:
def add_metric(
self,
name: str,
unit: Union[MetricUnit, str],
value: float,
resolution: Union[MetricResolution, int] = 60,
) -> None:
"""Method to prevent more than one metric being created

Parameters
Expand All @@ -454,18 +528,21 @@ def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> N
Metric unit (e.g. "Seconds", MetricUnit.Seconds)
value : float
Metric value
resolution : MetricResolution
Metric resolution (e.g. 60, MetricResolution.Standard)
"""
if len(self.metric_set) > 0:
logger.debug(f"Metric {name} already set, skipping...")
return
return super().add_metric(name, unit, value)
return super().add_metric(name, unit, value, resolution)


@contextmanager
def single_metric(
name: str,
unit: MetricUnit,
value: float,
resolution: Union[MetricResolution, int] = 60,
namespace: Optional[str] = None,
default_dimensions: Optional[Dict[str, str]] = None,
) -> Generator[SingleMetric, None, None]:
Expand All @@ -477,8 +554,9 @@ def single_metric(

from aws_lambda_powertools import single_metric
from aws_lambda_powertools.metrics import MetricUnit
from aws_lambda_powertools.metrics import MetricResolution

with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1, namespace="ServerlessAirline") as metric:
with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1, resolution=MetricResolution.Standard, namespace="ServerlessAirline") as metric: # noqa E501
metric.add_dimension(name="function_version", value="47")

**Same as above but set namespace using environment variable**
Expand All @@ -487,8 +565,9 @@ def single_metric(

from aws_lambda_powertools import single_metric
from aws_lambda_powertools.metrics import MetricUnit
from aws_lambda_powertools.metrics import MetricResolution

with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1) as metric:
with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1, resolution=MetricResolution.Standard) as metric: # noqa E501
metric.add_dimension(name="function_version", value="47")

Parameters
Expand All @@ -497,6 +576,8 @@ def single_metric(
Metric name
unit : MetricUnit
`aws_lambda_powertools.helper.models.MetricUnit`
resolution : MetricResolution
`aws_lambda_powertools.helper.models.MetricResolution`
value : float
Metric value
namespace: str
Expand All @@ -511,6 +592,8 @@ def single_metric(
------
MetricUnitError
When metric metric isn't supported by CloudWatch
MetricResolutionError
When metric resolution isn't supported by CloudWatch
MetricValueError
When metric value isn't a number
SchemaValidationError
Expand All @@ -519,7 +602,7 @@ def single_metric(
metric_set: Optional[Dict] = None
try:
metric: SingleMetric = SingleMetric(namespace=namespace)
metric.add_metric(name=name, unit=unit, value=value)
metric.add_metric(name=name, unit=unit, value=value, resolution=resolution)

if default_dimensions:
for dim_name, dim_value in default_dimensions.items():
Expand Down
6 changes: 6 additions & 0 deletions aws_lambda_powertools/metrics/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ class MetricUnitError(Exception):
pass


class MetricResolutionError(Exception):
"""When metric resolution is not supported by CloudWatch"""

pass


class SchemaValidationError(Exception):
"""When serialization fail schema validation"""

Expand Down
4 changes: 3 additions & 1 deletion aws_lambda_powertools/metrics/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ def lambda_handler():
Raises
------
MetricUnitError
When metric metric isn't supported by CloudWatch
When metric unit isn't supported by CloudWatch
MetricResolutionError
When metric resolution isn't supported by CloudWatch
MetricValueError
When metric value isn't a number
SchemaValidationError
Expand Down
13 changes: 13 additions & 0 deletions docs/core/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ You can create metrics using `add_metric`, and you can create dimensions for all
???+ warning "Warning: Do not create metrics or dimensions outside the handler"
Metrics or dimensions added in the global scope will only be added during cold start. Disregard if you that's the intended behavior.

### Adding high-resolution metrics

You can create [high-resolution metrics](https://aws.amazon.com/pt/about-aws/whats-new/2023/02/amazon-cloudwatch-high-resolution-metric-extraction-structured-logs/) passing `resolution` parameter to `add_metric`.

=== "add_high_resolution_metrics.py"

```python hl_lines="14-15"
--8<-- "examples/metrics/src/add_high_resolution_metric.py"
```

???+ tip "Tip: Autocomplete Metric Resolutions"
`MetricResolution` enum facilitates finding a supported metric resolution by CloudWatch. Alternatively, you can pass the values 1 or 60 (must be one of them) as an integer _e.g. `resolution=1`_.

### Adding multi-value metrics

You can call `add_metric()` with the same metric name multiple times. The values will be grouped together in a list.
Expand Down
10 changes: 10 additions & 0 deletions examples/metrics/src/add_high_resolution_metric.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from aws_lambda_powertools import Metrics
from aws_lambda_powertools.metrics import MetricResolution, MetricUnit
from aws_lambda_powertools.utilities.typing import LambdaContext

metrics = Metrics()


@metrics.log_metrics # ensures metrics are flushed upon request completion/failure
def lambda_handler(event: dict, context: LambdaContext):
metrics.add_metric(name="SuccessfulBooking", unit=MetricUnit.Count, value=1, resolution=MetricResolution.High)
Loading