From bc1c71897173a5ff271a5771a893cc3e286dcd41 Mon Sep 17 00:00:00 2001 From: Stephen-Bao Date: Mon, 22 Aug 2022 12:01:42 -0400 Subject: [PATCH 1/6] Support clear custom dimensions and related enhancements --- README.md | 39 +++++- .../logger/metrics_context.py | 22 +++- aws_embedded_metrics/logger/metrics_logger.py | 12 +- tests/logger/test_metrics_logger.py | 123 ++++++++++++++++++ 4 files changed, 189 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 58af6a7..f361ab9 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,9 @@ put_dimensions({ "Operation": "Aggregator" }) put_dimensions({ "Operation": "Aggregator", "DeviceType": "Actuator" }) ``` -- **set_dimensions**(\*dimensions: Dict[str, str]) -> MetricsLogger +- **set_dimensions**(\*dimensions: Dict[str, str], use_default: bool = False) -> MetricsLogger -Explicitly override all dimensions. This will remove the default dimensions. +Explicitly override all dimensions. By default, this will disable the default dimensions, but can be configured using the *keyword-only* parameter `use_default`. **WARNING**: Every distinct value will result in a new CloudWatch Metric. If the cardinality of a particular value is expected to be high, you should consider @@ -132,6 +132,23 @@ set_dimensions( ) ``` +```py +set_dimensions( + { "Operation": "Aggregator" }, + use_default = True # default dimensions would be enabled +) +``` + +- **reset_dimensions**(use_default: bool) -> MetricsLogger + +Explicitly clear all custom dimensions. The behavior of whether default dimensions should be used can be configured by `use_default` parameter. + +Examples: + +```py +reset_dimensions(False) # this will clear all custom dimensions as well as disable default dimensions +``` + - **set_namespace**(value: str) -> MetricsLogger Sets the CloudWatch [namespace](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Namespace) that extracted metrics should be published to. If not set, a default value of aws-embedded-metrics will be used. @@ -151,6 +168,24 @@ set_namespace("MyApplication") Flushes the current MetricsContext to the configured sink and resets all properties, dimensions and metric values. The namespace and default dimensions will be preserved across flushes. +The default behavior is to clear all custom dimensions (dimensions added by `put_dimension`) across each flush(), but this can be configured by invoking `logger.flush_preserve_dimensions = True`. + +Example: + +```py +logger.flush() # only default dimensions will be preserved after each flush() +``` + +```py +logger.flush_preserve_dimensions(True) +logger.flush() # custom dimensions and default dimensions will be preserved after each flush() +``` + +```py +logger.reset_dimensions(False) +logger.flush() # default dimensions are disabled; no dimensions will be preserved after each flush() +``` + ### Configuration All configuration values can be set using environment variables with the prefix (`AWS_EMF_`). Configuration should be performed as close to application start up as possible. diff --git a/aws_embedded_metrics/logger/metrics_context.py b/aws_embedded_metrics/logger/metrics_context.py index 3f46894..11f1693 100644 --- a/aws_embedded_metrics/logger/metrics_context.py +++ b/aws_embedded_metrics/logger/metrics_context.py @@ -81,7 +81,7 @@ def put_dimensions(self, dimension_set: Dict[str, str]) -> None: self.dimensions.append(dimension_set) - def set_dimensions(self, dimension_sets: List[Dict[str, str]]) -> None: + def set_dimensions(self, dimension_sets: List[Dict[str, str]], use_default: bool = False) -> None: """ Overwrite all dimensions. ``` @@ -90,7 +90,7 @@ def set_dimensions(self, dimension_sets: List[Dict[str, str]]) -> None: { "k1": "v1", "k2": "v2" }]) ``` """ - self.should_use_default_dimensions = False + self.should_use_default_dimensions = use_default for dimension_set in dimension_sets: self.validate_dimension_set(dimension_set) @@ -108,6 +108,16 @@ def set_default_dimensions(self, default_dimensions: Dict) -> None: """ self.default_dimensions = default_dimensions + def reset_dimensions(self, use_default: bool) -> None: + """ + Clear all custom dimensions on this MetricsLogger instance. Whether default dimensions should + be used can be configured by the input parameter. + :param use_default: indicates whether default dimensions should be used + """ + new_dimensions: List[Dict] = [] + self.dimensions = new_dimensions + self.should_use_default_dimensions = use_default + def set_property(self, key: str, value: Any) -> None: self.properties[key] = value @@ -162,6 +172,14 @@ def create_copy_with_context(self) -> "MetricsContext": self.namespace, new_properties, new_dimensions, new_default_dimensions ) + def create_copy_with_context_with_dimensions(self) -> "MetricsContext": + # dimensions added with put_dimension will be copied. + # this helps reuse of dimension sets. + new_context = self.create_copy_with_context() + new_context.dimensions = self.dimensions + + return new_context + @staticmethod def empty() -> "MetricsContext": return MetricsContext() diff --git a/aws_embedded_metrics/logger/metrics_logger.py b/aws_embedded_metrics/logger/metrics_logger.py index c1da08a..13defe5 100644 --- a/aws_embedded_metrics/logger/metrics_logger.py +++ b/aws_embedded_metrics/logger/metrics_logger.py @@ -29,6 +29,7 @@ def __init__( ): self.resolve_environment = resolve_environment self.context: MetricsContext = context or MetricsContext.empty() + self.flush_preserve_dimensions: bool = False async def flush(self) -> None: # resolve the environment and get the sink @@ -42,7 +43,8 @@ async def flush(self) -> None: # accept and reset the context sink.accept(self.context) - self.context = self.context.create_copy_with_context() + self.context = self.context.create_copy_with_context() if not self.flush_preserve_dimensions \ + else self.context.create_copy_with_context_with_dimensions() def __configureContextForEnvironment(self, env: Environment) -> None: default_dimensions = { @@ -63,8 +65,12 @@ def put_dimensions(self, dimensions: Dict[str, str]) -> "MetricsLogger": self.context.put_dimensions(dimensions) return self - def set_dimensions(self, *dimensions: Dict[str, str]) -> "MetricsLogger": - self.context.set_dimensions(list(dimensions)) + def set_dimensions(self, *dimensions: Dict[str, str], use_default: bool = False) -> "MetricsLogger": + self.context.set_dimensions(list(dimensions), use_default) + return self + + def reset_dimensions(self, use_default: bool) -> "MetricsLogger": + self.context.reset_dimensions(use_default) return self def set_namespace(self, namespace: str) -> "MetricsLogger": diff --git a/tests/logger/test_metrics_logger.py b/tests/logger/test_metrics_logger.py index d3fe900..641234a 100644 --- a/tests/logger/test_metrics_logger.py +++ b/tests/logger/test_metrics_logger.py @@ -191,6 +191,50 @@ async def test_put_dimension(mocker): assert dimensions[0][expected_key] == expected_value +@pytest.mark.asyncio +async def test_reset_dimension_with_default_dimension(mocker): + # arrange + pair1 = ["key1", "val1"] + pair2 = ["key2", "val2"] + + logger, sink, env = get_logger_and_sink(mocker) + + # act + logger.put_dimensions({pair1[0]: pair1[1]}) + logger.reset_dimensions(True) + logger.put_dimensions({pair2[0]: pair2[1]}) + await logger.flush() + + # assert + context = get_flushed_context(sink) + dimensions = context.get_dimensions() + assert len(dimensions) == 1 + assert len(dimensions[0]) == 4 + assert dimensions[0][pair2[0]] == pair2[1] + + +@pytest.mark.asyncio +async def test_reset_dimension_without_default_dimension(mocker): + # arrange + pair1 = ["key1", "val1"] + pair2 = ["key2", "val2"] + + logger, sink, env = get_logger_and_sink(mocker) + + # act + logger.put_dimensions({pair1[0]: pair1[1]}) + logger.reset_dimensions(False) + logger.put_dimensions({pair2[0]: pair2[1]}) + await logger.flush() + + # assert + context = get_flushed_context(sink) + dimensions = context.get_dimensions() + assert len(dimensions) == 1 + assert len(dimensions[0]) == 1 + assert dimensions[0][pair2[0]] == pair2[1] + + @pytest.mark.asyncio async def test_logger_configures_default_dimensions_on_flush(before, mocker): # arrange @@ -267,6 +311,32 @@ async def test_set_dimensions_overrides_all_dimensions(mocker): assert dimensions[expected_key] == expected_value +@pytest.mark.asyncio +async def test_configure_set_dimensions_to_preserve_default_dimensions(mocker): + # arrange + logger, sink, env = get_logger_and_sink(mocker) + + # setup the typical default dimensions + env.get_log_group_name.return_value = fake.word() + env.get_name.return_value = fake.word() + env.get_type.return_value = fake.word() + + expected_key = fake.word() + expected_value = fake.word() + + # act + logger.set_dimensions({expected_key: expected_value}, use_default=True) + await logger.flush() + + # assert + context = get_flushed_context(sink) + dimension_sets = context.get_dimensions() + assert len(dimension_sets) == 1 + dimensions = dimension_sets[0] + assert len(dimensions) == 4 + assert dimensions[expected_key] == expected_value + + @pytest.mark.asyncio async def test_can_set_namespace(mocker): # arrange @@ -316,6 +386,59 @@ async def test_context_is_preserved_across_flushes(mocker): assert context.metrics[metric_key].values == [1] +@pytest.mark.asyncio +async def test_flush_dont_preserve_dimensions_by_default(mocker): + # arrange + dimension_key = "Dim" + dimension_value = "Value" + + logger, sink, env = get_logger_and_sink(mocker) + + logger.set_dimensions({dimension_key: dimension_value}) + + # act + await logger.flush() + + context = sink.accept.call_args[0][0] + dimensions = context.get_dimensions() + assert len(dimensions) == 1 + assert dimensions[0][dimension_key] == dimension_value + + await logger.flush() + + context = sink.accept.call_args[0][0] + dimensions = context.get_dimensions() + assert len(dimensions) == 1 + assert dimension_key not in dimensions[0] + + +@pytest.mark.asyncio +async def test_configure_flush_to_preserve_dimensions(mocker): + # arrange + dimension_key = "Dim" + dimension_value = "Value" + + logger, sink, env = get_logger_and_sink(mocker) + + logger.set_dimensions({dimension_key: dimension_value}) + logger.flush_preserve_dimensions = True + + # act + await logger.flush() + + context = sink.accept.call_args[0][0] + dimensions = context.get_dimensions() + assert len(dimensions) == 1 + assert dimensions[0][dimension_key] == dimension_value + + await logger.flush() + + context = sink.accept.call_args[0][0] + dimensions = context.get_dimensions() + assert len(dimensions) == 1 + assert dimensions[0][dimension_key] == dimension_value + + # Test helper methods From 2bc5172dff0f387a8de5ef7aa825a0a5f9f419a4 Mon Sep 17 00:00:00 2001 From: Stephen-Bao Date: Mon, 22 Aug 2022 16:00:32 -0400 Subject: [PATCH 2/6] Adjusted some doc to make it more clear --- README.md | 7 +++---- aws_embedded_metrics/logger/metrics_context.py | 8 +++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f361ab9..ac0f45a 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ set_dimensions( ```py set_dimensions( { "Operation": "Aggregator" }, - use_default = True # default dimensions would be enabled + use_default=True # default dimensions would be enabled ) ``` @@ -166,9 +166,8 @@ set_namespace("MyApplication") - **flush**() -Flushes the current MetricsContext to the configured sink and resets all properties, dimensions and metric values. The namespace and default dimensions will be preserved across flushes. - -The default behavior is to clear all custom dimensions (dimensions added by `put_dimension`) across each flush(), but this can be configured by invoking `logger.flush_preserve_dimensions = True`. +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. +Custom dimensions are **not** preserved by default, but this behavior can be changed by invoking `logger.flush_preserve_dimensions = True`, so that custom dimensions would be preserved after each flushing thereafter. Example: diff --git a/aws_embedded_metrics/logger/metrics_context.py b/aws_embedded_metrics/logger/metrics_context.py index 11f1693..32cef95 100644 --- a/aws_embedded_metrics/logger/metrics_context.py +++ b/aws_embedded_metrics/logger/metrics_context.py @@ -173,10 +173,12 @@ def create_copy_with_context(self) -> "MetricsContext": ) def create_copy_with_context_with_dimensions(self) -> "MetricsContext": - # dimensions added with put_dimension will be copied. - # this helps reuse of dimension sets. + """ + Creates a deep copy of the context excluding metrics. + Dimensions added with put_dimension will be copied, this helps reuse of dimension sets. + """ new_context = self.create_copy_with_context() - new_context.dimensions = self.dimensions + new_context.dimensions.extend(self.dimensions) return new_context From ba7c54caded1e678bb9e7488c2dad40f7cd0ed15 Mon Sep 17 00:00:00 2001 From: Stephen-Bao Date: Tue, 23 Aug 2022 17:14:11 -0400 Subject: [PATCH 3/6] Adjusted some comments --- aws_embedded_metrics/logger/metrics_context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_embedded_metrics/logger/metrics_context.py b/aws_embedded_metrics/logger/metrics_context.py index ba3c394..a046d3d 100644 --- a/aws_embedded_metrics/logger/metrics_context.py +++ b/aws_embedded_metrics/logger/metrics_context.py @@ -159,7 +159,7 @@ def create_copy_with_context(self) -> "MetricsContext": new_properties: Dict = {} new_properties.update(self.properties) - # dimensions added with put_dimension will not be copied. + # custom dimensions will not be copied. # the reason for this is so that you can flush the same scope multiple # times without stacking new dimensions. Example: # @@ -181,7 +181,7 @@ def create_copy_with_context(self) -> "MetricsContext": def create_copy_with_context_with_dimensions(self) -> "MetricsContext": """ Creates a deep copy of the context excluding metrics. - Dimensions added with put_dimension will be copied, this helps reuse of dimension sets. + Custom dimensions will be copied, this helps reuse of dimension sets. """ new_context = self.create_copy_with_context() new_context.dimensions.extend(self.dimensions) From 47f86689134676d3f66d90008e344be878e89165 Mon Sep 17 00:00:00 2001 From: Stephen-Bao Date: Wed, 24 Aug 2022 14:47:21 -0400 Subject: [PATCH 4/6] Adjust one more comment --- aws_embedded_metrics/logger/metrics_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_embedded_metrics/logger/metrics_context.py b/aws_embedded_metrics/logger/metrics_context.py index a046d3d..f3459d4 100644 --- a/aws_embedded_metrics/logger/metrics_context.py +++ b/aws_embedded_metrics/logger/metrics_context.py @@ -181,7 +181,7 @@ def create_copy_with_context(self) -> "MetricsContext": def create_copy_with_context_with_dimensions(self) -> "MetricsContext": """ Creates a deep copy of the context excluding metrics. - Custom dimensions will be copied, this helps reuse of dimension sets. + Custom dimensions will be copied, this helps with the reuse of dimension sets. """ new_context = self.create_copy_with_context() new_context.dimensions.extend(self.dimensions) From 41c7de14ead980407d6db02ecb35c1479aba610d Mon Sep 17 00:00:00 2001 From: Xinyu Bao <71293855+Stephen-Bao@users.noreply.github.com> Date: Fri, 26 Aug 2022 09:39:47 -0400 Subject: [PATCH 5/6] Update README.md Co-authored-by: Himtanaya Bhadada --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac0f45a..5dc145c 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ logger.flush() # only default dimensions will be preserved after each flush() ``` ```py -logger.flush_preserve_dimensions(True) +logger.flush_preserve_dimensions = True logger.flush() # custom dimensions and default dimensions will be preserved after each flush() ``` From 47ece133e6de15107f0e34e3107d065c01f1199e Mon Sep 17 00:00:00 2001 From: Mark Kuhn Date: Fri, 26 Aug 2022 10:05:04 -0700 Subject: [PATCH 6/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5dc145c..699c368 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ set_dimensions( - **reset_dimensions**(use_default: bool) -> MetricsLogger -Explicitly clear all custom dimensions. The behavior of whether default dimensions should be used can be configured by `use_default` parameter. +Explicitly clear all custom dimensions. The behavior of whether default dimensions should be used can be configured with the `use_default` parameter. Examples: