Skip to content

Commit c3ea4ed

Browse files
Stephen-BaoStephen-BaoMark KuhnHimtanaya
authored
Support clear custom dimensions and related enhancements (#89)
* Support clear custom dimensions and related enhancements * Adjusted some doc to make it more clear * Adjusted some comments * Adjust one more comment * Update README.md Co-authored-by: Himtanaya Bhadada <[email protected]> * Update README.md Co-authored-by: Stephen-Bao <[email protected]> Co-authored-by: Mark Kuhn <[email protected]> Co-authored-by: Himtanaya Bhadada <[email protected]>
1 parent 7e79b4c commit c3ea4ed

File tree

4 files changed

+192
-9
lines changed

4 files changed

+192
-9
lines changed

README.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ put_dimensions({ "Operation": "Aggregator" })
110110
put_dimensions({ "Operation": "Aggregator", "DeviceType": "Actuator" })
111111
```
112112

113-
- **set_dimensions**(\*dimensions: Dict[str, str]) -> MetricsLogger
113+
- **set_dimensions**(\*dimensions: Dict[str, str], use_default: bool = False) -> MetricsLogger
114114

115-
Explicitly override all dimensions. This will remove the default dimensions.
115+
Explicitly override all dimensions. By default, this will disable the default dimensions, but can be configured using the *keyword-only* parameter `use_default`.
116116

117117
**WARNING**: Every distinct value will result in a new CloudWatch Metric.
118118
If the cardinality of a particular value is expected to be high, you should consider
@@ -132,6 +132,23 @@ set_dimensions(
132132
)
133133
```
134134

135+
```py
136+
set_dimensions(
137+
{ "Operation": "Aggregator" },
138+
use_default=True # default dimensions would be enabled
139+
)
140+
```
141+
142+
- **reset_dimensions**(use_default: bool) -> MetricsLogger
143+
144+
Explicitly clear all custom dimensions. The behavior of whether default dimensions should be used can be configured with the `use_default` parameter.
145+
146+
Examples:
147+
148+
```py
149+
reset_dimensions(False) # this will clear all custom dimensions as well as disable default dimensions
150+
```
151+
135152
- **set_namespace**(value: str) -> MetricsLogger
136153

137154
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.
@@ -149,7 +166,24 @@ set_namespace("MyApplication")
149166

150167
- **flush**()
151168

152-
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.
169+
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.
170+
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.
171+
172+
Example:
173+
174+
```py
175+
logger.flush() # only default dimensions will be preserved after each flush()
176+
```
177+
178+
```py
179+
logger.flush_preserve_dimensions = True
180+
logger.flush() # custom dimensions and default dimensions will be preserved after each flush()
181+
```
182+
183+
```py
184+
logger.reset_dimensions(False)
185+
logger.flush() # default dimensions are disabled; no dimensions will be preserved after each flush()
186+
```
153187

154188
### Configuration
155189

aws_embedded_metrics/logger/metrics_context.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def put_dimensions(self, dimension_set: Dict[str, str]) -> None:
8787

8888
self.dimensions.append(dimension_set)
8989

90-
def set_dimensions(self, dimension_sets: List[Dict[str, str]]) -> None:
90+
def set_dimensions(self, dimension_sets: List[Dict[str, str]], use_default: bool = False) -> None:
9191
"""
9292
Overwrite all dimensions.
9393
```
@@ -96,7 +96,7 @@ def set_dimensions(self, dimension_sets: List[Dict[str, str]]) -> None:
9696
{ "k1": "v1", "k2": "v2" }])
9797
```
9898
"""
99-
self.should_use_default_dimensions = False
99+
self.should_use_default_dimensions = use_default
100100

101101
for dimension_set in dimension_sets:
102102
self.validate_dimension_set(dimension_set)
@@ -114,6 +114,16 @@ def set_default_dimensions(self, default_dimensions: Dict) -> None:
114114
"""
115115
self.default_dimensions = default_dimensions
116116

117+
def reset_dimensions(self, use_default: bool) -> None:
118+
"""
119+
Clear all custom dimensions on this MetricsLogger instance. Whether default dimensions should
120+
be used can be configured by the input parameter.
121+
:param use_default: indicates whether default dimensions should be used
122+
"""
123+
new_dimensions: List[Dict] = []
124+
self.dimensions = new_dimensions
125+
self.should_use_default_dimensions = use_default
126+
117127
def set_property(self, key: str, value: Any) -> None:
118128
self.properties[key] = value
119129

@@ -149,7 +159,7 @@ def create_copy_with_context(self) -> "MetricsContext":
149159
new_properties: Dict = {}
150160
new_properties.update(self.properties)
151161

152-
# dimensions added with put_dimension will not be copied.
162+
# custom dimensions will not be copied.
153163
# the reason for this is so that you can flush the same scope multiple
154164
# times without stacking new dimensions. Example:
155165
#
@@ -168,6 +178,16 @@ def create_copy_with_context(self) -> "MetricsContext":
168178
self.namespace, new_properties, new_dimensions, new_default_dimensions
169179
)
170180

181+
def create_copy_with_context_with_dimensions(self) -> "MetricsContext":
182+
"""
183+
Creates a deep copy of the context excluding metrics.
184+
Custom dimensions will be copied, this helps with the reuse of dimension sets.
185+
"""
186+
new_context = self.create_copy_with_context()
187+
new_context.dimensions.extend(self.dimensions)
188+
189+
return new_context
190+
171191
@staticmethod
172192
def empty() -> "MetricsContext":
173193
return MetricsContext()

aws_embedded_metrics/logger/metrics_logger.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def __init__(
2929
):
3030
self.resolve_environment = resolve_environment
3131
self.context: MetricsContext = context or MetricsContext.empty()
32+
self.flush_preserve_dimensions: bool = False
3233

3334
async def flush(self) -> None:
3435
# resolve the environment and get the sink
@@ -42,7 +43,8 @@ async def flush(self) -> None:
4243

4344
# accept and reset the context
4445
sink.accept(self.context)
45-
self.context = self.context.create_copy_with_context()
46+
self.context = self.context.create_copy_with_context() if not self.flush_preserve_dimensions \
47+
else self.context.create_copy_with_context_with_dimensions()
4648

4749
def __configure_context_for_environment(self, env: Environment) -> None:
4850
default_dimensions = {
@@ -63,8 +65,12 @@ def put_dimensions(self, dimensions: Dict[str, str]) -> "MetricsLogger":
6365
self.context.put_dimensions(dimensions)
6466
return self
6567

66-
def set_dimensions(self, *dimensions: Dict[str, str]) -> "MetricsLogger":
67-
self.context.set_dimensions(list(dimensions))
68+
def set_dimensions(self, *dimensions: Dict[str, str], use_default: bool = False) -> "MetricsLogger":
69+
self.context.set_dimensions(list(dimensions), use_default)
70+
return self
71+
72+
def reset_dimensions(self, use_default: bool) -> "MetricsLogger":
73+
self.context.reset_dimensions(use_default)
6874
return self
6975

7076
def set_namespace(self, namespace: str) -> "MetricsLogger":

tests/logger/test_metrics_logger.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,50 @@ async def test_put_dimension(mocker):
191191
assert dimensions[0][expected_key] == expected_value
192192

193193

194+
@pytest.mark.asyncio
195+
async def test_reset_dimension_with_default_dimension(mocker):
196+
# arrange
197+
pair1 = ["key1", "val1"]
198+
pair2 = ["key2", "val2"]
199+
200+
logger, sink, env = get_logger_and_sink(mocker)
201+
202+
# act
203+
logger.put_dimensions({pair1[0]: pair1[1]})
204+
logger.reset_dimensions(True)
205+
logger.put_dimensions({pair2[0]: pair2[1]})
206+
await logger.flush()
207+
208+
# assert
209+
context = get_flushed_context(sink)
210+
dimensions = context.get_dimensions()
211+
assert len(dimensions) == 1
212+
assert len(dimensions[0]) == 4
213+
assert dimensions[0][pair2[0]] == pair2[1]
214+
215+
216+
@pytest.mark.asyncio
217+
async def test_reset_dimension_without_default_dimension(mocker):
218+
# arrange
219+
pair1 = ["key1", "val1"]
220+
pair2 = ["key2", "val2"]
221+
222+
logger, sink, env = get_logger_and_sink(mocker)
223+
224+
# act
225+
logger.put_dimensions({pair1[0]: pair1[1]})
226+
logger.reset_dimensions(False)
227+
logger.put_dimensions({pair2[0]: pair2[1]})
228+
await logger.flush()
229+
230+
# assert
231+
context = get_flushed_context(sink)
232+
dimensions = context.get_dimensions()
233+
assert len(dimensions) == 1
234+
assert len(dimensions[0]) == 1
235+
assert dimensions[0][pair2[0]] == pair2[1]
236+
237+
194238
@pytest.mark.asyncio
195239
async def test_logger_configures_default_dimensions_on_flush(before, mocker):
196240
# arrange
@@ -267,6 +311,32 @@ async def test_set_dimensions_overrides_all_dimensions(mocker):
267311
assert dimensions[expected_key] == expected_value
268312

269313

314+
@pytest.mark.asyncio
315+
async def test_configure_set_dimensions_to_preserve_default_dimensions(mocker):
316+
# arrange
317+
logger, sink, env = get_logger_and_sink(mocker)
318+
319+
# setup the typical default dimensions
320+
env.get_log_group_name.return_value = fake.word()
321+
env.get_name.return_value = fake.word()
322+
env.get_type.return_value = fake.word()
323+
324+
expected_key = fake.word()
325+
expected_value = fake.word()
326+
327+
# act
328+
logger.set_dimensions({expected_key: expected_value}, use_default=True)
329+
await logger.flush()
330+
331+
# assert
332+
context = get_flushed_context(sink)
333+
dimension_sets = context.get_dimensions()
334+
assert len(dimension_sets) == 1
335+
dimensions = dimension_sets[0]
336+
assert len(dimensions) == 4
337+
assert dimensions[expected_key] == expected_value
338+
339+
270340
@pytest.mark.asyncio
271341
async def test_can_set_namespace(mocker):
272342
# arrange
@@ -316,6 +386,59 @@ async def test_context_is_preserved_across_flushes(mocker):
316386
assert context.metrics[metric_key].values == [1]
317387

318388

389+
@pytest.mark.asyncio
390+
async def test_flush_dont_preserve_dimensions_by_default(mocker):
391+
# arrange
392+
dimension_key = "Dim"
393+
dimension_value = "Value"
394+
395+
logger, sink, env = get_logger_and_sink(mocker)
396+
397+
logger.set_dimensions({dimension_key: dimension_value})
398+
399+
# act
400+
await logger.flush()
401+
402+
context = sink.accept.call_args[0][0]
403+
dimensions = context.get_dimensions()
404+
assert len(dimensions) == 1
405+
assert dimensions[0][dimension_key] == dimension_value
406+
407+
await logger.flush()
408+
409+
context = sink.accept.call_args[0][0]
410+
dimensions = context.get_dimensions()
411+
assert len(dimensions) == 1
412+
assert dimension_key not in dimensions[0]
413+
414+
415+
@pytest.mark.asyncio
416+
async def test_configure_flush_to_preserve_dimensions(mocker):
417+
# arrange
418+
dimension_key = "Dim"
419+
dimension_value = "Value"
420+
421+
logger, sink, env = get_logger_and_sink(mocker)
422+
423+
logger.set_dimensions({dimension_key: dimension_value})
424+
logger.flush_preserve_dimensions = True
425+
426+
# act
427+
await logger.flush()
428+
429+
context = sink.accept.call_args[0][0]
430+
dimensions = context.get_dimensions()
431+
assert len(dimensions) == 1
432+
assert dimensions[0][dimension_key] == dimension_value
433+
434+
await logger.flush()
435+
436+
context = sink.accept.call_args[0][0]
437+
dimensions = context.get_dimensions()
438+
assert len(dimensions) == 1
439+
assert dimensions[0][dimension_key] == dimension_value
440+
441+
319442
# Test helper methods
320443

321444

0 commit comments

Comments
 (0)