Skip to content

Commit 4d6dc34

Browse files
authored
feat: add capture_cold_start_metric for log_metrics (#67)
* feat: add capture_cold_start_metric for log_metrics * docs: document capture_cold_start_metric
1 parent 481a15f commit 4d6dc34

File tree

3 files changed

+92
-3
lines changed

3 files changed

+92
-3
lines changed

Diff for: aws_lambda_powertools/metrics/metrics.py

+28-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
logger = logging.getLogger(__name__)
1010

11+
is_cold_start = True
12+
1113

1214
class Metrics(MetricManager):
1315
"""Metrics create an EMF object with up to 100 metrics
@@ -80,7 +82,7 @@ def clear_metrics(self):
8082
self.metric_set.clear()
8183
self.dimension_set.clear()
8284

83-
def log_metrics(self, lambda_handler: Callable[[Any, Any], Any] = None):
85+
def log_metrics(self, lambda_handler: Callable[[Any, Any], Any] = None, capture_cold_start_metric: bool = False):
8486
"""Decorator to serialize and publish metrics at the end of a function execution.
8587
8688
Be aware that the log_metrics **does call* the decorated function (e.g. lambda_handler).
@@ -107,10 +109,18 @@ def handler(event, context)
107109
Propagate error received
108110
"""
109111

112+
# If handler is None we've been called with parameters
113+
# Return a partial function with args filled
114+
if lambda_handler is None:
115+
logger.debug("Decorator called with parameters")
116+
return functools.partial(self.log_metrics, capture_cold_start_metric=capture_cold_start_metric)
117+
110118
@functools.wraps(lambda_handler)
111-
def decorate(*args, **kwargs):
119+
def decorate(event, context):
112120
try:
113-
response = lambda_handler(*args, **kwargs)
121+
response = lambda_handler(event, context)
122+
if capture_cold_start_metric:
123+
self.__add_cold_start_metric(context=context)
114124
finally:
115125
metrics = self.serialize_metric_set()
116126
self.clear_metrics()
@@ -120,3 +130,18 @@ def decorate(*args, **kwargs):
120130
return response
121131

122132
return decorate
133+
134+
def __add_cold_start_metric(self, context: Any):
135+
"""Add cold start metric and function_name dimension
136+
137+
Parameters
138+
----------
139+
context : Any
140+
Lambda context
141+
"""
142+
global is_cold_start
143+
if is_cold_start:
144+
logger.debug("Adding cold start metric and function_name dimension")
145+
self.add_metric(name="ColdStart", value=1, unit="Count")
146+
self.add_dimension(name="function_name", value=context.function_name)
147+
is_cold_start = False

Diff for: docs/content/core/metrics.mdx

+16
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,22 @@ print(json.dumps(your_metrics_object))
147147
# highlight-end
148148
```
149149

150+
## Capturing cold start metric
151+
152+
You can capture cold start metrics automatically with `log_metrics` via `capture_cold_start_metric` param.
153+
154+
```python:title=lambda_handler.py
155+
from aws_lambda_powertools.metrics import Metrics, MetricUnit
156+
157+
metrics = Metrics(service="ExampleService")
158+
159+
@metrics.log_metrics(capture_cold_start_metric=True) # highlight-line
160+
def lambda_handler(evt, ctx):
161+
...
162+
```
163+
164+
If it's a cold start, this feature will add a metric named `ColdStart` and a dimension named `function_name`.
165+
150166
## Testing your code
151167

152168
Use `POWERTOOLS_METRICS_NAMESPACE` and `POWERTOOLS_SERVICE_NAME` env vars when unit testing your code to ensure metric namespace and dimension objects are created, and your code doesn't fail validation.

Diff for: tests/functional/test_metrics.py

+48
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from collections import namedtuple
23
from typing import Any, Dict, List
34

45
import pytest
@@ -585,3 +586,50 @@ def test_namespace_var_precedence(monkeypatch, capsys, metric, dimension, namesp
585586

586587
# THEN namespace should match the explicitly passed variable and not the env var
587588
assert expected["_aws"] == output["_aws"]
589+
590+
591+
def test_emit_cold_start_metric(capsys, namespace):
592+
# GIVEN Metrics is initialized
593+
my_metrics = Metrics()
594+
my_metrics.add_namespace(**namespace)
595+
596+
# WHEN log_metrics is used with capture_cold_start_metric
597+
@my_metrics.log_metrics(capture_cold_start_metric=True)
598+
def lambda_handler(evt, context):
599+
return True
600+
601+
LambdaContext = namedtuple("LambdaContext", "function_name")
602+
lambda_handler({}, LambdaContext("example_fn"))
603+
604+
output = json.loads(capsys.readouterr().out.strip())
605+
606+
# THEN ColdStart metric and function_name dimension should be logged
607+
assert output["ColdStart"] == 1
608+
assert output["function_name"] == "example_fn"
609+
610+
611+
def test_emit_cold_start_metric_only_once(capsys, namespace, dimension, metric):
612+
# GIVEN Metrics is initialized
613+
my_metrics = Metrics()
614+
my_metrics.add_namespace(**namespace)
615+
616+
# WHEN log_metrics is used with capture_cold_start_metric
617+
# and handler is called more than once
618+
@my_metrics.log_metrics(capture_cold_start_metric=True)
619+
def lambda_handler(evt, context):
620+
my_metrics.add_metric(**metric)
621+
my_metrics.add_dimension(**dimension)
622+
623+
LambdaContext = namedtuple("LambdaContext", "function_name")
624+
lambda_handler({}, LambdaContext("example_fn"))
625+
capsys.readouterr().out.strip()
626+
627+
# THEN ColdStart metric and function_name dimension should be logged
628+
# only once
629+
lambda_handler({}, LambdaContext("example_fn"))
630+
631+
output = json.loads(capsys.readouterr().out.strip())
632+
633+
assert "ColdStart" not in output
634+
635+
assert "function_name" not in output

0 commit comments

Comments
 (0)