Skip to content

Commit a23f0bf

Browse files
feat(metrics) - add support for high resolution metrics (#1915)
Co-authored-by: heitorlessa <[email protected]>
1 parent 8689708 commit a23f0bf

File tree

8 files changed

+252
-20
lines changed

8 files changed

+252
-20
lines changed

Diff for: aws_lambda_powertools/metrics/__init__.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
"""CloudWatch Embedded Metric Format utility
22
"""
3-
from .base import MetricUnit
4-
from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError
3+
from .base import MetricResolution, MetricUnit
4+
from .exceptions import (
5+
MetricResolutionError,
6+
MetricUnitError,
7+
MetricValueError,
8+
SchemaValidationError,
9+
)
510
from .metric import single_metric
611
from .metrics import EphemeralMetrics, Metrics
712

@@ -11,6 +16,8 @@
1116
"single_metric",
1217
"MetricUnit",
1318
"MetricUnitError",
19+
"MetricResolution",
20+
"MetricResolutionError",
1421
"SchemaValidationError",
1522
"MetricValueError",
1623
]

Diff for: aws_lambda_powertools/metrics/base.py

+102-16
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212

1313
from ..shared import constants
1414
from ..shared.functions import resolve_env_var_choice
15-
from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError
15+
from .exceptions import (
16+
MetricResolutionError,
17+
MetricUnitError,
18+
MetricValueError,
19+
SchemaValidationError,
20+
)
21+
from .types import MetricNameUnitResolution
1622

1723
logger = logging.getLogger(__name__)
1824

@@ -22,6 +28,11 @@
2228
is_cold_start = True
2329

2430

31+
class MetricResolution(Enum):
32+
Standard = 60
33+
High = 1
34+
35+
2536
class MetricUnit(Enum):
2637
Seconds = "Seconds"
2738
Microseconds = "Microseconds"
@@ -72,7 +83,9 @@ class MetricManager:
7283
Raises
7384
------
7485
MetricUnitError
75-
When metric metric isn't supported by CloudWatch
86+
When metric unit isn't supported by CloudWatch
87+
MetricResolutionError
88+
When metric resolution isn't supported by CloudWatch
7689
MetricValueError
7790
When metric value isn't a number
7891
SchemaValidationError
@@ -93,9 +106,16 @@ def __init__(
93106
self.service = resolve_env_var_choice(choice=service, env=os.getenv(constants.SERVICE_NAME_ENV))
94107
self.metadata_set = metadata_set if metadata_set is not None else {}
95108
self._metric_units = [unit.value for unit in MetricUnit]
96-
self._metric_unit_options = list(MetricUnit.__members__)
109+
self._metric_unit_valid_options = list(MetricUnit.__members__)
110+
self._metric_resolutions = [resolution.value for resolution in MetricResolution]
97111

98-
def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> None:
112+
def add_metric(
113+
self,
114+
name: str,
115+
unit: Union[MetricUnit, str],
116+
value: float,
117+
resolution: Union[MetricResolution, int] = 60,
118+
) -> None:
99119
"""Adds given metric
100120
101121
Example
@@ -108,6 +128,10 @@ def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> N
108128
109129
metric.add_metric(name="BookingConfirmation", unit="Count", value=1)
110130
131+
**Add given metric with MetricResolution non default value**
132+
133+
metric.add_metric(name="BookingConfirmation", unit="Count", value=1, resolution=MetricResolution.High)
134+
111135
Parameters
112136
----------
113137
name : str
@@ -116,18 +140,24 @@ def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> N
116140
`aws_lambda_powertools.helper.models.MetricUnit`
117141
value : float
118142
Metric value
143+
resolution : Union[MetricResolution, int]
144+
`aws_lambda_powertools.helper.models.MetricResolution`
119145
120146
Raises
121147
------
122148
MetricUnitError
123149
When metric unit is not supported by CloudWatch
150+
MetricResolutionError
151+
When metric resolution is not supported by CloudWatch
124152
"""
125153
if not isinstance(value, numbers.Number):
126154
raise MetricValueError(f"{value} is not a valid number")
127155

128156
unit = self._extract_metric_unit_value(unit=unit)
157+
resolution = self._extract_metric_resolution_value(resolution=resolution)
129158
metric: Dict = self.metric_set.get(name, defaultdict(list))
130159
metric["Unit"] = unit
160+
metric["StorageResolution"] = resolution
131161
metric["Value"].append(float(value))
132162
logger.debug(f"Adding metric: {name} with {metric}")
133163
self.metric_set[name] = metric
@@ -194,15 +224,28 @@ def serialize_metric_set(
194224

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

197-
metric_names_and_units: List[Dict[str, str]] = [] # [ { "Name": "metric_name", "Unit": "Count" } ]
227+
# For standard resolution metrics, don't add StorageResolution field to avoid unnecessary ingestion of data into cloudwatch # noqa E501
228+
# Example: [ { "Name": "metric_name", "Unit": "Count"} ] # noqa E800
229+
#
230+
# In case using high-resolution metrics, add StorageResolution field
231+
# Example: [ { "Name": "metric_name", "Unit": "Count", "StorageResolution": 1 } ] # noqa E800
232+
metric_definition: List[MetricNameUnitResolution] = []
198233
metric_names_and_values: Dict[str, float] = {} # { "metric_name": 1.0 }
199234

200235
for metric_name in metrics:
201236
metric: dict = metrics[metric_name]
202237
metric_value: int = metric.get("Value", 0)
203238
metric_unit: str = metric.get("Unit", "")
239+
metric_resolution: int = metric.get("StorageResolution", 60)
240+
241+
metric_definition_data: MetricNameUnitResolution = {"Name": metric_name, "Unit": metric_unit}
242+
243+
# high-resolution metrics
244+
if metric_resolution == 1:
245+
metric_definition_data["StorageResolution"] = metric_resolution
246+
247+
metric_definition.append(metric_definition_data)
204248

205-
metric_names_and_units.append({"Name": metric_name, "Unit": metric_unit})
206249
metric_names_and_values.update({metric_name: metric_value})
207250

208251
return {
@@ -212,7 +255,7 @@ def serialize_metric_set(
212255
{
213256
"Namespace": self.namespace, # "test_namespace"
214257
"Dimensions": [list(dimensions.keys())], # [ "service" ]
215-
"Metrics": metric_names_and_units,
258+
"Metrics": metric_definition,
216259
}
217260
],
218261
},
@@ -358,6 +401,34 @@ def decorate(event, context):
358401

359402
return decorate
360403

404+
def _extract_metric_resolution_value(self, resolution: Union[int, MetricResolution]) -> int:
405+
"""Return metric value from metric unit whether that's str or MetricResolution enum
406+
407+
Parameters
408+
----------
409+
unit : Union[int, MetricResolution]
410+
Metric resolution
411+
412+
Returns
413+
-------
414+
int
415+
Metric resolution value must be 1 or 60
416+
417+
Raises
418+
------
419+
MetricResolutionError
420+
When metric resolution is not supported by CloudWatch
421+
"""
422+
if isinstance(resolution, MetricResolution):
423+
return resolution.value
424+
425+
if isinstance(resolution, int) and resolution in self._metric_resolutions:
426+
return resolution
427+
428+
raise MetricResolutionError(
429+
f"Invalid metric resolution '{resolution}', expected either option: {self._metric_resolutions}" # noqa: E501
430+
)
431+
361432
def _extract_metric_unit_value(self, unit: Union[str, MetricUnit]) -> str:
362433
"""Return metric value from metric unit whether that's str or MetricUnit enum
363434
@@ -378,12 +449,12 @@ def _extract_metric_unit_value(self, unit: Union[str, MetricUnit]) -> str:
378449
"""
379450

380451
if isinstance(unit, str):
381-
if unit in self._metric_unit_options:
452+
if unit in self._metric_unit_valid_options:
382453
unit = MetricUnit[unit].value
383454

384455
if unit not in self._metric_units:
385456
raise MetricUnitError(
386-
f"Invalid metric unit '{unit}', expected either option: {self._metric_unit_options}"
457+
f"Invalid metric unit '{unit}', expected either option: {self._metric_unit_valid_options}"
387458
)
388459

389460
if isinstance(unit, MetricUnit):
@@ -429,10 +500,10 @@ class SingleMetric(MetricManager):
429500
**Creates cold start metric with function_version as dimension**
430501
431502
import json
432-
from aws_lambda_powertools.metrics import single_metric, MetricUnit
503+
from aws_lambda_powertools.metrics import single_metric, MetricUnit, MetricResolution
433504
metric = single_metric(namespace="ServerlessAirline")
434505
435-
metric.add_metric(name="ColdStart", unit=MetricUnit.Count, value=1)
506+
metric.add_metric(name="ColdStart", unit=MetricUnit.Count, value=1, resolution=MetricResolution.Standard)
436507
metric.add_dimension(name="function_version", value=47)
437508
438509
print(json.dumps(metric.serialize_metric_set(), indent=4))
@@ -443,7 +514,13 @@ class SingleMetric(MetricManager):
443514
Inherits from `aws_lambda_powertools.metrics.base.MetricManager`
444515
"""
445516

446-
def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> None:
517+
def add_metric(
518+
self,
519+
name: str,
520+
unit: Union[MetricUnit, str],
521+
value: float,
522+
resolution: Union[MetricResolution, int] = 60,
523+
) -> None:
447524
"""Method to prevent more than one metric being created
448525
449526
Parameters
@@ -454,18 +531,21 @@ def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> N
454531
Metric unit (e.g. "Seconds", MetricUnit.Seconds)
455532
value : float
456533
Metric value
534+
resolution : MetricResolution
535+
Metric resolution (e.g. 60, MetricResolution.Standard)
457536
"""
458537
if len(self.metric_set) > 0:
459538
logger.debug(f"Metric {name} already set, skipping...")
460539
return
461-
return super().add_metric(name, unit, value)
540+
return super().add_metric(name, unit, value, resolution)
462541

463542

464543
@contextmanager
465544
def single_metric(
466545
name: str,
467546
unit: MetricUnit,
468547
value: float,
548+
resolution: Union[MetricResolution, int] = 60,
469549
namespace: Optional[str] = None,
470550
default_dimensions: Optional[Dict[str, str]] = None,
471551
) -> Generator[SingleMetric, None, None]:
@@ -477,8 +557,9 @@ def single_metric(
477557
478558
from aws_lambda_powertools import single_metric
479559
from aws_lambda_powertools.metrics import MetricUnit
560+
from aws_lambda_powertools.metrics import MetricResolution
480561
481-
with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1, namespace="ServerlessAirline") as metric:
562+
with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1, resolution=MetricResolution.Standard, namespace="ServerlessAirline") as metric: # noqa E501
482563
metric.add_dimension(name="function_version", value="47")
483564
484565
**Same as above but set namespace using environment variable**
@@ -487,8 +568,9 @@ def single_metric(
487568
488569
from aws_lambda_powertools import single_metric
489570
from aws_lambda_powertools.metrics import MetricUnit
571+
from aws_lambda_powertools.metrics import MetricResolution
490572
491-
with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1) as metric:
573+
with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1, resolution=MetricResolution.Standard) as metric: # noqa E501
492574
metric.add_dimension(name="function_version", value="47")
493575
494576
Parameters
@@ -497,6 +579,8 @@ def single_metric(
497579
Metric name
498580
unit : MetricUnit
499581
`aws_lambda_powertools.helper.models.MetricUnit`
582+
resolution : MetricResolution
583+
`aws_lambda_powertools.helper.models.MetricResolution`
500584
value : float
501585
Metric value
502586
namespace: str
@@ -511,6 +595,8 @@ def single_metric(
511595
------
512596
MetricUnitError
513597
When metric metric isn't supported by CloudWatch
598+
MetricResolutionError
599+
When metric resolution isn't supported by CloudWatch
514600
MetricValueError
515601
When metric value isn't a number
516602
SchemaValidationError
@@ -519,7 +605,7 @@ def single_metric(
519605
metric_set: Optional[Dict] = None
520606
try:
521607
metric: SingleMetric = SingleMetric(namespace=namespace)
522-
metric.add_metric(name=name, unit=unit, value=value)
608+
metric.add_metric(name=name, unit=unit, value=value, resolution=resolution)
523609

524610
if default_dimensions:
525611
for dim_name, dim_value in default_dimensions.items():

Diff for: aws_lambda_powertools/metrics/exceptions.py

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ class MetricUnitError(Exception):
44
pass
55

66

7+
class MetricResolutionError(Exception):
8+
"""When metric resolution is not supported by CloudWatch"""
9+
10+
pass
11+
12+
713
class SchemaValidationError(Exception):
814
"""When serialization fail schema validation"""
915

Diff for: aws_lambda_powertools/metrics/metrics.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ def lambda_handler():
5050
Raises
5151
------
5252
MetricUnitError
53-
When metric metric isn't supported by CloudWatch
53+
When metric unit isn't supported by CloudWatch
54+
MetricResolutionError
55+
When metric resolution isn't supported by CloudWatch
5456
MetricValueError
5557
When metric value isn't a number
5658
SchemaValidationError

Diff for: aws_lambda_powertools/metrics/types.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from typing_extensions import NotRequired, TypedDict
2+
3+
4+
class MetricNameUnitResolution(TypedDict):
5+
Name: str
6+
Unit: str
7+
StorageResolution: NotRequired[int]

Diff for: docs/core/metrics.md

+19
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar
2020

2121
* **Namespace**. It's the highest level container that will group multiple metrics from multiple services for a given application, for example `ServerlessEcommerce`.
2222
* **Dimensions**. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example `ColdStart` metric by Payment `service`.
23+
* **Metric**. It's the name of the metric, for example: `SuccessfulBooking` or `UpdatedBooking`.
24+
* **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).
2326

2427
<figure>
2528
<img src="../../media/metrics_terminology.png" />
@@ -78,6 +81,22 @@ You can create metrics using `add_metric`, and you can create dimensions for all
7881
???+ warning "Warning: Do not create metrics or dimensions outside the handler"
7982
Metrics or dimensions added in the global scope will only be added during cold start. Disregard if you that's the intended behavior.
8083

84+
### Adding high-resolution metrics
85+
86+
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`.
87+
88+
???+ tip "High-resolution metrics - when is it useful?"
89+
High-resolution metrics are data with a granularity of one second and are very useful in several situations such as telemetry, time series, real-time incident management, and others.
90+
91+
=== "add_high_resolution_metrics.py"
92+
93+
```python hl_lines="10"
94+
--8<-- "examples/metrics/src/add_high_resolution_metric.py"
95+
```
96+
97+
???+ tip "Tip: Autocomplete Metric Resolutions"
98+
`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`_.
99+
81100
### Adding multi-value metrics
82101

83102
You can call `add_metric()` with the same metric name multiple times. The values will be grouped together in a list.

Diff for: examples/metrics/src/add_high_resolution_metric.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from aws_lambda_powertools import Metrics
2+
from aws_lambda_powertools.metrics import MetricResolution, MetricUnit
3+
from aws_lambda_powertools.utilities.typing import LambdaContext
4+
5+
metrics = Metrics()
6+
7+
8+
@metrics.log_metrics # ensures metrics are flushed upon request completion/failure
9+
def lambda_handler(event: dict, context: LambdaContext):
10+
metrics.add_metric(name="SuccessfulBooking", unit=MetricUnit.Count, value=1, resolution=MetricResolution.High)

0 commit comments

Comments
 (0)