From 557c133f1526a53e9d77aff3def647e992f48c6c Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Wed, 23 Jul 2025 11:22:01 +0100 Subject: [PATCH 1/2] Added runtime validation for the metric name --- aws_lambda_powertools/metrics/base.py | 15 +++++++- .../provider/cloudwatch_emf/cloudwatch.py | 13 ++++++- .../provider/cloudwatch_emf/constants.py | 2 ++ .../provider/cloudwatch_emf/exceptions.py | 6 ++++ .../test_metrics_cloudwatch_emf.py | 35 +++++++++++++++++++ 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/metrics/base.py b/aws_lambda_powertools/metrics/base.py index 0465d54cc6e..1d5c0e48570 100644 --- a/aws_lambda_powertools/metrics/base.py +++ b/aws_lambda_powertools/metrics/base.py @@ -25,7 +25,13 @@ ) from aws_lambda_powertools.metrics.functions import convert_timestamp_to_emf_format, validate_emf_timestamp from aws_lambda_powertools.metrics.provider import cold_start -from aws_lambda_powertools.metrics.provider.cloudwatch_emf.constants import MAX_DIMENSIONS, MAX_METRICS +from aws_lambda_powertools.metrics.provider.cloudwatch_emf.constants import ( + MAX_DIMENSIONS, + MAX_METRIC_NAME_LENGTH, + MAX_METRICS, + MIN_METRIC_NAME_LENGTH, +) +from aws_lambda_powertools.metrics.provider.cloudwatch_emf.exceptions import MetricNameError from aws_lambda_powertools.metrics.provider.cloudwatch_emf.metric_properties import MetricResolution, MetricUnit from aws_lambda_powertools.metrics.provider.cold_start import ( reset_cold_start_flag, # noqa: F401 # backwards compatibility @@ -129,11 +135,18 @@ def add_metric( Raises ------ + MetricNameError + When metric name does not fall under Cloudwatch constraints MetricUnitError When metric unit is not supported by CloudWatch MetricResolutionError When metric resolution is not supported by CloudWatch """ + name = name.strip() + if len(name) < MIN_METRIC_NAME_LENGTH or len(name) > MAX_METRIC_NAME_LENGTH: + raise MetricNameError( + f"The metric name should be between {MIN_METRIC_NAME_LENGTH} and {MAX_METRIC_NAME_LENGTH} characters", + ) if not isinstance(value, numbers.Number): raise MetricValueError(f"{value} is not a valid number") diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py index 5b79d55ab2b..f84e1b0ff42 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py @@ -20,7 +20,13 @@ validate_emf_timestamp, ) from aws_lambda_powertools.metrics.provider.base import BaseProvider -from aws_lambda_powertools.metrics.provider.cloudwatch_emf.constants import MAX_DIMENSIONS, MAX_METRICS +from aws_lambda_powertools.metrics.provider.cloudwatch_emf.constants import ( + MAX_DIMENSIONS, + MAX_METRIC_NAME_LENGTH, + MAX_METRICS, + MIN_METRIC_NAME_LENGTH, +) +from aws_lambda_powertools.metrics.provider.cloudwatch_emf.exceptions import MetricNameError from aws_lambda_powertools.metrics.provider.cloudwatch_emf.metric_properties import MetricResolution, MetricUnit from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.functions import resolve_env_var_choice @@ -137,6 +143,11 @@ def add_metric( When metric resolution is not supported by CloudWatch """ + name = name.strip() + if len(name) < MIN_METRIC_NAME_LENGTH or len(name) > MAX_METRIC_NAME_LENGTH: + raise MetricNameError( + f"The metric name should be between {MIN_METRIC_NAME_LENGTH} and {MAX_METRIC_NAME_LENGTH} characters", + ) if not isinstance(value, numbers.Number): raise MetricValueError(f"{value} is not a valid number") diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/constants.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/constants.py index d8f5da0cec8..6c21eb47692 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/constants.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/constants.py @@ -1,2 +1,4 @@ MAX_DIMENSIONS = 29 MAX_METRICS = 100 +MIN_METRIC_NAME_LENGTH = 1 +MAX_METRIC_NAME_LENGTH = 255 diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/exceptions.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/exceptions.py index 6ac2d932ea7..a4256b84da2 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/exceptions.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/exceptions.py @@ -1,3 +1,9 @@ +class MetricNameError(Exception): + """When metric name does not fall under Cloudwatch constraints""" + + pass + + class MetricUnitError(Exception): """When metric unit is not supported by CloudWatch""" diff --git a/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py b/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py index 2e8a866ac10..9ab21116713 100644 --- a/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py +++ b/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py @@ -24,7 +24,10 @@ ) from aws_lambda_powertools.metrics.provider.cloudwatch_emf.constants import ( MAX_DIMENSIONS, + MAX_METRIC_NAME_LENGTH, + MIN_METRIC_NAME_LENGTH, ) +from aws_lambda_powertools.metrics.provider.cloudwatch_emf.exceptions import MetricNameError if TYPE_CHECKING: from aws_lambda_powertools.metrics.provider.cloudwatch_emf.types import ( @@ -443,6 +446,38 @@ def test_schema_validation_no_namespace(metric, dimension): my_metric.add_dimension(**dimension) +def test_schema_validation_empty_metric_name(metric, dimension, namespace): + # GIVEN we pass an empty metric name + metric["name"] = "" + + # WHEN we attempt to add a metric + # THEN it should fail validation and raise MetricNameError + with pytest.raises( + MetricNameError, + match=f"The metric name should be between {MIN_METRIC_NAME_LENGTH} and {MAX_METRIC_NAME_LENGTH} characters", + ): + with single_metric(**metric): + pass + + +def test_schema_validation_long_metric_name(metric, dimension, namespace): + # GIVEN we pass a metric name outside the maximum length constraint + metric[ + "name" + ] = """Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. + Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, + ultricies nec, pellentesque eu, pretium quis,.""" + + # WHEN we attempt to serialize a valid EMF object + # THEN it should fail validation and raise SchemaValidationError + with pytest.raises( + MetricNameError, + match=f"The metric name should be between {MIN_METRIC_NAME_LENGTH} and {MAX_METRIC_NAME_LENGTH} characters", + ): + with single_metric(**metric): + pass + + def test_schema_validation_incorrect_metric_value(metric, dimension, namespace): # GIVEN we pass an incorrect metric value (non-numeric) metric["value"] = "some_value" From bf7639c238b809a428820541a1c9c282756ccccb Mon Sep 17 00:00:00 2001 From: Swopnil Dangol Date: Wed, 23 Jul 2025 13:44:56 +0100 Subject: [PATCH 2/2] Updated the tests to test the EMFMetricProvider --- .../required_dependencies/test_metrics_cloudwatch_emf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py b/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py index 9ab21116713..e32d7f3a880 100644 --- a/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py +++ b/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py @@ -448,6 +448,7 @@ def test_schema_validation_no_namespace(metric, dimension): def test_schema_validation_empty_metric_name(metric, dimension, namespace): # GIVEN we pass an empty metric name + my_metrics = AmazonCloudWatchEMFProvider(namespace=namespace) metric["name"] = "" # WHEN we attempt to add a metric @@ -456,12 +457,12 @@ def test_schema_validation_empty_metric_name(metric, dimension, namespace): MetricNameError, match=f"The metric name should be between {MIN_METRIC_NAME_LENGTH} and {MAX_METRIC_NAME_LENGTH} characters", ): - with single_metric(**metric): - pass + my_metrics.add_metric(**metric) def test_schema_validation_long_metric_name(metric, dimension, namespace): # GIVEN we pass a metric name outside the maximum length constraint + my_metrics = AmazonCloudWatchEMFProvider(namespace=namespace) metric[ "name" ] = """Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. @@ -474,8 +475,7 @@ def test_schema_validation_long_metric_name(metric, dimension, namespace): MetricNameError, match=f"The metric name should be between {MIN_METRIC_NAME_LENGTH} and {MAX_METRIC_NAME_LENGTH} characters", ): - with single_metric(**metric): - pass + my_metrics.add_metric(**metric) def test_schema_validation_incorrect_metric_value(metric, dimension, namespace):