From 8e8a56acb2abf709b776a186d2216151d2ba9f57 Mon Sep 17 00:00:00 2001 From: Eric Nielsen <4120606+ericbn@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:11:34 -0500 Subject: [PATCH 1/2] fix(metrics): warnings when there are default_dimensions This fixes and reorganizes the code in a few ways: * Make it clearer that _metrics, _dimensions, _metadata and _default_dimensions are class attributes. They don't need to be also set as instance attributes when constructing Metrics, as they're only meant to be used to construct the provider with shared data. * Expose metric_set, dimension_set, metadata_set and default_dimensions as attributes just to keep backwards compatibility. Don't expose setters for these as previously setting them would have no side effect. Now users will get an error, which is a small breaking change but arguably for something they should never be doing anyway. * Fix setting the default_dimensions in AmazonCloudWatchEMFProvider, as `default_dimensions or {}` was setting a new dict instance when the given default_dimensions was empty and we want to share the given dict. --- aws_lambda_powertools/metrics/metrics.py | 38 ++++++++++--------- .../provider/cloudwatch_emf/cloudwatch.py | 5 ++- .../test_metrics_cloudwatch_emf.py | 22 +++++++++++ 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/aws_lambda_powertools/metrics/metrics.py b/aws_lambda_powertools/metrics/metrics.py index 2ed7b43c35e..3f6d05b6a1c 100644 --- a/aws_lambda_powertools/metrics/metrics.py +++ b/aws_lambda_powertools/metrics/metrics.py @@ -91,21 +91,14 @@ def __init__( provider: AmazonCloudWatchEMFProvider | None = None, function_name: str | None = None, ): - self.metric_set = self._metrics - self.metadata_set = self._metadata - self.default_dimensions = self._default_dimensions - self.dimension_set = self._dimensions - - self.dimension_set.update(**self._default_dimensions) - if provider is None: self.provider = AmazonCloudWatchEMFProvider( namespace=namespace, service=service, - metric_set=self.metric_set, - dimension_set=self.dimension_set, - metadata_set=self.metadata_set, - default_dimensions=self._default_dimensions, + metric_set=Metrics._metrics, + dimension_set=Metrics._dimensions, + metadata_set=Metrics._metadata, + default_dimensions=Metrics._default_dimensions, function_name=function_name, ) else: @@ -174,7 +167,6 @@ def log_metrics( ) def set_default_dimensions(self, **dimensions) -> None: - self.provider.set_default_dimensions(**dimensions) """Persist dimensions across Lambda invocations Parameters @@ -195,14 +187,10 @@ def set_default_dimensions(self, **dimensions) -> None: def lambda_handler(): return True """ - for name, value in dimensions.items(): - self.add_dimension(name, value) - - self.default_dimensions.update(**dimensions) + self.provider.set_default_dimensions(**dimensions) def clear_default_dimensions(self) -> None: self.provider.default_dimensions.clear() - self.default_dimensions.clear() def clear_metrics(self) -> None: self.provider.clear_metrics() @@ -227,6 +215,22 @@ def service(self): def service(self, service): self.provider.service = service + @property + def metric_set(self): + return self.provider.metric_set + + @property + def dimension_set(self): + return self.provider.dimension_set + + @property + def metadata_set(self): + return self.provider.metadata_set + + @property + def default_dimensions(self): + return self.provider.default_dimensions + # Maintenance: until v3, we can't afford to break customers. # AmazonCloudWatchEMFProvider has the exact same functionality (non-singleton) diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py index 243fc561593..7805dca3513 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py @@ -87,7 +87,7 @@ def __init__( ): self.metric_set = metric_set if metric_set is not None else {} self.dimension_set = dimension_set if dimension_set is not None else {} - self.default_dimensions = default_dimensions or {} + self.default_dimensions = default_dimensions if default_dimensions is not None else {} self.namespace = resolve_env_var_choice(choice=namespace, env=os.getenv(constants.METRICS_NAMESPACE_ENV)) self.service = resolve_env_var_choice(choice=service, env=os.getenv(constants.SERVICE_NAME_ENV)) self.function_name = function_name @@ -453,7 +453,8 @@ def clear_metrics(self) -> None: self.dimension_set.clear() self.dimension_sets.clear() self.metadata_set.clear() - self.set_default_dimensions(**self.default_dimensions) + # Initialize dimension_set as in __init__ + self.dimension_set.update(**self.default_dimensions) def flush_metrics(self, raise_on_empty_metrics: bool = False) -> None: """Manually flushes the metrics. This is normally not necessary, 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 834575e4754..d965f5cf1f4 100644 --- a/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py +++ b/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py @@ -1221,6 +1221,28 @@ def lambda_handler(evt, ctx): assert "environment" in second_invocation +def test_flush_metrics_with_default_dimensions(capsys, metrics, dimensions, namespace): + # GIVEN a Metrics is initialized + my_metrics = Metrics(namespace=namespace) + my_metrics.set_default_dimensions(environment="test", log_group="/lambda/test") + + # WHEN we add_metric and flush_metrics + # THEN we should have no warnings + with warnings.catch_warnings(record=True) as w: + for metric in metrics: + my_metrics.add_metric(**metric) + assert not w + + with warnings.catch_warnings(record=True) as w: + my_metrics.flush_metrics() + assert not w + + # THEN we should have default dimensions in output + output = capture_metrics_output(capsys) + assert "environment" in output + assert "log_group" in output + + def test_metrics_reuse_dimension_set(metric, dimension, namespace): # GIVEN Metrics is initialized with a metric and dimension my_metrics = Metrics(namespace=namespace) From 11ca80ae39f9000ee1a950396d0348254c64c307 Mon Sep 17 00:00:00 2001 From: Eric Nielsen <4120606+ericbn@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:15:53 -0500 Subject: [PATCH 2/2] squash! fix(metrics): warnings when there are default_dimensions * Property cast dimension value to str in all code that updates dimension_set. * No need to check if `isinstance(value, str)` before casting to str. This is already optimized in CPython, which reuses the same instance if value is already an str. This is unnecessary overengineering. --- aws_lambda_powertools/metrics/base.py | 13 ++++--------- .../provider/cloudwatch_emf/cloudwatch.py | 19 ++++++++----------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/aws_lambda_powertools/metrics/base.py b/aws_lambda_powertools/metrics/base.py index 65e47cd5061..22456f6925c 100644 --- a/aws_lambda_powertools/metrics/base.py +++ b/aws_lambda_powertools/metrics/base.py @@ -286,9 +286,8 @@ def add_dimension(self, name: str, value: str) -> None: f"Maximum number of dimensions exceeded ({MAX_DIMENSIONS}): Unable to add dimension {name}.", ) # Cast value to str according to EMF spec - # Majority of values are expected to be string already, so - # checking before casting improves performance in most cases - self.dimension_set[name] = value if isinstance(value, str) else str(value) + # Majority of values are expected to be string already + self.dimension_set[name] = str(value) def add_metadata(self, key: str, value: Any) -> None: """Adds high cardinal metadata for metrics object @@ -315,12 +314,8 @@ def add_metadata(self, key: str, value: Any) -> None: logger.debug(f"Adding metadata: {key}:{value}") # Cast key to str according to EMF spec - # Majority of keys are expected to be string already, so - # checking before casting improves performance in most cases - if isinstance(key, str): - self.metadata_set[key] = value - else: - self.metadata_set[str(key)] = value + # Majority of keys are expected to be string already + self.metadata_set[str(key)] = value def set_timestamp(self, timestamp: int | datetime.datetime): """ diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py index 7805dca3513..97e6e8c0ffb 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py @@ -100,7 +100,8 @@ def __init__( self._metric_unit_valid_options = list(MetricUnit.__members__) self._metric_resolutions = [resolution.value for resolution in MetricResolution] - self.dimension_set.update(**self.default_dimensions) + for name, value in self.default_dimensions.items(): + self.add_dimension(name, value) def add_metric( self, @@ -306,7 +307,7 @@ def add_dimension(self, name: str, value: str) -> None: f"Maximum number of dimensions exceeded ({MAX_DIMENSIONS}): Unable to add dimension {name}.", ) - value = value if isinstance(value, str) else str(value) + value = str(value) if not name.strip() or not value.strip(): warnings.warn( @@ -317,7 +318,7 @@ def add_dimension(self, name: str, value: str) -> None: ) return - if name in self.dimension_set or name in self.default_dimensions: + if name in self.dimension_set: warnings.warn( f"Dimension '{name}' has already been added. The previous value will be overwritten.", category=PowertoolsUserWarning, @@ -415,12 +416,8 @@ def add_metadata(self, key: str, value: Any) -> None: logger.debug(f"Adding metadata: {key}:{value}") # Cast key to str according to EMF spec - # Majority of keys are expected to be string already, so - # checking before casting improves performance in most cases - if isinstance(key, str): - self.metadata_set[key] = value - else: - self.metadata_set[str(key)] = value + # Majority of keys are expected to be string already + self.metadata_set[str(key)] = value def set_timestamp(self, timestamp: int | datetime.datetime): """ @@ -453,8 +450,8 @@ def clear_metrics(self) -> None: self.dimension_set.clear() self.dimension_sets.clear() self.metadata_set.clear() - # Initialize dimension_set as in __init__ - self.dimension_set.update(**self.default_dimensions) + for name, value in self.default_dimensions.items(): + self.add_dimension(name, value) def flush_metrics(self, raise_on_empty_metrics: bool = False) -> None: """Manually flushes the metrics. This is normally not necessary,