diff --git a/aws_embedded_metrics/constants.py b/aws_embedded_metrics/constants.py index 6f99bfa..0b975b7 100644 --- a/aws_embedded_metrics/constants.py +++ b/aws_embedded_metrics/constants.py @@ -12,6 +12,6 @@ # limitations under the License. DEFAULT_NAMESPACE = "aws-embedded-metrics" -MAX_DIMENSIONS = 9 +MAX_DIMENSION_SET_SIZE = 30 MAX_METRICS_PER_EVENT = 100 MAX_DATAPOINTS_PER_METRIC = 100 diff --git a/aws_embedded_metrics/exceptions.py b/aws_embedded_metrics/exceptions.py new file mode 100644 index 0000000..2747924 --- /dev/null +++ b/aws_embedded_metrics/exceptions.py @@ -0,0 +1,17 @@ +# Copyright 2022 Amazon.com, Inc. or its affiliates. +# Licensed under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class DimensionSetExceededError(Exception): + def __init__(self, message: str) -> None: + # Call the base class constructor with the parameters it needs + super().__init__(message) diff --git a/aws_embedded_metrics/logger/metrics_context.py b/aws_embedded_metrics/logger/metrics_context.py index 11a84ca..47ed1dd 100644 --- a/aws_embedded_metrics/logger/metrics_context.py +++ b/aws_embedded_metrics/logger/metrics_context.py @@ -14,6 +14,8 @@ from aws_embedded_metrics import constants, utils from aws_embedded_metrics.config import get_config +from aws_embedded_metrics.constants import MAX_DIMENSION_SET_SIZE +from aws_embedded_metrics.exceptions import DimensionSetExceededError from aws_embedded_metrics.logger.metric import Metric from typing import List, Dict, Any @@ -55,6 +57,15 @@ def put_metric(self, key: str, value: float, unit: str = None) -> None: else: self.metrics[key] = Metric(value, unit) + @staticmethod + def validate_dimension_set(dimensions: Dict[str, str]) -> None: + """ + Validates dimension set length is not more than MAX_DIMENSION_SET_SIZE + """ + if len(dimensions) > MAX_DIMENSION_SET_SIZE: + raise DimensionSetExceededError( + f"Maximum number of dimensions per dimension set allowed are {MAX_DIMENSION_SET_SIZE}") + def put_dimensions(self, dimensions: Dict[str, str]) -> None: """ Adds dimensions to the context. @@ -66,6 +77,8 @@ def put_dimensions(self, dimensions: Dict[str, str]) -> None: # TODO add ability to define failure strategy return + self.validate_dimension_set(dimensions) + self.dimensions.append(dimensions) def set_dimensions(self, dimensionSets: List[Dict[str, str]]) -> None: @@ -78,6 +91,10 @@ def set_dimensions(self, dimensionSets: List[Dict[str, str]]) -> None: ``` """ self.should_use_default_dimensions = False + + for dimensionSet in dimensionSets: + self.validate_dimension_set(dimensionSet) + self.dimensions = dimensionSets def set_default_dimensions(self, default_dimensions: Dict) -> None: diff --git a/aws_embedded_metrics/serializers/log_serializer.py b/aws_embedded_metrics/serializers/log_serializer.py index d5d7ec8..156e868 100644 --- a/aws_embedded_metrics/serializers/log_serializer.py +++ b/aws_embedded_metrics/serializers/log_serializer.py @@ -15,8 +15,9 @@ from aws_embedded_metrics.logger.metrics_context import MetricsContext from aws_embedded_metrics.serializers import Serializer from aws_embedded_metrics.constants import ( - MAX_DIMENSIONS, MAX_METRICS_PER_EVENT, MAX_DATAPOINTS_PER_METRIC + MAX_DIMENSION_SET_SIZE, MAX_METRICS_PER_EVENT, MAX_DATAPOINTS_PER_METRIC ) +from aws_embedded_metrics.exceptions import DimensionSetExceededError import json from typing import Any, Dict, List @@ -31,7 +32,11 @@ def serialize(context: MetricsContext) -> List[str]: for dimension_set in context.get_dimensions(): keys = list(dimension_set.keys()) - dimension_keys.append(keys[0:MAX_DIMENSIONS]) + if len(keys) > MAX_DIMENSION_SET_SIZE: + err_msg = (f"Maximum number of dimensions per dimension set allowed are {MAX_DIMENSION_SET_SIZE}. " + f"Account for default dimensions if not using set_dimensions.") + raise DimensionSetExceededError(err_msg) + dimension_keys.append(keys) dimensions_properties = {**dimensions_properties, **dimension_set} def create_body() -> Dict[str, Any]: diff --git a/setup.py b/setup.py index ab81d6a..5ae82a3 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="aws-embedded-metrics", - version="1.0.8", + version="2.0.0", author="Amazon Web Services", author_email="jarnance@amazon.com", description="AWS Embedded Metrics Package", diff --git a/tests/logger/test_metrics_context.py b/tests/logger/test_metrics_context.py index fde3d82..d1f8b0c 100644 --- a/tests/logger/test_metrics_context.py +++ b/tests/logger/test_metrics_context.py @@ -1,8 +1,10 @@ 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.exceptions import DimensionSetExceededError from importlib import reload from faker import Faker +import pytest fake = Faker() @@ -42,14 +44,14 @@ def test_put_dimension_adds_to_dimensions(): # arrange context = MetricsContext() - dimension_key = fake.word() - dimension_value = fake.word() + dimensions_to_add = 30 + dimension_set = generate_dimension_set(dimensions_to_add) # act - context.put_dimensions({dimension_key: dimension_value}) + context.put_dimensions(dimension_set) # assert - assert context.dimensions == [{dimension_key: dimension_value}] + assert context.dimensions == [dimension_set] def test_get_dimensions_returns_only_custom_dimensions_if_no_default_dimensions_not_set(): @@ -264,3 +266,32 @@ def test_create_copy_with_context_does_not_repeat_dimensions(): # assert assert len(new_context.get_dimensions()) == 1 + + +def test_cannot_set_more_than_30_dimensions(): + context = MetricsContext() + dimensions_to_add = 32 + dimension_set = generate_dimension_set(dimensions_to_add) + + with pytest.raises(DimensionSetExceededError): + context.set_dimensions([dimension_set]) + + +def test_cannot_put_more_than_30_dimensions(): + context = MetricsContext() + dimensions_to_add = 32 + dimension_set = generate_dimension_set(dimensions_to_add) + + with pytest.raises(DimensionSetExceededError): + context.put_dimensions(dimension_set) + + +# Test utility method + + +def generate_dimension_set(dimensions_to_add): + dimension_set = {} + for i in range(0, dimensions_to_add): + dimension_set[f"{i}"] = fake.word() + + return dimension_set diff --git a/tests/serializer/test_log_serializer.py b/tests/serializer/test_log_serializer.py index 314adb9..e892922 100644 --- a/tests/serializer/test_log_serializer.py +++ b/tests/serializer/test_log_serializer.py @@ -1,9 +1,11 @@ from aws_embedded_metrics.config import get_config +from aws_embedded_metrics.exceptions import DimensionSetExceededError from aws_embedded_metrics.logger.metrics_context import MetricsContext from aws_embedded_metrics.serializers.log_serializer import LogSerializer from collections import Counter from faker import Faker import json +import pytest fake = Faker() @@ -30,37 +32,6 @@ def test_serialize_dimensions(): assert_json_equality(result_json, expected) -def test_cannot_serialize_more_than_9_dimensions(): - # arrange - dimensions = {} - dimension_pointers = [] - allowed_dimensions = 9 - dimensions_to_add = 15 - - for i in range(0, dimensions_to_add): - print(i) - expected_key = f"{i}" - expected_value = fake.word() - dimensions[expected_key] = expected_value - dimension_pointers.append(expected_key) - - expected_dimensions_pointers = dimension_pointers[0:allowed_dimensions] - - expected = {**get_empty_payload(), **dimensions} - expected["_aws"]["CloudWatchMetrics"][0]["Dimensions"].append( - expected_dimensions_pointers - ) - - context = get_context() - context.put_dimensions(dimensions) - - # act - result_json = serializer.serialize(context)[0] - - # assert - assert_json_equality(result_json, expected) - - def test_serialize_properties(): # arrange expected_key = fake.word() @@ -79,6 +50,25 @@ def test_serialize_properties(): assert_json_equality(result_json, expected) +def test_default_and_custom_dimensions_combined_limit_exceeded(): + # While serializing default dimensions are added to the custom dimension set, + # and the combined size of the dimension set should not be more than 30 + dimensions = {} + default_dimension_key = fake.word() + default_dimension_value = fake.word() + custom_dimensions_to_add = 30 + + for i in range(0, custom_dimensions_to_add): + dimensions[f"{i}"] = fake.word() + + context = get_context() + context.set_default_dimensions({default_dimension_key: default_dimension_value}) + context.put_dimensions(dimensions) + + with pytest.raises(DimensionSetExceededError): + serializer.serialize(context) + + def test_serialize_metrics(): # arrange expected_key = fake.word()