Skip to content

Commit 4413844

Browse files
committed
feat(metrics): add cold start test
1 parent 54e9550 commit 4413844

File tree

4 files changed

+131
-17
lines changed

4 files changed

+131
-17
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from aws_lambda_powertools import Metrics
2+
3+
my_metrics = Metrics()
4+
5+
6+
@my_metrics.log_metrics(capture_cold_start_metric=True)
7+
def lambda_handler(event, context):
8+
# Maintenance: create a public method to set these explicitly
9+
my_metrics.namespace = event.get("namespace")
10+
my_metrics.service = event.get("service")
11+
12+
return "success"

tests/e2e/metrics/infrastructure.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
class MetricsStack:
1717
def __init__(self, handlers_dir: Path) -> None:
1818
self.stack_name = f"test-metrics-{uuid4()}"
19-
self.handlers_dir = handlers_dir # hardcoded as we're not using a fixture yet
19+
self.handlers_dir = handlers_dir
2020
self.app = App()
2121
self.stack = Stack(self.app, self.stack_name)
2222
self.session = boto3.Session()
@@ -41,7 +41,7 @@ def create_functions(self):
4141
layers=[
4242
LayerVersion.from_layer_version_arn(
4343
self.stack,
44-
"lambda-powertools",
44+
f"{fn_name}-lambda-powertools",
4545
f"arn:aws:lambda:{self.region}:017000801446:layer:AWSLambdaPowertoolsPython:29",
4646
)
4747
],
@@ -57,7 +57,9 @@ def create_functions(self):
5757

5858
# CFN Outputs only support hyphen
5959
fn_name_camel_case = fn_name.title().replace("_", "") # basic_handler -> BasicHandler
60-
CfnOutput(self.stack, f"{fn_name_camel_case}Arn", value=function_python.function_arn)
60+
self._add_resource_output(
61+
name=fn_name_camel_case, value=function_python.function_name, arn_value=function_python.function_arn
62+
)
6163

6264
def deploy(self) -> Dict[str, str]:
6365
"""Creates CloudFormation Stack and return stack outputs as dict
@@ -75,6 +77,9 @@ def deploy(self) -> Dict[str, str]:
7577
def delete(self):
7678
self.cf_client.delete_stack(StackName=self.stack_name)
7779

80+
def get_stack_outputs(self) -> Dict[str, str]:
81+
return self.stack_outputs
82+
7883
def _synthesize(self) -> Tuple[Dict, Path]:
7984
self.create_functions()
8085
cloud_assembly = self.app.synth()
@@ -103,5 +108,19 @@ def _deploy_stack(self, stack_name: str, template: Dict) -> Dict[str, str]:
103108

104109
return self.stack_outputs
105110

106-
def get_stack_outputs(self) -> Dict[str, str]:
107-
return self.stack_outputs
111+
def _add_resource_output(self, name: str, value: str, arn_value: str):
112+
"""Add both resource value and ARN as Outputs to facilitate tests.
113+
114+
This will create two outputs: {Name} and {Name}Arn
115+
116+
Parameters
117+
----------
118+
name : str
119+
CloudFormation Output Key
120+
value : str
121+
CloudFormation Output Value
122+
arn_value : str
123+
CloudFormation Output Value for ARN
124+
"""
125+
CfnOutput(self.stack, f"{name}", value=value)
126+
CfnOutput(self.stack, f"{name}Arn", value=arn_value)

tests/e2e/metrics/test_metrics.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
from tests.e2e.metrics.infrastructure import MetricsStack
77
from tests.e2e.utils import helpers
88

9-
METRIC_NAMESPACE = "powertools-e2e-metric"
10-
119

1210
@pytest.fixture
1311
def infra_outputs(infrastructure: MetricsStack):
@@ -19,6 +17,19 @@ def basic_handler_fn(infra_outputs: dict) -> str:
1917
return infra_outputs.get("BasicHandlerArn", "")
2018

2119

20+
@pytest.fixture
21+
def cold_start_fn(infra_outputs: dict) -> str:
22+
return infra_outputs.get("ColdStart", "")
23+
24+
25+
@pytest.fixture
26+
def cold_start_fn_arn(infra_outputs: dict) -> str:
27+
return infra_outputs.get("ColdStartArn", "")
28+
29+
30+
METRIC_NAMESPACE = "powertools-e2e-metric"
31+
32+
2233
def test_basic_lambda_metric_is_visible(basic_handler_fn):
2334
# GIVEN
2435
metric_name = helpers.build_metric_name()
@@ -42,6 +53,29 @@ def test_basic_lambda_metric_is_visible(basic_handler_fn):
4253
assert metric_data and metric_data[0] == 3.0
4354

4455

56+
def test_cold_start_metric(cold_start_fn_arn: str, cold_start_fn: str):
57+
# GIVEN
58+
metric_name = "ColdStart"
59+
service = helpers.build_service_name()
60+
dimensions = helpers.build_add_dimensions_input(function_name=cold_start_fn, service=service)
61+
62+
# WHEN
63+
event = json.dumps({"service": service, "namespace": METRIC_NAMESPACE})
64+
_, execution_time = helpers.trigger_lambda(lambda_arn=cold_start_fn_arn, payload=event)
65+
66+
metrics = helpers.get_metrics(
67+
start_date=execution_time,
68+
end_date=execution_time + datetime.timedelta(minutes=2),
69+
namespace=METRIC_NAMESPACE,
70+
metric_name=metric_name,
71+
dimensions=dimensions,
72+
)
73+
74+
# THEN
75+
metric_data = metrics.get("Values", [])
76+
assert metric_data and metric_data[0] == 1.0
77+
78+
4579
# helpers: adjust retries and wait to be much smaller
4680
# helpers: make retry config adjustable
4781
# Infra: Add temporary Powertools Layer

tests/e2e/utils/helpers.py

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,21 @@ def get_metrics(
6868
namespace: str,
6969
start_date: datetime,
7070
metric_name: str,
71-
service_name: str,
71+
service_name: str = "",
72+
dimensions: Optional[List[DimensionTypeDef]] = None,
7273
cw_client: Optional[CloudWatchClient] = None,
7374
end_date: Optional[datetime] = None,
7475
period: int = 60,
7576
stat: str = "Sum",
7677
) -> MetricDataResultTypeDef:
7778
cw_client = cw_client or boto3.client("cloudwatch")
7879
metric_query = build_metric_query_data(
79-
namespace=namespace, metric_name=metric_name, service_name=service_name, period=period, stat=stat
80+
namespace=namespace,
81+
metric_name=metric_name,
82+
service_name=service_name,
83+
period=period,
84+
stat=stat,
85+
dimensions=dimensions,
8086
)
8187

8288
response = cw_client.get_metric_data(
@@ -156,27 +162,34 @@ def build_random_value(nbytes: int = 10) -> str:
156162
def build_metric_query_data(
157163
namespace: str,
158164
metric_name: str,
159-
service_name: str,
165+
service_name: str = "",
160166
period: int = 60,
161167
stat: str = "Sum",
162-
dimensions: Optional[DimensionTypeDef] = None,
168+
dimensions: Optional[List[DimensionTypeDef]] = None,
163169
) -> List[MetricDataQueryTypeDef]:
164-
metric_dimensions: List[DimensionTypeDef] = [{"Name": "service", "Value": service_name}]
165-
if dimensions is not None:
166-
metric_dimensions.append(dimensions)
170+
dimensions = dimensions or []
167171

168-
return [
172+
# Maintenance: get rid of service_name param to avoid future mistakes
173+
if service_name:
174+
dimensions.append({"Name": "service", "Value": service_name})
175+
176+
data_query: List[MetricDataQueryTypeDef] = [
169177
{
170-
"Id": metric_name,
178+
"Id": metric_name.lower(),
171179
"MetricStat": {
172-
"Metric": {"Namespace": namespace, "MetricName": metric_name, "Dimensions": metric_dimensions},
180+
"Metric": {"Namespace": namespace, "MetricName": metric_name},
173181
"Period": period,
174182
"Stat": stat,
175183
},
176184
"ReturnData": True,
177185
}
178186
]
179187

188+
if dimensions:
189+
data_query[0]["MetricStat"]["Metric"]["Dimensions"] = dimensions
190+
191+
return data_query
192+
180193

181194
def build_add_metric_input(metric_name: str, value: float, unit: str = MetricUnit.Count.value) -> Dict:
182195
"""Create a metric input to be used with Metrics.add_metric()
@@ -220,3 +233,39 @@ def build_multiple_add_metric_input(
220233
List of metrics
221234
"""
222235
return [{"name": metric_name, "unit": unit, "value": value} for _ in range(quantity)]
236+
237+
238+
def build_add_dimension_input(name: str, value: str) -> DimensionTypeDef:
239+
"""Create a dimension input to be used with either Metrics.add_dimension()
240+
241+
Parameters
242+
----------
243+
name : str
244+
dimension name
245+
value : float
246+
dimension value
247+
248+
Returns
249+
-------
250+
Dict
251+
Metric dimension input
252+
"""
253+
return {"Name": name, "Value": value}
254+
255+
256+
def build_add_dimensions_input(**dimensions) -> List[DimensionTypeDef]:
257+
"""Create dimensions input to be used with either get_metrics or Metrics.add_dimension()
258+
259+
Parameters
260+
----------
261+
name : str
262+
dimension name
263+
value : float
264+
dimension value
265+
266+
Returns
267+
-------
268+
Dict
269+
Metric dimension input
270+
"""
271+
return [{"Name": name, "Value": value} for name, value in dimensions.items()]

0 commit comments

Comments
 (0)