Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Added support to set custom timestamp #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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="[email protected]",
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