Skip to content

Commit 214bec8

Browse files
author
Meshwa Savalia
committed
Initial implementation for supporting High Resolution Metrics
1 parent ab030ba commit 214bec8

File tree

10 files changed

+144
-24
lines changed

10 files changed

+144
-24
lines changed

aws_embedded_metrics/logger/metric.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313

1414

1515
class Metric(object):
16-
def __init__(self, value: float, unit: str = None):
16+
def __init__(self, value: float, unit: str = None, storageResolution: int = 60):
1717
self.values = [value]
1818
self.unit = unit or "None"
19+
self.storageResolution = storageResolution
1920

2021
def add_value(self, value: float) -> None:
2122
self.values.append(value)

aws_embedded_metrics/logger/metrics_context.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ def __init__(
3939
self.metrics: Dict[str, Metric] = {}
4040
self.should_use_default_dimensions = True
4141
self.meta: Dict[str, Any] = {"Timestamp": utils.now()}
42+
self.metricNameAndResolutionMap: Dict[str, int] = {}
4243

43-
def put_metric(self, key: str, value: float, unit: str = None) -> None:
44+
def put_metric(self, key: str, value: float, unit: str = None, storageResolution: int = 60) -> None:
4445
"""
4546
Adds a metric measurement to the context.
4647
Multiple calls using the same key will be stored as an
@@ -49,13 +50,13 @@ def put_metric(self, key: str, value: float, unit: str = None) -> None:
4950
context.put_metric("Latency", 100, "Milliseconds")
5051
```
5152
"""
52-
validate_metric(key, value, unit)
53+
validate_metric(key, value, unit, storageResolution, self.metricNameAndResolutionMap)
5354
metric = self.metrics.get(key)
5455
if metric:
5556
# TODO: we should log a warning if the unit has been changed
5657
metric.add_value(value)
5758
else:
58-
self.metrics[key] = Metric(value, unit)
59+
self.metrics[key] = Metric(value, unit, storageResolution)
5960

6061
def put_dimensions(self, dimension_set: Dict[str, str]) -> None:
6162
"""

aws_embedded_metrics/logger/metrics_logger.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ def set_namespace(self, namespace: str) -> "MetricsLogger":
7878
self.context.namespace = namespace
7979
return self
8080

81-
def put_metric(self, key: str, value: float, unit: str = "None") -> "MetricsLogger":
82-
self.context.put_metric(key, value, unit)
81+
def put_metric(self, key: str, value: float, unit: str = "None", storageResolution: int = 60) -> "MetricsLogger":
82+
self.context.put_metric(key, value, unit, storageResolution)
8383
return self
8484

8585
def add_stack_trace(self, key: str, details: Any = None, exc_info: Tuple = None) -> "MetricsLogger":

aws_embedded_metrics/serializers/log_serializer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,11 @@ def create_body() -> Dict[str, Any]:
8787
if len(metric.values) > end_index:
8888
remaining_data = True
8989

90+
metricBody = {"Name": metric_name, "Unit": metric.unit}
91+
if metric.storageResolution == 1:
92+
metricBody["StorageResolution"] = metric.storageResolution
9093
if not config.disable_metric_extraction:
91-
current_body["_aws"]["CloudWatchMetrics"][0]["Metrics"].append({"Name": metric_name, "Unit": metric.unit})
94+
current_body["_aws"]["CloudWatchMetrics"][0]["Metrics"].append(metricBody)
9295
num_metrics_in_current_body += 1
9396

9497
if (num_metrics_in_current_body == MAX_METRICS_PER_EVENT):
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from enum import Enum, EnumMeta
2+
3+
4+
class StorageResolutionMeta(EnumMeta):
5+
def __contains__(self, item: object) -> bool:
6+
try:
7+
self(item)
8+
except (ValueError, TypeError):
9+
return False
10+
else:
11+
return True
12+
13+
14+
class StorageResolution(Enum, metaclass=StorageResolutionMeta):
15+
STANDARD = 60
16+
HIGH = 1

aws_embedded_metrics/validator.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import re
1616
from typing import Dict, Optional
1717
from aws_embedded_metrics.unit import Unit
18+
from aws_embedded_metrics.storageResolution import StorageResolution
1819
from aws_embedded_metrics.exceptions import DimensionSetExceededError, InvalidDimensionError, InvalidMetricError, InvalidNamespaceError
1920
import aws_embedded_metrics.constants as constants
2021

@@ -57,14 +58,15 @@ def validate_dimension_set(dimension_set: Dict[str, str]) -> None:
5758
raise InvalidDimensionError("Dimension name cannot start with ':'")
5859

5960

60-
def validate_metric(name: str, value: float, unit: Optional[str]) -> None:
61+
def validate_metric(name: str, value: float, unit: Optional[str], storageResolution: Optional[int], metricNameAndResolutionMap: Optional[dict]) -> None: # noqa: E501
6162
"""
6263
Validates a metric
6364
6465
Parameters:
6566
name (str): The name of the metric
6667
value (float): The value of the metric
6768
unit (Optional[str]): The unit of the metric
69+
storageResolution (Optional[int]): The storage resolution of metric
6870
6971
Raises:
7072
InvalidMetricError: If the metric is invalid
@@ -81,6 +83,16 @@ def validate_metric(name: str, value: float, unit: Optional[str]) -> None:
8183
if unit is not None and unit not in Unit:
8284
raise InvalidMetricError(f"Metric unit is not valid: {unit}")
8385

86+
if storageResolution is None or storageResolution not in StorageResolution:
87+
raise InvalidMetricError(f"Metric storage Resolution is not valid: {storageResolution}")
88+
89+
if metricNameAndResolutionMap and name in metricNameAndResolutionMap:
90+
if metricNameAndResolutionMap.get(name) is not storageResolution:
91+
raise InvalidMetricError(
92+
f"Resolution for metrics ${name} is already set. A single log event cannot have a metric with two different resolutions.")
93+
else:
94+
metricNameAndResolutionMap[name] = storageResolution
95+
8496

8597
def validate_namespace(namespace: str) -> None:
8698
"""

tests/canary/agent/canary.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ async def app(init, last_run_duration, metrics):
2626
metrics.set_dimensions({"Runtime": 'Python', "Platform": 'ECS', "Agent": 'CloudWatchAgent', "Version": version})
2727
metrics.put_metric('Invoke', 1, "Count")
2828
metrics.put_metric('Duration', last_run_duration, 'Seconds')
29-
metrics.put_metric('Memory.RSS', process.memory_info().rss, 'Bytes')
29+
metrics.put_metric('Memory.RSS', process.memory_info().rss, 'Bytes', 1)
3030

3131

3232
async def main():

tests/logger/test_metrics_context.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import random
44
from aws_embedded_metrics import constants
55
from aws_embedded_metrics.unit import Unit
6+
from aws_embedded_metrics.storageResolution import StorageResolution
67
from aws_embedded_metrics import config
78
from aws_embedded_metrics.logger.metrics_context import MetricsContext
89
from aws_embedded_metrics.constants import DEFAULT_NAMESPACE
@@ -263,14 +264,16 @@ def test_put_metric_adds_metrics():
263264
metric_key = fake.word()
264265
metric_value = fake.random.random()
265266
metric_unit = random.choice(list(Unit)).value
267+
metric_storageResolution = random.choice(list(StorageResolution)).value
266268

267269
# act
268-
context.put_metric(metric_key, metric_value, metric_unit)
270+
context.put_metric(metric_key, metric_value, metric_unit, metric_storageResolution)
269271

270272
# assert
271273
metric = context.metrics[metric_key]
272274
assert metric.unit == metric_unit
273275
assert metric.values == [metric_value]
276+
assert metric.storageResolution == metric_storageResolution
274277

275278

276279
def test_put_metric_uses_none_unit_if_not_provided():
@@ -287,26 +290,42 @@ def test_put_metric_uses_none_unit_if_not_provided():
287290
assert metric.unit == "None"
288291

289292

293+
def test_put_metric_uses_60_storage_resolution_if_not_provided():
294+
# arrange
295+
context = MetricsContext()
296+
metric_key = fake.word()
297+
metric_value = fake.random.random()
298+
299+
# act
300+
context.put_metric(metric_key, metric_value)
301+
302+
# assert
303+
metric = context.metrics[metric_key]
304+
assert metric.storageResolution == 60
305+
306+
290307
@pytest.mark.parametrize(
291-
"name, value, unit",
308+
"name, value, unit, storageResolution",
292309
[
293-
("", 1, "None"),
294-
(" ", 1, "Seconds"),
295-
("a" * (constants.MAX_METRIC_NAME_LENGTH + 1), 1, "None"),
296-
("metric", float("inf"), "Count"),
297-
("metric", float("-inf"), "Count"),
298-
("metric", float("nan"), "Count"),
299-
("metric", math.inf, "Seconds"),
300-
("metric", -math.inf, "Seconds"),
301-
("metric", math.nan, "Seconds"),
302-
("metric", 1, "Kilometers/Fahrenheit")
310+
("", 1, "None", 60),
311+
(" ", 1, "Seconds", 60),
312+
("a" * (constants.MAX_METRIC_NAME_LENGTH + 1), 1, "None", 60),
313+
("metric", float("inf"), "Count", 60),
314+
("metric", float("-inf"), "Count", 60),
315+
("metric", float("nan"), "Count", 60),
316+
("metric", math.inf, "Seconds", 60),
317+
("metric", -math.inf, "Seconds", 60),
318+
("metric", math.nan, "Seconds", 60),
319+
("metric", 1, "Kilometers/Fahrenheit", 60),
320+
("metric", 1, "Seconds", 2),
321+
("metric", 1, "Seconds", None)
303322
]
304323
)
305-
def test_put_invalid_metric_raises_exception(name, value, unit):
324+
def test_put_invalid_metric_raises_exception(name, value, unit, storageResolution):
306325
context = MetricsContext()
307326

308327
with pytest.raises(InvalidMetricError):
309-
context.put_metric(name, value, unit)
328+
context.put_metric(name, value, unit, storageResolution)
310329

311330

312331
def test_create_copy_with_context_creates_new_instance():

tests/logger/test_metrics_logger.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from aws_embedded_metrics.logger import metrics_logger
33
from aws_embedded_metrics.sinks import Sink
44
from aws_embedded_metrics.environment import Environment
5-
from aws_embedded_metrics.exceptions import InvalidNamespaceError
5+
from aws_embedded_metrics.exceptions import InvalidNamespaceError, InvalidMetricError
66
import aws_embedded_metrics.constants as constants
77
import pytest
88
from faker import Faker
@@ -53,6 +53,51 @@ async def test_can_put_metric(mocker):
5353
assert context.metrics[expected_key].unit == "None"
5454

5555

56+
@pytest.mark.asyncio
57+
async def test_can_put_metric_with_different_storage_resolution_different_flush(mocker):
58+
# arrange
59+
expected_key = fake.word()
60+
expected_value = fake.random.randrange(100)
61+
metric_storageResolution = 1
62+
63+
logger, sink, env = get_logger_and_sink(mocker)
64+
65+
# act
66+
logger.put_metric(expected_key, expected_value, None, metric_storageResolution)
67+
await logger.flush()
68+
69+
# assert
70+
context = sink.accept.call_args[0][0]
71+
assert context.metrics[expected_key].values == [expected_value]
72+
assert context.metrics[expected_key].unit == "None"
73+
assert context.metrics[expected_key].storageResolution == metric_storageResolution
74+
75+
expected_key = fake.word()
76+
expected_value = fake.random.randrange(100)
77+
logger.put_metric(expected_key, expected_value, None)
78+
await logger.flush()
79+
context = sink.accept.call_args[0][0]
80+
assert context.metrics[expected_key].values == [expected_value]
81+
assert context.metrics[expected_key].unit == "None"
82+
assert context.metrics[expected_key].storageResolution == 60
83+
84+
85+
@pytest.mark.asyncio
86+
async def test_cannot_put_metric_with_different_storage_resolution_same_flush(mocker):
87+
# arrange
88+
expected_key = fake.word()
89+
expected_value = fake.random.randrange(100)
90+
metric_storageResolution = 1
91+
92+
logger, sink, env = get_logger_and_sink(mocker)
93+
94+
# act
95+
with pytest.raises(InvalidMetricError):
96+
logger.put_metric(expected_key, expected_value, None, metric_storageResolution)
97+
logger.put_metric(expected_key, expected_value, None, 60)
98+
await logger.flush()
99+
100+
56101
@pytest.mark.asyncio
57102
async def test_can_add_stack_trace(mocker):
58103
# arrange

tests/serializer/test_log_serializer.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,29 @@ def test_serialize_metrics():
9292
assert_json_equality(result_json, expected)
9393

9494

95+
def test_serialize_metrics_with_storageResolution():
96+
# arrange
97+
expected_key = fake.word()
98+
expected_value = fake.random.randrange(0, 100)
99+
100+
expected_metric_definition = {"Name": expected_key, "Unit": "None", "StorageResolution": 1}
101+
102+
expected = {**get_empty_payload()}
103+
expected[expected_key] = expected_value
104+
expected["_aws"]["CloudWatchMetrics"][0]["Metrics"].append(
105+
expected_metric_definition
106+
)
107+
108+
context = get_context()
109+
context.put_metric(expected_key, expected_value, "None", 1)
110+
111+
# act
112+
result_json = serializer.serialize(context)[0]
113+
114+
# assert
115+
assert_json_equality(result_json, expected)
116+
117+
95118
def test_serialize_more_than_100_metrics():
96119
# arrange
97120
expected_value = fake.random.randrange(0, 100)

0 commit comments

Comments
 (0)