Skip to content

Commit a25e3b1

Browse files
author
Tom McCarthy
authored
feat: emf multiple metric values (#167)
* feat: Update metrics to allow multiple values for a single metric * chore: update test comments * chore: fix type hint * chore: type hint update * chore: update CHANGELOG
1 parent 5ee14b1 commit a25e3b1

File tree

3 files changed

+80
-8
lines changed

3 files changed

+80
-8
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88

99
### Added
10+
- **Metrics**: Support adding multiple metric values to a metric name
1011
- **Utilities**: Add new `Validator` utility to validate inbound events and responses using JSON Schema
1112

1213
## [1.5.0] - 2020-09-04

Diff for: aws_lambda_powertools/metrics/base.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import numbers
55
import os
66
import pathlib
7+
from collections import defaultdict
78
from enum import Enum
89
from typing import Any, Dict, List, Union
910

@@ -93,7 +94,7 @@ def __init__(
9394
self._metric_unit_options = list(MetricUnit.__members__)
9495
self.metadata_set = self.metadata_set if metadata_set is not None else {}
9596

96-
def add_metric(self, name: str, unit: MetricUnit, value: Union[float, int]):
97+
def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float):
9798
"""Adds given metric
9899
99100
Example
@@ -110,9 +111,9 @@ def add_metric(self, name: str, unit: MetricUnit, value: Union[float, int]):
110111
----------
111112
name : str
112113
Metric name
113-
unit : MetricUnit
114+
unit : Union[MetricUnit, str]
114115
`aws_lambda_powertools.helper.models.MetricUnit`
115-
value : Union[float, int]
116+
value : float
116117
Metric value
117118
118119
Raises
@@ -124,7 +125,9 @@ def add_metric(self, name: str, unit: MetricUnit, value: Union[float, int]):
124125
raise MetricValueError(f"{value} is not a valid number")
125126

126127
unit = self.__extract_metric_unit_value(unit=unit)
127-
metric = {"Unit": unit, "Value": float(value)}
128+
metric = self.metric_set.get(name, defaultdict(list))
129+
metric["Unit"] = unit
130+
metric["Value"].append(float(value))
128131
logger.debug(f"Adding metric: {name} with {metric}")
129132
self.metric_set[name] = metric
130133

Diff for: tests/functional/test_metrics.py

+72-4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ def metrics() -> List[Dict[str, str]]:
3232
]
3333

3434

35+
@pytest.fixture
36+
def metrics_same_name() -> List[Dict[str, str]]:
37+
return [
38+
{"name": "metric_one", "unit": MetricUnit.Count, "value": 1},
39+
{"name": "metric_one", "unit": MetricUnit.Count, "value": 5},
40+
]
41+
42+
3543
@pytest.fixture
3644
def dimension() -> Dict[str, str]:
3745
return {"name": "test_dimension", "value": "test"}
@@ -485,7 +493,7 @@ def lambda_handler(evt, context):
485493
output = capture_metrics_output(capsys)
486494

487495
# THEN ColdStart metric and function_name dimension should be logged
488-
assert output["ColdStart"] == 1
496+
assert output["ColdStart"] == [1.0]
489497
assert output["function_name"] == "example_fn"
490498

491499

@@ -607,7 +615,7 @@ def lambda_handler(evt, ctx):
607615

608616
def test_serialize_metric_set_metric_definition(metric, dimension, namespace, service, metadata):
609617
expected_metric_definition = {
610-
"single_metric": 1.0,
618+
"single_metric": [1.0],
611619
"_aws": {
612620
"Timestamp": 1592237875494,
613621
"CloudWatchMetrics": [
@@ -655,7 +663,7 @@ def lambda_handler(evt, context):
655663

656664
# THEN ColdStart metric and function_name dimension should be logged
657665
# in a separate EMF blob than the application metrics
658-
assert cold_start_blob["ColdStart"] == 1
666+
assert cold_start_blob["ColdStart"] == [1.0]
659667
assert cold_start_blob["function_name"] == "example_fn"
660668
assert cold_start_blob["service"] == service
661669

@@ -669,5 +677,65 @@ def lambda_handler(evt, context):
669677

670678
# and that application metrics are recorded as normal
671679
assert custom_metrics_blob["service"] == service
672-
assert custom_metrics_blob["single_metric"] == metric["value"]
680+
assert custom_metrics_blob["single_metric"] == [float(metric["value"])]
673681
assert custom_metrics_blob["test_dimension"] == dimension["value"]
682+
683+
684+
def test_log_multiple_metrics(capsys, metrics_same_name, dimensions, namespace):
685+
# GIVEN Metrics is initialized
686+
my_metrics = Metrics(namespace=namespace)
687+
688+
for dimension in dimensions:
689+
my_metrics.add_dimension(**dimension)
690+
691+
# WHEN we utilize log_metrics to serialize
692+
# and flush multiple metrics with the same name at the end of a function execution
693+
@my_metrics.log_metrics
694+
def lambda_handler(evt, ctx):
695+
for metric in metrics_same_name:
696+
my_metrics.add_metric(**metric)
697+
698+
lambda_handler({}, {})
699+
output = capture_metrics_output(capsys)
700+
expected = serialize_metrics(metrics=metrics_same_name, dimensions=dimensions, namespace=namespace)
701+
702+
# THEN we should have no exceptions
703+
# and a valid EMF object should be flushed correctly
704+
remove_timestamp(metrics=[output, expected])
705+
assert expected == output
706+
707+
708+
def test_serialize_metric_set_metric_definition_multiple_values(
709+
metrics_same_name, dimension, namespace, service, metadata
710+
):
711+
expected_metric_definition = {
712+
"metric_one": [1.0, 5.0],
713+
"_aws": {
714+
"Timestamp": 1592237875494,
715+
"CloudWatchMetrics": [
716+
{
717+
"Namespace": "test_namespace",
718+
"Dimensions": [["test_dimension", "service"]],
719+
"Metrics": [{"Name": "metric_one", "Unit": "Count"}],
720+
}
721+
],
722+
},
723+
"service": "test_service",
724+
"username": "test",
725+
"test_dimension": "test",
726+
}
727+
728+
# GIVEN Metrics is initialized and multiple metrics are added with the same name
729+
my_metrics = Metrics(service=service, namespace=namespace)
730+
for metric in metrics_same_name:
731+
my_metrics.add_metric(**metric)
732+
my_metrics.add_dimension(**dimension)
733+
my_metrics.add_metadata(**metadata)
734+
735+
# WHEN metrics are serialized manually
736+
metric_definition_output = my_metrics.serialize_metric_set()
737+
738+
# THEN we should emit a valid embedded metric definition object
739+
assert "Timestamp" in metric_definition_output["_aws"]
740+
remove_timestamp(metrics=[metric_definition_output, expected_metric_definition])
741+
assert metric_definition_output == expected_metric_definition

0 commit comments

Comments
 (0)