Skip to content

Commit

Permalink
Added support to set custom timestamp (#110)
Browse files Browse the repository at this point in the history
Added support to set custom timestamp

Co-authored-by: Mark Kuhn <kuhnmar@amazon.com>
  • Loading branch information
rayabagi and Mark Kuhn authored Sep 13, 2023
1 parent 7137ad1 commit 4c36304
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 9 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,20 @@ Examples:
set_namespace("MyApplication")
```

- **set_timestamp**(timestamp: datetime) -> MetricsLogger

Sets the timestamp of the metrics. If not set, current time of the client will be used.

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.

Examples:

```py
set_timestamp(datetime.datetime.now())
```



- **flush**()

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.
Expand Down
3 changes: 3 additions & 0 deletions aws_embedded_metrics/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@
MAX_METRIC_NAME_LENGTH = 1024
MAX_NAMESPACE_LENGTH = 256
VALID_NAMESPACE_REGEX = '^[a-zA-Z0-9._#:/-]+$'
TIMESTAMP = "Timestamp"
MAX_TIMESTAMP_PAST_AGE = 14 * 24 * 60 * 60 * 1000 # 14 days
MAX_TIMESTAMP_FUTURE_AGE = 2 * 60 * 60 * 1000 # 2 hours
6 changes: 6 additions & 0 deletions aws_embedded_metrics/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ class InvalidNamespaceError(Exception):
def __init__(self, message: str) -> None:
# Call the base class constructor with the parameters it needs
super().__init__(message)


class InvalidTimestampError(Exception):
def __init__(self, message: str) -> None:
# Call the base class constructor with the parameters it needs
super().__init__(message)
23 changes: 21 additions & 2 deletions aws_embedded_metrics/logger/metrics_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
# limitations under the License.


from aws_embedded_metrics import constants, utils
from datetime import datetime
from aws_embedded_metrics import constants, utils, validator
from aws_embedded_metrics.config import get_config
from aws_embedded_metrics.logger.metric import Metric
from aws_embedded_metrics.validator import validate_dimension_set, validate_metric
Expand All @@ -39,7 +40,7 @@ def __init__(
self.default_dimensions: Dict[str, str] = default_dimensions or {}
self.metrics: Dict[str, Metric] = {}
self.should_use_default_dimensions = True
self.meta: Dict[str, Any] = {"Timestamp": utils.now()}
self.meta: Dict[str, Any] = {constants.TIMESTAMP: utils.now()}
self.metric_name_and_resolution_map: Dict[str, StorageResolution] = {}

def put_metric(self, key: str, value: float, unit: str = None, storage_resolution: StorageResolution = StorageResolution.STANDARD) -> None:
Expand Down Expand Up @@ -176,3 +177,21 @@ def create_copy_with_context(self, preserve_dimensions: bool = False) -> "Metric
@staticmethod
def empty() -> "MetricsContext":
return MetricsContext()

def set_timestamp(self, timestamp: datetime) -> None:
"""
Set the timestamp of metrics emitted in this context. If not set, the timestamp will default to the time the context is constructed.
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.
Parameters:
timestamp (datetime): The timestamp value to be set.
Raises:
InvalidTimestampError: If the provided timestamp is invalid.
"""
validator.validate_timestamp(timestamp)
self.meta[constants.TIMESTAMP] = utils.convert_to_milliseconds(timestamp)
5 changes: 5 additions & 0 deletions aws_embedded_metrics/logger/metrics_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from datetime import datetime
from aws_embedded_metrics.environment import Environment
from aws_embedded_metrics.logger.metrics_context import MetricsContext
from aws_embedded_metrics.validator import validate_namespace
Expand Down Expand Up @@ -114,6 +115,10 @@ def add_stack_trace(self, key: str, details: Any = None, exc_info: Tuple = None)
self.set_property(key, trace_value)
return self

def set_timestamp(self, timestamp: datetime) -> "MetricsLogger":
self.context.set_timestamp(timestamp)
return self

def new(self) -> "MetricsLogger":
return MetricsLogger(
self.resolve_environment, self.context.create_copy_with_context()
Expand Down
8 changes: 8 additions & 0 deletions aws_embedded_metrics/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,12 @@
# limitations under the License.

import time
from datetime import datetime
def now() -> int: return int(round(time.time() * 1000))


def convert_to_milliseconds(dt: datetime) -> int:
if dt == datetime.min:
return 0

return int(round(dt.timestamp() * 1000))
33 changes: 32 additions & 1 deletion aws_embedded_metrics/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
from aws_embedded_metrics.unit import Unit
from aws_embedded_metrics.storage_resolution import StorageResolution
from aws_embedded_metrics.exceptions import DimensionSetExceededError, InvalidDimensionError, InvalidMetricError, InvalidNamespaceError
import aws_embedded_metrics.constants as constants
from aws_embedded_metrics.exceptions import InvalidTimestampError
from datetime import datetime
from aws_embedded_metrics import constants, utils


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

if not re.match(constants.VALID_NAMESPACE_REGEX, namespace):
raise InvalidNamespaceError(f"Namespace contains invalid characters: {namespace}")


def validate_timestamp(timestamp: datetime) -> None:
"""
Validates a given timestamp based on CloudWatch Timestamp guidelines.
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.
Parameters:
timestamp (datetime): Datetime object representing the timestamp to validate.
Raises:
InvalidTimestampError: If the timestamp is either None, too old, or too far in the future.
"""
if not timestamp:
raise InvalidTimestampError("Timestamp must be a valid datetime object")

given_time_in_milliseconds = utils.convert_to_milliseconds(timestamp)
current_time_in_milliseconds = utils.now()

if given_time_in_milliseconds < (current_time_in_milliseconds - constants.MAX_TIMESTAMP_PAST_AGE):
raise InvalidTimestampError(
f"Timestamp {str(timestamp)} must not be older than {int(constants.MAX_TIMESTAMP_PAST_AGE/(24 * 60 * 60 * 1000))} days")

if given_time_in_milliseconds > (current_time_in_milliseconds + constants.MAX_TIMESTAMP_FUTURE_AGE):
raise InvalidTimestampError(
f"Timestamp {str(timestamp)} must not be newer than {int(constants.MAX_TIMESTAMP_FUTURE_AGE/(60 * 60 * 1000))} hours")
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name="aws-embedded-metrics",
version="3.1.1",
version="3.2.0",
author="Amazon Web Services",
author_email="jarnance@amazon.com",
description="AWS Embedded Metrics Package",
Expand Down
1 change: 1 addition & 0 deletions tests/integ/agent/test_end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ async def do_work(metrics):
metrics.put_dimensions({"Operation": "Agent"})
metrics.put_metric(metric_name, 100, "Milliseconds")
metrics.set_property("RequestId", "422b1569-16f6-4a03-b8f0-fe3fd9b100f8")
metrics.set_timestamp(datetime.utcnow())

# act
await do_work()
Expand Down
48 changes: 44 additions & 4 deletions tests/logger/test_metrics_context.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from faker import Faker
from importlib import reload
from datetime import datetime, timedelta
import pytest
import math
import random
from aws_embedded_metrics import constants
from aws_embedded_metrics import constants, utils
from aws_embedded_metrics.unit import Unit
from aws_embedded_metrics.storage_resolution import StorageResolution
from aws_embedded_metrics import config
from aws_embedded_metrics.logger.metrics_context import MetricsContext
from aws_embedded_metrics.constants import DEFAULT_NAMESPACE
from aws_embedded_metrics.constants import DEFAULT_NAMESPACE, MAX_TIMESTAMP_FUTURE_AGE, MAX_TIMESTAMP_PAST_AGE
from aws_embedded_metrics.exceptions import DimensionSetExceededError, InvalidDimensionError, InvalidMetricError
from importlib import reload
from faker import Faker
from aws_embedded_metrics.exceptions import InvalidTimestampError

fake = Faker()

Expand Down Expand Up @@ -458,6 +460,44 @@ def test_cannot_put_more_than_30_dimensions():
context.put_dimensions(dimension_set)


@pytest.mark.parametrize(
"timestamp",
[
datetime.now(),
datetime.now() - timedelta(milliseconds=MAX_TIMESTAMP_PAST_AGE - 5000),
datetime.now() + timedelta(milliseconds=MAX_TIMESTAMP_FUTURE_AGE - 5000)
]
)
def test_set_valid_timestamp_verify_timestamp(timestamp: datetime):
context = MetricsContext()

context.set_timestamp(timestamp)

assert context.meta[constants.TIMESTAMP] == utils.convert_to_milliseconds(timestamp)


@pytest.mark.parametrize(
"timestamp",
[
None,
datetime.min,
datetime(1970, 1, 1, 0, 0, 0),
datetime.max,
datetime(9999, 12, 31, 23, 59, 59, 999999),
datetime(1, 1, 1, 0, 0, 0, 0, None),
datetime(1, 1, 1),
datetime(1, 1, 1, 0, 0),
datetime.now() - timedelta(milliseconds=MAX_TIMESTAMP_PAST_AGE + 1),
datetime.now() + timedelta(milliseconds=MAX_TIMESTAMP_FUTURE_AGE + 5000)
]
)
def test_set_invalid_timestamp_raises_exception(timestamp: datetime):
context = MetricsContext()

with pytest.raises(InvalidTimestampError):
context.set_timestamp(timestamp)


# Test utility method


Expand Down
18 changes: 17 additions & 1 deletion tests/logger/test_metrics_logger.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from aws_embedded_metrics import config
from datetime import datetime
from aws_embedded_metrics import config, utils
from aws_embedded_metrics.logger import metrics_logger
from aws_embedded_metrics.sinks import Sink
from aws_embedded_metrics.environment import Environment
Expand Down Expand Up @@ -493,6 +494,21 @@ async def test_configure_flush_to_preserve_dimensions(mocker):
assert dimensions[0][dimension_key] == dimension_value


@pytest.mark.asyncio
async def test_can_set_timestamp(mocker):
# arrange
expected_value = datetime.now()

logger, sink, env = get_logger_and_sink(mocker)

# act
logger.set_timestamp(expected_value)
await logger.flush()

# assert
context = get_flushed_context(sink)
assert context.meta[constants.TIMESTAMP] == utils.convert_to_milliseconds(expected_value)

# Test helper methods


Expand Down

0 comments on commit 4c36304

Please sign in to comment.