Skip to content

Commit 33e0225

Browse files
Merging from develop
2 parents f0561cc + 016aba9 commit 33e0225

39 files changed

+851
-425
lines changed

.github/actionlint.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
self-hosted-runner:
22
labels:
3-
- aws-powertools_ubuntu-latest_4-core
43
- aws-powertools_ubuntu-latest_8-core

.github/workflows/dependency-review.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ jobs:
1919
- name: 'Checkout Repository'
2020
uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
2121
- name: 'Dependency Review'
22-
uses: actions/dependency-review-action@733dd5d4a5203f238c33806593ec0f5fc5343d8c # v4.2.4
22+
uses: actions/dependency-review-action@5bbc3ba658137598168acb2ab73b21c432dd411b # v4.2.5

.github/workflows/publish_v2_layer.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ jobs:
105105
with:
106106
node-version: "16.12"
107107
- name: Setup python
108-
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
108+
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
109109
with:
110110
python-version: "3.12"
111111
cache: "pip"

.github/workflows/quality_check.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
- name: Install poetry
5555
run: pipx install poetry
5656
- name: Set up Python ${{ matrix.python-version }}
57-
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
57+
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
5858
with:
5959
python-version: ${{ matrix.python-version }}
6060
cache: "poetry"
@@ -71,7 +71,7 @@ jobs:
7171
- name: Complexity baseline
7272
run: make complexity-baseline
7373
- name: Upload coverage to Codecov
74-
uses: codecov/codecov-action@54bcd8715eee62d40e33596ef5e8f0f48dbbccab # 4.1.0
74+
uses: codecov/codecov-action@c16abc29c95fcf9174b58eb7e1abf4c866893bc8 # 4.1.1
7575
with:
7676
file: ./coverage.xml
7777
env_vars: PYTHON

.github/workflows/quality_check_pydanticv2.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
- name: Install poetry
5454
run: pipx install poetry
5555
- name: Set up Python ${{ matrix.python-version }}
56-
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
56+
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
5757
with:
5858
python-version: ${{ matrix.python-version }}
5959
cache: "poetry"

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ jobs:
131131
- name: Install poetry
132132
run: pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0
133133
- name: Set up Python
134-
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
134+
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
135135
with:
136136
python-version: "3.12"
137137
cache: "poetry"
@@ -169,7 +169,7 @@ jobs:
169169
- name: Install poetry
170170
run: pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0
171171
- name: Set up Python
172-
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
172+
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
173173
with:
174174
python-version: "3.12"
175175
cache: "poetry"

.github/workflows/reusable_deploy_v2_layer_stack.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ jobs:
162162
with:
163163
node-version: "16.12"
164164
- name: Setup python
165-
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
165+
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
166166
with:
167167
python-version: "3.12"
168168
cache: "pip"

.github/workflows/reusable_publish_docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
- name: Install poetry
5252
run: pipx install poetry
5353
- name: Set up Python
54-
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
54+
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
5555
with:
5656
python-version: "3.12"
5757
cache: "poetry"

.github/workflows/run-e2e-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
- name: Install poetry
5656
run: pipx install poetry
5757
- name: "Use Python"
58-
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
58+
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
5959
with:
6060
python-version: ${{ matrix.version }}
6161
architecture: "x64"

CHANGELOG.md

Lines changed: 63 additions & 26 deletions
Large diffs are not rendered by default.

aws_lambda_powertools/metrics/base.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
MetricValueError,
1818
SchemaValidationError,
1919
)
20+
from aws_lambda_powertools.metrics.functions import convert_timestamp_to_emf_format, validate_emf_timestamp
2021
from aws_lambda_powertools.metrics.provider import cold_start
2122
from aws_lambda_powertools.metrics.provider.cloudwatch_emf.constants import MAX_DIMENSIONS, MAX_METRICS
2223
from aws_lambda_powertools.metrics.provider.cloudwatch_emf.metric_properties import MetricResolution, MetricUnit
@@ -76,6 +77,8 @@ def __init__(
7677
self.namespace = resolve_env_var_choice(choice=namespace, env=os.getenv(constants.METRICS_NAMESPACE_ENV))
7778
self.service = resolve_env_var_choice(choice=service, env=os.getenv(constants.SERVICE_NAME_ENV))
7879
self.metadata_set = metadata_set if metadata_set is not None else {}
80+
self.timestamp: int | None = None
81+
7982
self._metric_units = [unit.value for unit in MetricUnit]
8083
self._metric_unit_valid_options = list(MetricUnit.__members__)
8184
self._metric_resolutions = [resolution.value for resolution in MetricResolution]
@@ -224,7 +227,7 @@ def serialize_metric_set(
224227

225228
return {
226229
"_aws": {
227-
"Timestamp": int(datetime.datetime.now().timestamp() * 1000), # epoch
230+
"Timestamp": self.timestamp or int(datetime.datetime.now().timestamp() * 1000), # epoch
228231
"CloudWatchMetrics": [
229232
{
230233
"Namespace": self.namespace, # "test_namespace"
@@ -296,6 +299,31 @@ def add_metadata(self, key: str, value: Any) -> None:
296299
else:
297300
self.metadata_set[str(key)] = value
298301

302+
def set_timestamp(self, timestamp: int | datetime.datetime):
303+
"""
304+
Set the timestamp for the metric.
305+
306+
Parameters:
307+
-----------
308+
timestamp: int | datetime.datetime
309+
The timestamp to create the metric.
310+
If an integer is provided, it is assumed to be the epoch time in milliseconds.
311+
If a datetime object is provided, it will be converted to epoch time in milliseconds.
312+
"""
313+
# The timestamp must be a Datetime object or an integer representing an epoch time.
314+
# This should not exceed 14 days in the past or be more than 2 hours in the future.
315+
# Any metrics failing to meet this criteria will be skipped by Amazon CloudWatch.
316+
# See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
317+
# See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatch-Logs-Monitoring-CloudWatch-Metrics.html
318+
if not validate_emf_timestamp(timestamp):
319+
warnings.warn(
320+
"This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. "
321+
"Ensure the timestamp is within 14 days past or 2 hours future.",
322+
stacklevel=2,
323+
)
324+
325+
self.timestamp = convert_timestamp_to_emf_format(timestamp)
326+
299327
def clear_metrics(self) -> None:
300328
logger.debug("Clearing out existing metric set from memory")
301329
self.metric_set.clear()
@@ -576,6 +604,9 @@ def single_metric(
576604
Metric value
577605
namespace: str
578606
Namespace for metrics
607+
default_dimensions: Dict[str, str], optional
608+
Metric dimensions as key=value that will always be present
609+
579610
580611
Yields
581612
-------

aws_lambda_powertools/metrics/functions.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from __future__ import annotations
22

3+
from datetime import datetime
4+
35
from aws_lambda_powertools.metrics.provider.cloudwatch_emf.exceptions import (
46
MetricResolutionError,
57
MetricUnitError,
68
)
79
from aws_lambda_powertools.metrics.provider.cloudwatch_emf.metric_properties import MetricResolution, MetricUnit
10+
from aws_lambda_powertools.shared import constants
811
from aws_lambda_powertools.shared.types import List
912

1013

@@ -69,3 +72,66 @@ def extract_cloudwatch_metric_unit_value(metric_units: List, metric_valid_option
6972
unit = unit.value
7073

7174
return unit
75+
76+
77+
def validate_emf_timestamp(timestamp: int | datetime) -> bool:
78+
"""
79+
Validates a given timestamp based on CloudWatch Timestamp guidelines.
80+
81+
Timestamp must meet CloudWatch requirements, otherwise an InvalidTimestampError will be raised.
82+
See [Timestamps](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp)
83+
for valid values.
84+
85+
Parameters:
86+
----------
87+
timestamp: int | datetime
88+
Datetime object or epoch time in milliseconds representing the timestamp to validate.
89+
90+
Returns
91+
-------
92+
bool
93+
Valid or not timestamp values
94+
"""
95+
96+
if not isinstance(timestamp, (int, datetime)):
97+
return False
98+
99+
if isinstance(timestamp, datetime):
100+
# Converting timestamp to epoch time in milliseconds
101+
timestamp = int(timestamp.timestamp() * 1000)
102+
103+
# Consider current timezone when working with date and time
104+
current_timezone = datetime.now().astimezone().tzinfo
105+
106+
current_time = int(datetime.now(current_timezone).timestamp() * 1000)
107+
min_valid_timestamp = current_time - constants.EMF_MAX_TIMESTAMP_PAST_AGE
108+
max_valid_timestamp = current_time + constants.EMF_MAX_TIMESTAMP_FUTURE_AGE
109+
110+
return min_valid_timestamp <= timestamp <= max_valid_timestamp
111+
112+
113+
def convert_timestamp_to_emf_format(timestamp: int | datetime) -> int:
114+
"""
115+
Converts a timestamp to EMF compatible format.
116+
117+
Parameters
118+
----------
119+
timestamp: int | datetime
120+
The timestamp to convert. If already in epoch milliseconds format, returns it as is.
121+
If datetime object, converts it to milliseconds since Unix epoch.
122+
123+
Returns:
124+
--------
125+
int
126+
The timestamp converted to EMF compatible format (milliseconds since Unix epoch).
127+
"""
128+
if isinstance(timestamp, int):
129+
return timestamp
130+
131+
try:
132+
return int(round(timestamp.timestamp() * 1000))
133+
except AttributeError:
134+
# If this point is reached, it indicates timestamp is not a datetime object
135+
# Returning zero represents the initial date of epoch time,
136+
# which will be skipped by Amazon CloudWatch.
137+
return 0

aws_lambda_powertools/metrics/metrics.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,19 @@ def serialize_metric_set(
125125
def add_metadata(self, key: str, value: Any) -> None:
126126
self.provider.add_metadata(key=key, value=value)
127127

128+
def set_timestamp(self, timestamp: int):
129+
"""
130+
Set the timestamp for the metric.
131+
132+
Parameters:
133+
-----------
134+
timestamp: int | datetime.datetime
135+
The timestamp to create the metric.
136+
If an integer is provided, it is assumed to be the epoch time in milliseconds.
137+
If a datetime object is provided, it will be converted to epoch time in milliseconds.
138+
"""
139+
self.provider.set_timestamp(timestamp=timestamp)
140+
128141
def flush_metrics(self, raise_on_empty_metrics: bool = False) -> None:
129142
self.provider.flush_metrics(raise_on_empty_metrics=raise_on_empty_metrics)
130143

aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
from aws_lambda_powertools.metrics.base import single_metric
1313
from aws_lambda_powertools.metrics.exceptions import MetricValueError, SchemaValidationError
1414
from aws_lambda_powertools.metrics.functions import (
15+
convert_timestamp_to_emf_format,
1516
extract_cloudwatch_metric_resolution_value,
1617
extract_cloudwatch_metric_unit_value,
18+
validate_emf_timestamp,
1719
)
1820
from aws_lambda_powertools.metrics.provider.base import BaseProvider
1921
from aws_lambda_powertools.metrics.provider.cloudwatch_emf.constants import MAX_DIMENSIONS, MAX_METRICS
@@ -73,6 +75,7 @@ def __init__(
7375
self.namespace = resolve_env_var_choice(choice=namespace, env=os.getenv(constants.METRICS_NAMESPACE_ENV))
7476
self.service = resolve_env_var_choice(choice=service, env=os.getenv(constants.SERVICE_NAME_ENV))
7577
self.metadata_set = metadata_set if metadata_set is not None else {}
78+
self.timestamp: int | None = None
7679

7780
self._metric_units = [unit.value for unit in MetricUnit]
7881
self._metric_unit_valid_options = list(MetricUnit.__members__)
@@ -231,7 +234,7 @@ def serialize_metric_set(
231234

232235
return {
233236
"_aws": {
234-
"Timestamp": int(datetime.datetime.now().timestamp() * 1000), # epoch
237+
"Timestamp": self.timestamp or int(datetime.datetime.now().timestamp() * 1000), # epoch
235238
"CloudWatchMetrics": [
236239
{
237240
"Namespace": self.namespace, # "test_namespace"
@@ -304,6 +307,31 @@ def add_metadata(self, key: str, value: Any) -> None:
304307
else:
305308
self.metadata_set[str(key)] = value
306309

310+
def set_timestamp(self, timestamp: int | datetime.datetime):
311+
"""
312+
Set the timestamp for the metric.
313+
314+
Parameters:
315+
-----------
316+
timestamp: int | datetime.datetime
317+
The timestamp to create the metric.
318+
If an integer is provided, it is assumed to be the epoch time in milliseconds.
319+
If a datetime object is provided, it will be converted to epoch time in milliseconds.
320+
"""
321+
# The timestamp must be a Datetime object or an integer representing an epoch time.
322+
# This should not exceed 14 days in the past or be more than 2 hours in the future.
323+
# Any metrics failing to meet this criteria will be skipped by Amazon CloudWatch.
324+
# See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
325+
# See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatch-Logs-Monitoring-CloudWatch-Metrics.html
326+
if not validate_emf_timestamp(timestamp):
327+
warnings.warn(
328+
"This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. "
329+
"Ensure the timestamp is within 14 days past or 2 hours future.",
330+
stacklevel=2,
331+
)
332+
333+
self.timestamp = convert_timestamp_to_emf_format(timestamp)
334+
307335
def clear_metrics(self) -> None:
308336
logger.debug("Clearing out existing metric set from memory")
309337
self.metric_set.clear()

aws_lambda_powertools/shared/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
METRICS_NAMESPACE_ENV: str = "POWERTOOLS_METRICS_NAMESPACE"
4040
DATADOG_FLUSH_TO_LOG: str = "DD_FLUSH_TO_LOG"
4141
SERVICE_NAME_ENV: str = "POWERTOOLS_SERVICE_NAME"
42+
# If the timestamp of log event is more than 2 hours in future, the log event is skipped.
43+
# If the timestamp of log event is more than 14 days in past, the log event is skipped.
44+
# See https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AgentReference.html
45+
EMF_MAX_TIMESTAMP_PAST_AGE = 14 * 24 * 60 * 60 * 1000 # 14 days
46+
EMF_MAX_TIMESTAMP_FUTURE_AGE = 2 * 60 * 60 * 1000 # 2 hours
4247

4348
# Parameters constants
4449
PARAMETERS_SSM_DECRYPT_ENV: str = "POWERTOOLS_PARAMETERS_SSM_DECRYPT"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Exposes version constant to avoid circular dependencies."""
22

3-
VERSION = "2.35.1"
3+
VERSION = "2.36.0"

aws_lambda_powertools/utilities/data_classes/active_mq_event.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def properties(self) -> dict:
5959

6060
@property
6161
def destination_physicalname(self) -> str:
62-
return self["destination"]["physicalname"]
62+
return self["destination"]["physicalName"]
6363

6464
@property
6565
def delivery_mode(self) -> Optional[int]:

docs/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# v9.1.18
2-
FROM squidfunk/mkdocs-material@sha256:33076657e536b6b8439168296a193098aef3c4c88cc2cecd0736cd391b90e7fd
2+
FROM squidfunk/mkdocs-material@sha256:6b124e13728a591607e0f087920763e405fc18861736a997896d0b253867a7b7
33
# pip-compile --generate-hashes --output-file=requirements.txt requirements.in
44
COPY requirements.txt /tmp/
55
RUN pip install --require-hashes -r /tmp/requirements.txt

0 commit comments

Comments
 (0)