Skip to content

Commit 4c36304

Browse files
rayabagiMark Kuhn
and
Mark Kuhn
authored
Added support to set custom timestamp (#110)
Added support to set custom timestamp Co-authored-by: Mark Kuhn <[email protected]>
1 parent 7137ad1 commit 4c36304

11 files changed

+152
-9
lines changed

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,20 @@ Examples:
178178
set_namespace("MyApplication")
179179
```
180180

181+
- **set_timestamp**(timestamp: datetime) -> MetricsLogger
182+
183+
Sets the timestamp of the metrics. If not set, current time of the client will be used.
184+
185+
Timestamp must meet CloudWatch requirements, otherwise a InvalidTimestampError will be thrown. See [Timestamps](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp) for valid values.
186+
187+
Examples:
188+
189+
```py
190+
set_timestamp(datetime.datetime.now())
191+
```
192+
193+
194+
181195
- **flush**()
182196

183197
Flushes the current MetricsContext to the configured sink and resets all properties and metric values. The namespace and default dimensions will be preserved across flushes.

aws_embedded_metrics/constants.py

+3
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@
2020
MAX_METRIC_NAME_LENGTH = 1024
2121
MAX_NAMESPACE_LENGTH = 256
2222
VALID_NAMESPACE_REGEX = '^[a-zA-Z0-9._#:/-]+$'
23+
TIMESTAMP = "Timestamp"
24+
MAX_TIMESTAMP_PAST_AGE = 14 * 24 * 60 * 60 * 1000 # 14 days
25+
MAX_TIMESTAMP_FUTURE_AGE = 2 * 60 * 60 * 1000 # 2 hours

aws_embedded_metrics/exceptions.py

+6
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,9 @@ class InvalidNamespaceError(Exception):
3333
def __init__(self, message: str) -> None:
3434
# Call the base class constructor with the parameters it needs
3535
super().__init__(message)
36+
37+
38+
class InvalidTimestampError(Exception):
39+
def __init__(self, message: str) -> None:
40+
# Call the base class constructor with the parameters it needs
41+
super().__init__(message)

aws_embedded_metrics/logger/metrics_context.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
# limitations under the License.
1313

1414

15-
from aws_embedded_metrics import constants, utils
15+
from datetime import datetime
16+
from aws_embedded_metrics import constants, utils, validator
1617
from aws_embedded_metrics.config import get_config
1718
from aws_embedded_metrics.logger.metric import Metric
1819
from aws_embedded_metrics.validator import validate_dimension_set, validate_metric
@@ -39,7 +40,7 @@ def __init__(
3940
self.default_dimensions: Dict[str, str] = default_dimensions or {}
4041
self.metrics: Dict[str, Metric] = {}
4142
self.should_use_default_dimensions = True
42-
self.meta: Dict[str, Any] = {"Timestamp": utils.now()}
43+
self.meta: Dict[str, Any] = {constants.TIMESTAMP: utils.now()}
4344
self.metric_name_and_resolution_map: Dict[str, StorageResolution] = {}
4445

4546
def put_metric(self, key: str, value: float, unit: str = None, storage_resolution: StorageResolution = StorageResolution.STANDARD) -> None:
@@ -176,3 +177,21 @@ def create_copy_with_context(self, preserve_dimensions: bool = False) -> "Metric
176177
@staticmethod
177178
def empty() -> "MetricsContext":
178179
return MetricsContext()
180+
181+
def set_timestamp(self, timestamp: datetime) -> None:
182+
"""
183+
Set the timestamp of metrics emitted in this context. If not set, the timestamp will default to the time the context is constructed.
184+
185+
Timestamp must meet CloudWatch requirements, otherwise a InvalidTimestampError will be thrown.
186+
See [Timestamps](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp)
187+
for valid values.
188+
189+
Parameters:
190+
timestamp (datetime): The timestamp value to be set.
191+
192+
Raises:
193+
InvalidTimestampError: If the provided timestamp is invalid.
194+
195+
"""
196+
validator.validate_timestamp(timestamp)
197+
self.meta[constants.TIMESTAMP] = utils.convert_to_milliseconds(timestamp)

aws_embedded_metrics/logger/metrics_logger.py

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# See the License for the specific language governing permissions and
1212
# limitations under the License.
1313

14+
from datetime import datetime
1415
from aws_embedded_metrics.environment import Environment
1516
from aws_embedded_metrics.logger.metrics_context import MetricsContext
1617
from aws_embedded_metrics.validator import validate_namespace
@@ -114,6 +115,10 @@ def add_stack_trace(self, key: str, details: Any = None, exc_info: Tuple = None)
114115
self.set_property(key, trace_value)
115116
return self
116117

118+
def set_timestamp(self, timestamp: datetime) -> "MetricsLogger":
119+
self.context.set_timestamp(timestamp)
120+
return self
121+
117122
def new(self) -> "MetricsLogger":
118123
return MetricsLogger(
119124
self.resolve_environment, self.context.create_copy_with_context()

aws_embedded_metrics/utils.py

+8
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,12 @@
1212
# limitations under the License.
1313

1414
import time
15+
from datetime import datetime
1516
def now() -> int: return int(round(time.time() * 1000))
17+
18+
19+
def convert_to_milliseconds(dt: datetime) -> int:
20+
if dt == datetime.min:
21+
return 0
22+
23+
return int(round(dt.timestamp() * 1000))

aws_embedded_metrics/validator.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
from aws_embedded_metrics.unit import Unit
1818
from aws_embedded_metrics.storage_resolution import StorageResolution
1919
from aws_embedded_metrics.exceptions import DimensionSetExceededError, InvalidDimensionError, InvalidMetricError, InvalidNamespaceError
20-
import aws_embedded_metrics.constants as constants
20+
from aws_embedded_metrics.exceptions import InvalidTimestampError
21+
from datetime import datetime
22+
from aws_embedded_metrics import constants, utils
2123

2224

2325
def validate_dimension_set(dimension_set: Dict[str, str]) -> None:
@@ -114,3 +116,32 @@ def validate_namespace(namespace: str) -> None:
114116

115117
if not re.match(constants.VALID_NAMESPACE_REGEX, namespace):
116118
raise InvalidNamespaceError(f"Namespace contains invalid characters: {namespace}")
119+
120+
121+
def validate_timestamp(timestamp: datetime) -> None:
122+
"""
123+
Validates a given timestamp based on CloudWatch Timestamp guidelines.
124+
125+
Timestamp must meet CloudWatch requirements, otherwise a InvalidTimestampError will be thrown.
126+
See [Timestamps](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp)
127+
for valid values.
128+
129+
Parameters:
130+
timestamp (datetime): Datetime object representing the timestamp to validate.
131+
132+
Raises:
133+
InvalidTimestampError: If the timestamp is either None, too old, or too far in the future.
134+
"""
135+
if not timestamp:
136+
raise InvalidTimestampError("Timestamp must be a valid datetime object")
137+
138+
given_time_in_milliseconds = utils.convert_to_milliseconds(timestamp)
139+
current_time_in_milliseconds = utils.now()
140+
141+
if given_time_in_milliseconds < (current_time_in_milliseconds - constants.MAX_TIMESTAMP_PAST_AGE):
142+
raise InvalidTimestampError(
143+
f"Timestamp {str(timestamp)} must not be older than {int(constants.MAX_TIMESTAMP_PAST_AGE/(24 * 60 * 60 * 1000))} days")
144+
145+
if given_time_in_milliseconds > (current_time_in_milliseconds + constants.MAX_TIMESTAMP_FUTURE_AGE):
146+
raise InvalidTimestampError(
147+
f"Timestamp {str(timestamp)} must not be newer than {int(constants.MAX_TIMESTAMP_FUTURE_AGE/(60 * 60 * 1000))} hours")

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name="aws-embedded-metrics",
8-
version="3.1.1",
8+
version="3.2.0",
99
author="Amazon Web Services",
1010
author_email="[email protected]",
1111
description="AWS Embedded Metrics Package",

tests/integ/agent/test_end_to_end.py

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ async def do_work(metrics):
4242
metrics.put_dimensions({"Operation": "Agent"})
4343
metrics.put_metric(metric_name, 100, "Milliseconds")
4444
metrics.set_property("RequestId", "422b1569-16f6-4a03-b8f0-fe3fd9b100f8")
45+
metrics.set_timestamp(datetime.utcnow())
4546

4647
# act
4748
await do_work()

tests/logger/test_metrics_context.py

+44-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1+
from faker import Faker
2+
from importlib import reload
3+
from datetime import datetime, timedelta
14
import pytest
25
import math
36
import random
4-
from aws_embedded_metrics import constants
7+
from aws_embedded_metrics import constants, utils
58
from aws_embedded_metrics.unit import Unit
69
from aws_embedded_metrics.storage_resolution import StorageResolution
710
from aws_embedded_metrics import config
811
from aws_embedded_metrics.logger.metrics_context import MetricsContext
9-
from aws_embedded_metrics.constants import DEFAULT_NAMESPACE
12+
from aws_embedded_metrics.constants import DEFAULT_NAMESPACE, MAX_TIMESTAMP_FUTURE_AGE, MAX_TIMESTAMP_PAST_AGE
1013
from aws_embedded_metrics.exceptions import DimensionSetExceededError, InvalidDimensionError, InvalidMetricError
11-
from importlib import reload
12-
from faker import Faker
14+
from aws_embedded_metrics.exceptions import InvalidTimestampError
1315

1416
fake = Faker()
1517

@@ -458,6 +460,44 @@ def test_cannot_put_more_than_30_dimensions():
458460
context.put_dimensions(dimension_set)
459461

460462

463+
@pytest.mark.parametrize(
464+
"timestamp",
465+
[
466+
datetime.now(),
467+
datetime.now() - timedelta(milliseconds=MAX_TIMESTAMP_PAST_AGE - 5000),
468+
datetime.now() + timedelta(milliseconds=MAX_TIMESTAMP_FUTURE_AGE - 5000)
469+
]
470+
)
471+
def test_set_valid_timestamp_verify_timestamp(timestamp: datetime):
472+
context = MetricsContext()
473+
474+
context.set_timestamp(timestamp)
475+
476+
assert context.meta[constants.TIMESTAMP] == utils.convert_to_milliseconds(timestamp)
477+
478+
479+
@pytest.mark.parametrize(
480+
"timestamp",
481+
[
482+
None,
483+
datetime.min,
484+
datetime(1970, 1, 1, 0, 0, 0),
485+
datetime.max,
486+
datetime(9999, 12, 31, 23, 59, 59, 999999),
487+
datetime(1, 1, 1, 0, 0, 0, 0, None),
488+
datetime(1, 1, 1),
489+
datetime(1, 1, 1, 0, 0),
490+
datetime.now() - timedelta(milliseconds=MAX_TIMESTAMP_PAST_AGE + 1),
491+
datetime.now() + timedelta(milliseconds=MAX_TIMESTAMP_FUTURE_AGE + 5000)
492+
]
493+
)
494+
def test_set_invalid_timestamp_raises_exception(timestamp: datetime):
495+
context = MetricsContext()
496+
497+
with pytest.raises(InvalidTimestampError):
498+
context.set_timestamp(timestamp)
499+
500+
461501
# Test utility method
462502

463503

tests/logger/test_metrics_logger.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from aws_embedded_metrics import config
1+
from datetime import datetime
2+
from aws_embedded_metrics import config, utils
23
from aws_embedded_metrics.logger import metrics_logger
34
from aws_embedded_metrics.sinks import Sink
45
from aws_embedded_metrics.environment import Environment
@@ -493,6 +494,21 @@ async def test_configure_flush_to_preserve_dimensions(mocker):
493494
assert dimensions[0][dimension_key] == dimension_value
494495

495496

497+
@pytest.mark.asyncio
498+
async def test_can_set_timestamp(mocker):
499+
# arrange
500+
expected_value = datetime.now()
501+
502+
logger, sink, env = get_logger_and_sink(mocker)
503+
504+
# act
505+
logger.set_timestamp(expected_value)
506+
await logger.flush()
507+
508+
# assert
509+
context = get_flushed_context(sink)
510+
assert context.meta[constants.TIMESTAMP] == utils.convert_to_milliseconds(expected_value)
511+
496512
# Test helper methods
497513

498514

0 commit comments

Comments
 (0)