From e312a296c3705105d64fcc7b6cdfba0a0215dfe7 Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Thu, 6 Feb 2025 18:18:49 -0300 Subject: [PATCH 01/13] disable metrics env var cloudwatch --- .../provider/cloudwatch_emf/cloudwatch.py | 20 ++++++++++++++++++- aws_lambda_powertools/shared/constants.py | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py index 7a7db86c9c6..27f16f8d16e 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py @@ -21,7 +21,7 @@ from aws_lambda_powertools.metrics.provider.cloudwatch_emf.constants import MAX_DIMENSIONS, MAX_METRICS 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 +from aws_lambda_powertools.shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice from aws_lambda_powertools.warnings import PowertoolsUserWarning if TYPE_CHECKING: @@ -77,6 +77,8 @@ def __init__( self.default_dimensions = default_dimensions or {} 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.metrics_disabled = self.is_metrics_disabled() + self.metadata_set = metadata_set if metadata_set is not None else {} self.timestamp: int | None = None @@ -86,6 +88,14 @@ def __init__( self.dimension_set.update(**self.default_dimensions) + @staticmethod + def is_metrics_disabled() -> bool: + """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLE""" + is_disabled = resolve_truthy_env_var_choice(env=os.getenv(constants.METRICS_DISABLED_ENV, "false")) + if is_disabled: + logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") + return is_disabled + def add_metric( self, name: str, @@ -127,6 +137,8 @@ def add_metric( MetricResolutionError When metric resolution is not supported by CloudWatch """ + if self.metrics_disabled: + return if not isinstance(value, numbers.Number): raise MetricValueError(f"{value} is not a valid number") @@ -268,6 +280,8 @@ def add_dimension(self, name: str, value: str) -> None: value : str Dimension value """ + if self.metrics_disabled: + return logger.debug(f"Adding dimension: {name}:{value}") if len(self.dimension_set) == MAX_DIMENSIONS: raise SchemaValidationError( @@ -316,6 +330,8 @@ def add_metadata(self, key: str, value: Any) -> None: value : any Metadata value """ + if self.metrics_disabled: + return logger.debug(f"Adding metadata: {key}:{value}") # Cast key to str according to EMF spec @@ -368,6 +384,8 @@ def flush_metrics(self, raise_on_empty_metrics: bool = False) -> None: raise_on_empty_metrics : bool, optional raise exception if no metrics are emitted, by default False """ + if self.metrics_disabled: + return if not raise_on_empty_metrics and not self.metric_set: warnings.warn( "No application metrics to publish. The cold-start metric may be published if enabled. " diff --git a/aws_lambda_powertools/shared/constants.py b/aws_lambda_powertools/shared/constants.py index 9652e09a0b2..199f37d99bb 100644 --- a/aws_lambda_powertools/shared/constants.py +++ b/aws_lambda_powertools/shared/constants.py @@ -40,6 +40,7 @@ METRICS_NAMESPACE_ENV: str = "POWERTOOLS_METRICS_NAMESPACE" DATADOG_FLUSH_TO_LOG: str = "DD_FLUSH_TO_LOG" SERVICE_NAME_ENV: str = "POWERTOOLS_SERVICE_NAME" +METRICS_DISABLED_ENV: str = "POWERTOOLS_METRICS_DISABLED" # If the timestamp of log event is more than 2 hours in future, the log event is skipped. # If the timestamp of log event is more than 14 days in past, the log event is skipped. # See https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AgentReference.html From 7cff451df864ed38b3d27553308277f02e5ed98e Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Thu, 6 Feb 2025 18:51:28 -0300 Subject: [PATCH 02/13] add for datadog --- .../metrics/provider/datadog/datadog.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/metrics/provider/datadog/datadog.py b/aws_lambda_powertools/metrics/provider/datadog/datadog.py index 0442af3f86c..7dafa58e531 100644 --- a/aws_lambda_powertools/metrics/provider/datadog/datadog.py +++ b/aws_lambda_powertools/metrics/provider/datadog/datadog.py @@ -13,7 +13,7 @@ from aws_lambda_powertools.metrics.provider import BaseProvider from aws_lambda_powertools.metrics.provider.datadog.warnings import DatadogDataValidationWarning from aws_lambda_powertools.shared import constants -from aws_lambda_powertools.shared.functions import resolve_env_var_choice +from aws_lambda_powertools.shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice if TYPE_CHECKING: from aws_lambda_powertools.shared.types import AnyCallableT @@ -65,6 +65,15 @@ def __init__( ) self.default_tags = default_tags or {} self.flush_to_log = resolve_env_var_choice(choice=flush_to_log, env=os.getenv(constants.DATADOG_FLUSH_TO_LOG)) + self.metrics_disabled = self.is_metrics_disabled() + + @staticmethod + def is_metrics_disabled() -> bool: + """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLE""" + is_disabled = resolve_truthy_env_var_choice(env=os.getenv(constants.METRICS_DISABLED_ENV, "false")) + if is_disabled: + logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") + return is_disabled # adding name,value,timestamp,tags def add_metric( @@ -99,7 +108,8 @@ def add_metric( >>> sales='sam' >>> ) """ - + if self.metrics_disabled: + return # validating metric name if not self._validate_datadog_metric_name(name): docs = "https://docs.datadoghq.com/metrics/custom_metrics/#naming-custom-metrics" @@ -180,6 +190,8 @@ def flush_metrics(self, raise_on_empty_metrics: bool = False) -> None: raise_on_empty_metrics : bool, optional raise exception if no metrics are emitted, by default False """ + if self.metrics_disabled: + return if not raise_on_empty_metrics and len(self.metric_set) == 0: warnings.warn( "No application metrics to publish. The cold-start metric may be published if enabled. " From 9e466c786385a5ac51702de2617d424de6f85dfe Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Thu, 6 Feb 2025 19:38:40 -0300 Subject: [PATCH 03/13] add tests --- .../test_metrics_cloudwatch_emf.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) 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 5633d573a54..890c51153d8 100644 --- a/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py +++ b/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py @@ -1329,3 +1329,86 @@ def lambda_handler(evt, ctx): "This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. " "Ensure the timestamp is within 14 days past or 2 hours future." ) + + +def test_metrics_disabled_with_env_var(monkeypatch): + # GIVEN environment variable is set to disable metrics + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") + + # WHEN metrics is initialized and adding metrics + metrics = Metrics() + metrics.add_metric(name="test_metric", unit="Count", value=1) + + # WHEN flushing metrics + metrics_output = metrics.flush_metrics() + + # THEN metrics output should be empty + assert metrics_output is None + + +def test_metrics_disabled_persists_after_flush(monkeypatch): + # GIVEN environment variable is set to disable metrics + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") + metrics = Metrics() + + # WHEN multiple operations are performed with flush in between + metrics.add_metric(name="metric1", unit="Count", value=1) + first_flush = metrics.flush_metrics() + + metrics.add_metric(name="metric2", unit="Count", value=2) + second_flush = metrics.flush_metrics() + + # THEN all flush operations should return None + assert first_flush is None + assert second_flush is None + + +def test_metrics_disabled_with_namespace_and_service(monkeypatch): + # GIVEN environment variable is set to disable metrics + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") + + # WHEN metrics is initialized with namespace and service + metrics = Metrics(namespace="test_namespace", service="test_service") + metrics.add_metric(name="test_metric", unit="Count", value=1) + metrics_output = metrics.flush_metrics() + + # THEN metrics should still be disabled + assert metrics_output is None + + +def test_metrics_enabled_with_env_var_false(monkeypatch, capsys): + # GIVEN environment variable is set to enable metrics + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "false") + + # WHEN metrics is initialized with namespace and metrics added + metrics = Metrics(namespace="test") + metrics.add_metric(name="test_metric", unit="Count", value=1) + metrics.flush_metrics() + + # THEN metrics should be written to stdout + output = capsys.readouterr().out + metrics_output = json.loads(output) + + assert "test_metric" in metrics_output + assert metrics_output["test_metric"] == [1.0] + assert metrics_output["_aws"]["CloudWatchMetrics"][0]["Namespace"] == "test" + assert metrics_output["_aws"]["CloudWatchMetrics"][0]["Metrics"][0]["Name"] == "test_metric" + + +def test_metrics_enabled_with_env_var_not_set(monkeypatch, capsys): + # GIVEN environment variable is not set + monkeypatch.delenv("POWERTOOLS_METRICS_DISABLED", raising=False) + + # WHEN metrics is initialized with namespace and metrics added + metrics = Metrics(namespace="test") + metrics.add_metric(name="test_metric", unit="Count", value=1) + metrics.flush_metrics() + + # THEN metrics should be written to stdout + output = capsys.readouterr().out + metrics_output = json.loads(output) + + assert "test_metric" in metrics_output + assert metrics_output["test_metric"] == [1.0] + assert metrics_output["_aws"]["CloudWatchMetrics"][0]["Namespace"] == "test" + assert metrics_output["_aws"]["CloudWatchMetrics"][0]["Metrics"][0]["Name"] == "test_metric" From d566972984453ba463004dc56209ee1c17a06103 Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Thu, 6 Feb 2025 20:14:02 -0300 Subject: [PATCH 04/13] acept none env var --- .../metrics/provider/cloudwatch_emf/cloudwatch.py | 10 +++++++--- .../metrics/provider/datadog/datadog.py | 9 ++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py index 27f16f8d16e..b1623052caa 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py @@ -90,11 +90,15 @@ def __init__( @staticmethod def is_metrics_disabled() -> bool: - """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLE""" - is_disabled = resolve_truthy_env_var_choice(env=os.getenv(constants.METRICS_DISABLED_ENV, "false")) + """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLED""" + if constants.METRICS_DISABLED_ENV not in os.environ: + return False + + is_disabled = resolve_truthy_env_var_choice(env=os.getenv(constants.METRICS_DISABLED_ENV)) if is_disabled: logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") - return is_disabled + return bool(is_disabled) + def add_metric( self, diff --git a/aws_lambda_powertools/metrics/provider/datadog/datadog.py b/aws_lambda_powertools/metrics/provider/datadog/datadog.py index 7dafa58e531..b3c0885da88 100644 --- a/aws_lambda_powertools/metrics/provider/datadog/datadog.py +++ b/aws_lambda_powertools/metrics/provider/datadog/datadog.py @@ -69,11 +69,14 @@ def __init__( @staticmethod def is_metrics_disabled() -> bool: - """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLE""" - is_disabled = resolve_truthy_env_var_choice(env=os.getenv(constants.METRICS_DISABLED_ENV, "false")) + """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLED""" + if constants.METRICS_DISABLED_ENV not in os.environ: + return False + + is_disabled = resolve_truthy_env_var_choice(env=os.getenv(constants.METRICS_DISABLED_ENV)) if is_disabled: logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") - return is_disabled + return bool(is_disabled) # adding name,value,timestamp,tags def add_metric( From 2ea6c19c4ed7403287cac9966de76dfcc216e761 Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Fri, 7 Feb 2025 11:05:41 -0300 Subject: [PATCH 05/13] fix is disable metrics --- .../metrics/provider/cloudwatch_emf/cloudwatch.py | 13 +++---------- .../metrics/provider/datadog/datadog.py | 7 ++----- .../test_metrics_cloudwatch_emf.py | 8 ++++---- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py index b1623052caa..1cf36c4fc36 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py @@ -93,13 +93,12 @@ def is_metrics_disabled() -> bool: """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLED""" if constants.METRICS_DISABLED_ENV not in os.environ: return False - + is_disabled = resolve_truthy_env_var_choice(env=os.getenv(constants.METRICS_DISABLED_ENV)) if is_disabled: logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") return bool(is_disabled) - def add_metric( self, name: str, @@ -141,8 +140,7 @@ def add_metric( MetricResolutionError When metric resolution is not supported by CloudWatch """ - if self.metrics_disabled: - return + if not isinstance(value, numbers.Number): raise MetricValueError(f"{value} is not a valid number") @@ -284,8 +282,7 @@ def add_dimension(self, name: str, value: str) -> None: value : str Dimension value """ - if self.metrics_disabled: - return + logger.debug(f"Adding dimension: {name}:{value}") if len(self.dimension_set) == MAX_DIMENSIONS: raise SchemaValidationError( @@ -334,8 +331,6 @@ def add_metadata(self, key: str, value: Any) -> None: value : any Metadata value """ - if self.metrics_disabled: - return logger.debug(f"Adding metadata: {key}:{value}") # Cast key to str according to EMF spec @@ -388,8 +383,6 @@ def flush_metrics(self, raise_on_empty_metrics: bool = False) -> None: raise_on_empty_metrics : bool, optional raise exception if no metrics are emitted, by default False """ - if self.metrics_disabled: - return if not raise_on_empty_metrics and not self.metric_set: warnings.warn( "No application metrics to publish. The cold-start metric may be published if enabled. " diff --git a/aws_lambda_powertools/metrics/provider/datadog/datadog.py b/aws_lambda_powertools/metrics/provider/datadog/datadog.py index b3c0885da88..e8118e26a50 100644 --- a/aws_lambda_powertools/metrics/provider/datadog/datadog.py +++ b/aws_lambda_powertools/metrics/provider/datadog/datadog.py @@ -72,7 +72,7 @@ def is_metrics_disabled() -> bool: """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLED""" if constants.METRICS_DISABLED_ENV not in os.environ: return False - + is_disabled = resolve_truthy_env_var_choice(env=os.getenv(constants.METRICS_DISABLED_ENV)) if is_disabled: logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") @@ -111,8 +111,6 @@ def add_metric( >>> sales='sam' >>> ) """ - if self.metrics_disabled: - return # validating metric name if not self._validate_datadog_metric_name(name): docs = "https://docs.datadoghq.com/metrics/custom_metrics/#naming-custom-metrics" @@ -193,8 +191,7 @@ def flush_metrics(self, raise_on_empty_metrics: bool = False) -> None: raise_on_empty_metrics : bool, optional raise exception if no metrics are emitted, by default False """ - if self.metrics_disabled: - return + if not raise_on_empty_metrics and len(self.metric_set) == 0: warnings.warn( "No application metrics to publish. The cold-start metric may be published if enabled. " 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 890c51153d8..cfb7db31dbc 100644 --- a/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py +++ b/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py @@ -1331,12 +1331,12 @@ def lambda_handler(evt, ctx): ) -def test_metrics_disabled_with_env_var(monkeypatch): +def test_metrics_disabled_with_env_var(monkeypatch, namespace): # GIVEN environment variable is set to disable metrics monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") # WHEN metrics is initialized and adding metrics - metrics = Metrics() + metrics = Metrics(namespace=namespace) metrics.add_metric(name="test_metric", unit="Count", value=1) # WHEN flushing metrics @@ -1346,10 +1346,10 @@ def test_metrics_disabled_with_env_var(monkeypatch): assert metrics_output is None -def test_metrics_disabled_persists_after_flush(monkeypatch): +def test_metrics_disabled_persists_after_flush(monkeypatch, namespace): # GIVEN environment variable is set to disable metrics monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") - metrics = Metrics() + metrics = Metrics(namespace=namespace) # WHEN multiple operations are performed with flush in between metrics.add_metric(name="metric1", unit="Count", value=1) From 7345c3483f6dd19a01c3058dcffd3954ef6fd016 Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Fri, 7 Feb 2025 12:01:13 -0300 Subject: [PATCH 06/13] add documentation --- aws_lambda_powertools/metrics/metrics.py | 2 ++ docs/core/metrics.md | 8 ++++++-- docs/core/metrics/datadog.md | 6 +++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/metrics/metrics.py b/aws_lambda_powertools/metrics/metrics.py index 0bcf54917ed..a6493f25516 100644 --- a/aws_lambda_powertools/metrics/metrics.py +++ b/aws_lambda_powertools/metrics/metrics.py @@ -47,6 +47,8 @@ def lambda_handler(): metric namespace POWERTOOLS_SERVICE_NAME : str service name used for default dimension + POWERTOOLS_METRICS_DISABLED: bool + Powertools metrics disabled (e.g. `"true", "True", "TRUE"`) Parameters ---------- diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 6fdcf1fa043..b4ae9357cbe 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -34,12 +34,16 @@ If you're new to Amazon CloudWatch, there are five terminologies you must be awa ???+ tip All examples shared in this documentation are available within the [project repository](https://github.com/aws-powertools/powertools-lambda-python/tree/develop/examples){target="_blank"}. -Metric has two global settings that will be used across all metrics emitted: +Metric has three global settings that will be used across all metrics emitted: | Setting | Description | Environment variable | Constructor parameter | | -------------------- | ------------------------------------------------------------------------------- | ------------------------------ | --------------------- | | **Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` | | **Service** | Optionally, sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `service` | +| **Disable Powertools Metrics** | Optionally, disables all Powertools metrics. | `POWERTOOLS_METRICS_DISABLED` | N/A | + +???+ warning + `POWERTOOLS_METRICS_DISABLED` will not disable default metrics created by AWS services. ???+ tip Use your application or main service as the metric namespace to easily group all metrics. @@ -79,7 +83,7 @@ You can create metrics using `add_metric`, and you can create dimensions for all CloudWatch EMF supports a max of 100 metrics per batch. Metrics utility will flush all metrics when adding the 100th metric. Subsequent metrics (101th+) will be aggregated into a new EMF object, for your convenience. ???+ warning "Warning: Do not create metrics or dimensions outside the handler" - Metrics or dimensions added in the global scope will only be added during cold start. Disregard if you that's the intended behavior. + Metrics or dimensions added in the global scope will only be added during cold start. Disregard if that's the intended behavior. ### Adding high-resolution metrics diff --git a/docs/core/metrics/datadog.md b/docs/core/metrics/datadog.md index 3cf38e1c425..bfde98315dd 100644 --- a/docs/core/metrics/datadog.md +++ b/docs/core/metrics/datadog.md @@ -23,7 +23,7 @@ stateDiagram-v2 DatadogExtension --> Datadog: async state LambdaExtension { - DatadogExtension + DatadogExtension } ``` @@ -178,6 +178,10 @@ You can use any of the following environment variables to configure `DatadogMetr | -------------------- | -------------------------------------------------------------------------------- | ------------------------------ | --------------------- | | **Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` | | **Flush to log** | Use this when you want to flush metrics to be exported through Datadog Forwarder | `DD_FLUSH_TO_LOG` | `flush_to_log` | +| **Disable Powertools Metrics** | Optionally, disables all Powertools metrics. | `POWERTOOLS_METRICS_DISABLED` | N/A | + +???+ warning + `POWERTOOLS_METRICS_DISABLED` will not disable default metrics created by AWS services. ## Advanced From 2046d5788ebc318f0a5841ce576952a2caf0fdd7 Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Fri, 7 Feb 2025 13:00:41 -0300 Subject: [PATCH 07/13] fix mypy --- .../metrics/provider/cloudwatch_emf/cloudwatch.py | 3 ++- aws_lambda_powertools/metrics/provider/datadog/datadog.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py index 1cf36c4fc36..76150240145 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py @@ -94,7 +94,8 @@ def is_metrics_disabled() -> bool: if constants.METRICS_DISABLED_ENV not in os.environ: return False - is_disabled = resolve_truthy_env_var_choice(env=os.getenv(constants.METRICS_DISABLED_ENV)) + env_value = os.getenv(constants.METRICS_DISABLED_ENV) + is_disabled = resolve_truthy_env_var_choice(env=env_value or "false") if is_disabled: logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") return bool(is_disabled) diff --git a/aws_lambda_powertools/metrics/provider/datadog/datadog.py b/aws_lambda_powertools/metrics/provider/datadog/datadog.py index e8118e26a50..776c27f9eb0 100644 --- a/aws_lambda_powertools/metrics/provider/datadog/datadog.py +++ b/aws_lambda_powertools/metrics/provider/datadog/datadog.py @@ -73,7 +73,8 @@ def is_metrics_disabled() -> bool: if constants.METRICS_DISABLED_ENV not in os.environ: return False - is_disabled = resolve_truthy_env_var_choice(env=os.getenv(constants.METRICS_DISABLED_ENV)) + env_value = os.getenv(constants.METRICS_DISABLED_ENV) + is_disabled = resolve_truthy_env_var_choice(env=env_value or "false") if is_disabled: logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") return bool(is_disabled) From fd6c803414b859ae9f6387dc01a5dc0c83dd98aa Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Fri, 7 Feb 2025 13:29:29 -0300 Subject: [PATCH 08/13] add datadog tests --- .../metrics/datadog/test_metrics_datadog.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/functional/metrics/datadog/test_metrics_datadog.py b/tests/functional/metrics/datadog/test_metrics_datadog.py index 2626b8755c6..0ca4f522094 100644 --- a/tests/functional/metrics/datadog/test_metrics_datadog.py +++ b/tests/functional/metrics/datadog/test_metrics_datadog.py @@ -334,3 +334,81 @@ def test_namespace_env_var(monkeypatch): # THEN namespace should match the explicitly passed variable and not the env var assert output[0]["m"] == f"{env_namespace}.item_sold" + + +#################### +def test_metrics_disabled_with_env_var(monkeypatch): + # GIVEN environment variable is set to disable metrics + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") + + # WHEN metrics is initialized and adding metrics + metrics = DatadogMetrics() + metrics.add_metric(name="test_metric", value=1) + + # WHEN flushing metrics + metrics_output = metrics.flush_metrics() + + # THEN metrics output should be empty + assert metrics_output is None + + +def test_metrics_disabled_persists_after_flush(monkeypatch): + # GIVEN environment variable is set to disable metrics + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") + metrics = DatadogMetrics() + + # WHEN multiple operations are performed with flush in between + metrics.add_metric(name="metric1", unit="Count", value=1) + first_flush = metrics.flush_metrics() + + metrics.add_metric(name="metric2", unit="Count", value=2) + second_flush = metrics.flush_metrics() + + # THEN all flush operations should return None + assert first_flush is None + assert second_flush is None + + +def test_metrics_disabled_with_namespace(monkeypatch): + # GIVEN environment variable is set to disable metrics + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") + + # WHEN metrics is initialized with namespace and service + metrics = DatadogMetrics(namespace="test_namespace") + metrics.add_metric(name="test_metric", value=1) + metrics_output = metrics.flush_metrics() + + # THEN metrics should still be disabled + assert metrics_output is None + + +def test_metrics_enabled_with_env_var_false(monkeypatch, capsys): + # GIVEN environment variable is set to enable metrics + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "false") + + # WHEN metrics is initialized with namespace and metrics added + metrics = DatadogMetrics(namespace="test") + metrics.add_metric(name="test_metric", value=1) + metrics.flush_metrics() + + # THEN Datadog metrics should be written to stdout + output = capsys.readouterr().out + metrics_output = json.loads(output) + + assert metrics_output + + +def test_metrics_enabled_with_env_var_not_set(monkeypatch, capsys): + # GIVEN environment variable is not set + monkeypatch.delenv("POWERTOOLS_METRICS_DISABLED", raising=False) + + # WHEN metrics is initialized with namespace and metrics added + metrics = DatadogMetrics(namespace="test") + metrics.add_metric(name="test_metric", value=1) + metrics.flush_metrics() + + # THEN metrics should be written to stdout + output = capsys.readouterr().out + metrics_output = json.loads(output) + + assert "test.test_metric" in metrics_output["m"] From 7229bc461c334585de97473a7d735474f18ad9e2 Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Fri, 7 Feb 2025 14:44:31 -0300 Subject: [PATCH 09/13] disable when powertools dev --- .../provider/cloudwatch_emf/cloudwatch.py | 26 +++--- .../metrics/provider/datadog/datadog.py | 10 ++- docs/index.md | 1 + .../metrics/datadog/test_metrics_datadog.py | 80 ++++++++++++++++++- .../test_metrics_cloudwatch_emf.py | 67 ++++++++++++++++ 5 files changed, 173 insertions(+), 11 deletions(-) diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py index 76150240145..fbec1204b58 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py @@ -90,15 +90,23 @@ def __init__( @staticmethod def is_metrics_disabled() -> bool: - """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLED""" - if constants.METRICS_DISABLED_ENV not in os.environ: - return False - - env_value = os.getenv(constants.METRICS_DISABLED_ENV) - is_disabled = resolve_truthy_env_var_choice(env=env_value or "false") - if is_disabled: - logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") - return bool(is_disabled) + """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLED or POWERTOOLS_DEV""" + # First check POWERTOOLS_METRICS_DISABLED + if constants.METRICS_DISABLED_ENV in os.environ: + env_value = os.getenv(constants.METRICS_DISABLED_ENV) + is_disabled = resolve_truthy_env_var_choice(env=env_value or "false") + if is_disabled: + logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") + return bool(is_disabled) + + # Then check if POWERTOOLS_DEV is enabled + dev_mode_value = os.getenv(constants.POWERTOOLS_DEV_ENV) + dev_mode = resolve_truthy_env_var_choice(env=dev_mode_value or "false") + if dev_mode: + logger.debug("Metrics have been disabled via env var POWERTOOLS_DEV") + return True + + return False def add_metric( self, diff --git a/aws_lambda_powertools/metrics/provider/datadog/datadog.py b/aws_lambda_powertools/metrics/provider/datadog/datadog.py index 776c27f9eb0..4e3e8a36836 100644 --- a/aws_lambda_powertools/metrics/provider/datadog/datadog.py +++ b/aws_lambda_powertools/metrics/provider/datadog/datadog.py @@ -69,7 +69,15 @@ def __init__( @staticmethod def is_metrics_disabled() -> bool: - """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLED""" + """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLED or POWERTOOLS_DEV""" + # First check if POWERTOOLS_DEV is enabled + dev_mode_value = os.getenv(constants.POWERTOOLS_DEV_ENV) + dev_mode = resolve_truthy_env_var_choice(env=dev_mode_value or "false") + if dev_mode: + logger.debug("Metrics have been disabled via env var POWERTOOLS_DEV") + return True + + # Then check POWERTOOLS_METRICS_DISABLED if constants.METRICS_DISABLED_ENV not in os.environ: return False diff --git a/docs/index.md b/docs/index.md index f2155db96af..4f5c165f287 100644 --- a/docs/index.md +++ b/docs/index.md @@ -432,6 +432,7 @@ When `POWERTOOLS_DEV` is set to a truthy value (`1`, `true`), it'll have the fol | __Logger__ | Increase JSON indentation to 4. This will ease local debugging when running functions locally under emulators or direct calls while not affecting unit tests.

However, Amazon CloudWatch Logs view will degrade as each new line is treated as a new message. | | __Event Handler__ | Enable full traceback errors in the response, indent request/responses, and CORS in dev mode (`*`). | | __Tracer__ | Future-proof safety to disables tracing operations in non-Lambda environments. This already happens automatically in the Tracer utility. | +| __Metrics__ | Disables Powertools metrics emission by default.

However, this can be overridden by explicitly setting POWERTOOLS_METRICS_DISABLED=false, which takes precedence over the dev mode setting. | ## Debug mode diff --git a/tests/functional/metrics/datadog/test_metrics_datadog.py b/tests/functional/metrics/datadog/test_metrics_datadog.py index 0ca4f522094..eae87d86f6a 100644 --- a/tests/functional/metrics/datadog/test_metrics_datadog.py +++ b/tests/functional/metrics/datadog/test_metrics_datadog.py @@ -336,7 +336,6 @@ def test_namespace_env_var(monkeypatch): assert output[0]["m"] == f"{env_namespace}.item_sold" -#################### def test_metrics_disabled_with_env_var(monkeypatch): # GIVEN environment variable is set to disable metrics monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") @@ -412,3 +411,82 @@ def test_metrics_enabled_with_env_var_not_set(monkeypatch, capsys): metrics_output = json.loads(output) assert "test.test_metric" in metrics_output["m"] + + +def test_metrics_disabled_with_dev_mode_true(monkeypatch, capsys): + # GIVEN dev mode is enabled + monkeypatch.setenv("POWERTOOLS_DEV", "true") + + # WHEN metrics is initialized + metrics = DatadogMetrics(namespace="test") + metrics.add_metric(name="test_metric", value=1) + + # AND flushing metrics + metrics_output = metrics.flush_metrics() + + # THEN metrics output should be empty + assert metrics_output is None + + +def test_metrics_enabled_with_dev_mode_false(monkeypatch, capsys): + # GIVEN dev mode is disabled + monkeypatch.setenv("POWERTOOLS_DEV", "false") + + # WHEN metrics is initialized + metrics = DatadogMetrics(namespace="test") + metrics.add_metric(name="test_metric", value=1) + metrics.flush_metrics() + + # THEN metrics should be written to stdout + output = capsys.readouterr().out + metrics_output = json.loads(output) + assert metrics_output + + +def test_metrics_disabled_dev_mode_overrides_metrics_disabled(monkeypatch, capsys): + # GIVEN dev mode is enabled but metrics disabled is false + monkeypatch.setenv("POWERTOOLS_DEV", "true") + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "false") + + # WHEN metrics is initialized + metrics = DatadogMetrics(namespace="test") + metrics.add_metric(name="test_metric", value=1) + metrics.flush_metrics() + + # THEN metrics should be written to stdout (POWERTOOLS_METRICS_DISABLED takes precedence) + output = capsys.readouterr().out + metrics_output = json.loads(output) + assert metrics_output + + +def test_metrics_enabled_with_both_false(monkeypatch, capsys): + # GIVEN both dev mode and metrics disabled are false + monkeypatch.setenv("POWERTOOLS_DEV", "false") + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "false") + + # WHEN metrics is initialized + metrics = DatadogMetrics(namespace="test") + metrics.add_metric(name="test_metric", value=1) + metrics.flush_metrics() + + # THEN metrics should be written to stdout + output = capsys.readouterr().out + metrics_output = json.loads(output) + assert metrics_output + + +def test_metrics_disabled_with_dev_mode_false_and_metrics_disabled_true(monkeypatch): + # GIVEN dev mode is false but metrics disabled is true + monkeypatch.setenv("POWERTOOLS_DEV", "false") + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") + + # WHEN metrics is initialized + metrics = DatadogMetrics(namespace="test") + + metrics.add_metric(name="test_metric", value=1) + + # WHEN flushing metrics + metrics_output = metrics.flush_metrics() + + # THEN metrics output should be empty + assert metrics_output is None 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 cfb7db31dbc..08747c8808b 100644 --- a/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py +++ b/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py @@ -1412,3 +1412,70 @@ def test_metrics_enabled_with_env_var_not_set(monkeypatch, capsys): assert metrics_output["test_metric"] == [1.0] assert metrics_output["_aws"]["CloudWatchMetrics"][0]["Namespace"] == "test" assert metrics_output["_aws"]["CloudWatchMetrics"][0]["Metrics"][0]["Name"] == "test_metric" + + +def test_metrics_disabled_with_dev_mode(monkeypatch, namespace): + # GIVEN environment variable is set to disable metrics + monkeypatch.setenv("POWERTOOLS_DEV", "true") + + # WHEN metrics is initialized and adding metrics + metrics = Metrics(namespace=namespace) + metrics.add_metric(name="test_metric", unit="Count", value=1) + + # AND flushing metrics + metrics_output = metrics.flush_metrics() + + # THEN metrics output should be empty + assert metrics_output is None + + +def test_metrics_enabled_with_dev_mode_false(monkeypatch, capsys): + # GIVEN environment variable is set to enable metrics + monkeypatch.setenv("POWERTOOLS_DEV", "false") + + # WHEN metrics is initialized with namespace and metrics added + metrics = Metrics(namespace="test") + metrics.add_metric(name="test_metric", unit="Count", value=1) + metrics.flush_metrics() + + # THEN metrics should be written to stdout + output = capsys.readouterr().out + metrics_output = json.loads(output) + + assert "test_metric" in metrics_output + assert metrics_output["test_metric"] == [1.0] + assert metrics_output["_aws"]["CloudWatchMetrics"][0]["Namespace"] == "test" + assert metrics_output["_aws"]["CloudWatchMetrics"][0]["Metrics"][0]["Name"] == "test_metric" + + +def test_metrics_dev_mode_does_not_override_metrics_disabled(monkeypatch, capsys): + # GIVEN dev mode is enabled but metrics disabled is explicitly false + monkeypatch.setenv("POWERTOOLS_DEV", "true") + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "false") + + # WHEN metrics is initialized + metrics = Metrics(namespace="test") + metrics.add_metric(name="test_metric", value=1, unit="Count") + metrics.flush_metrics() + + # THEN metrics should be written to stdout (POWERTOOLS_METRICS_DISABLED takes precedence) + output = capsys.readouterr().out + metrics_output = json.loads(output) + assert metrics_output + + +def test_metrics_disabled_with_dev_mode_false_and_metrics_disabled_true(monkeypatch): + # GIVEN dev mode is false but metrics disabled is true + monkeypatch.setenv("POWERTOOLS_DEV", "false") + monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") + + # WHEN metrics is initialized + metrics = Metrics(namespace="test") + + metrics.add_metric(name="test_metric", value=1, unit="Count") + + # WHEN flushing metrics + metrics_output = metrics.flush_metrics() + + # THEN metrics output should be empty + assert metrics_output is None From c2ea4ba0dc34d3a2742b8cdb7b411fceb8f6cdb7 Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Mon, 10 Feb 2025 11:53:10 -0300 Subject: [PATCH 10/13] add disable to flush --- aws_lambda_powertools/metrics/functions.py | 29 ++++++ .../provider/cloudwatch_emf/cloudwatch.py | 27 ++---- .../metrics/provider/datadog/datadog.py | 27 +----- .../metrics/datadog/test_metrics_datadog.py | 88 ++++++++++--------- .../test_metrics_cloudwatch_emf.py | 63 +++++++------ 5 files changed, 119 insertions(+), 115 deletions(-) diff --git a/aws_lambda_powertools/metrics/functions.py b/aws_lambda_powertools/metrics/functions.py index 14c68e88275..3efa70e103f 100644 --- a/aws_lambda_powertools/metrics/functions.py +++ b/aws_lambda_powertools/metrics/functions.py @@ -1,5 +1,7 @@ from __future__ import annotations +import logging +import os from datetime import datetime from aws_lambda_powertools.metrics.provider.cloudwatch_emf.exceptions import ( @@ -8,6 +10,9 @@ ) 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_truthy_env_var_choice + +logger = logging.getLogger(__name__) def extract_cloudwatch_metric_resolution_value(metric_resolutions: list, resolution: int | MetricResolution) -> int: @@ -134,3 +139,27 @@ def convert_timestamp_to_emf_format(timestamp: int | datetime) -> int: # Returning zero represents the initial date of epoch time, # which will be skipped by Amazon CloudWatch. return 0 + + +def is_metrics_disabled() -> bool: + """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLED or POWERTOOLS_DEV + + Returns: + bool: True if metrics are disabled, False otherwise + """ + # First check POWERTOOLS_METRICS_DISABLED as it should take precedence + if constants.METRICS_DISABLED_ENV in os.environ: + env_value = os.getenv(constants.METRICS_DISABLED_ENV) + is_disabled = resolve_truthy_env_var_choice(env=env_value or "false") + if is_disabled: + logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") + return bool(is_disabled) + + # Then check if POWERTOOLS_DEV is enabled + dev_mode_value = os.getenv(constants.POWERTOOLS_DEV_ENV) + dev_mode = resolve_truthy_env_var_choice(env=dev_mode_value or "false") + if dev_mode: + logger.debug("Metrics have been disabled via env var POWERTOOLS_DEV") + return True + + return False diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py index fbec1204b58..93f1ccc8115 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py @@ -15,13 +15,14 @@ convert_timestamp_to_emf_format, extract_cloudwatch_metric_resolution_value, extract_cloudwatch_metric_unit_value, + is_metrics_disabled, 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.metric_properties import MetricResolution, MetricUnit from aws_lambda_powertools.shared import constants -from aws_lambda_powertools.shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice +from aws_lambda_powertools.shared.functions import resolve_env_var_choice from aws_lambda_powertools.warnings import PowertoolsUserWarning if TYPE_CHECKING: @@ -77,7 +78,7 @@ def __init__( self.default_dimensions = default_dimensions or {} 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.metrics_disabled = self.is_metrics_disabled() + self.metrics_disabled = is_metrics_disabled() self.metadata_set = metadata_set if metadata_set is not None else {} self.timestamp: int | None = None @@ -88,25 +89,7 @@ def __init__( self.dimension_set.update(**self.default_dimensions) - @staticmethod - def is_metrics_disabled() -> bool: - """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLED or POWERTOOLS_DEV""" - # First check POWERTOOLS_METRICS_DISABLED - if constants.METRICS_DISABLED_ENV in os.environ: - env_value = os.getenv(constants.METRICS_DISABLED_ENV) - is_disabled = resolve_truthy_env_var_choice(env=env_value or "false") - if is_disabled: - logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") - return bool(is_disabled) - - # Then check if POWERTOOLS_DEV is enabled - dev_mode_value = os.getenv(constants.POWERTOOLS_DEV_ENV) - dev_mode = resolve_truthy_env_var_choice(env=dev_mode_value or "false") - if dev_mode: - logger.debug("Metrics have been disabled via env var POWERTOOLS_DEV") - return True - - return False + logger.info("im here") def add_metric( self, @@ -398,7 +381,7 @@ def flush_metrics(self, raise_on_empty_metrics: bool = False) -> None: "If application metrics should never be empty, consider using 'raise_on_empty_metrics'", stacklevel=2, ) - else: + elif not self.metrics_disabled: logger.debug("Flushing existing metrics") metrics = self.serialize_metric_set() print(json.dumps(metrics, separators=(",", ":"))) diff --git a/aws_lambda_powertools/metrics/provider/datadog/datadog.py b/aws_lambda_powertools/metrics/provider/datadog/datadog.py index 4e3e8a36836..5122f7e6d3d 100644 --- a/aws_lambda_powertools/metrics/provider/datadog/datadog.py +++ b/aws_lambda_powertools/metrics/provider/datadog/datadog.py @@ -10,10 +10,11 @@ from typing import TYPE_CHECKING, Any from aws_lambda_powertools.metrics.exceptions import MetricValueError, SchemaValidationError +from aws_lambda_powertools.metrics.functions import is_metrics_disabled from aws_lambda_powertools.metrics.provider import BaseProvider from aws_lambda_powertools.metrics.provider.datadog.warnings import DatadogDataValidationWarning from aws_lambda_powertools.shared import constants -from aws_lambda_powertools.shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice +from aws_lambda_powertools.shared.functions import resolve_env_var_choice if TYPE_CHECKING: from aws_lambda_powertools.shared.types import AnyCallableT @@ -65,27 +66,7 @@ def __init__( ) self.default_tags = default_tags or {} self.flush_to_log = resolve_env_var_choice(choice=flush_to_log, env=os.getenv(constants.DATADOG_FLUSH_TO_LOG)) - self.metrics_disabled = self.is_metrics_disabled() - - @staticmethod - def is_metrics_disabled() -> bool: - """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLED or POWERTOOLS_DEV""" - # First check if POWERTOOLS_DEV is enabled - dev_mode_value = os.getenv(constants.POWERTOOLS_DEV_ENV) - dev_mode = resolve_truthy_env_var_choice(env=dev_mode_value or "false") - if dev_mode: - logger.debug("Metrics have been disabled via env var POWERTOOLS_DEV") - return True - - # Then check POWERTOOLS_METRICS_DISABLED - if constants.METRICS_DISABLED_ENV not in os.environ: - return False - - env_value = os.getenv(constants.METRICS_DISABLED_ENV) - is_disabled = resolve_truthy_env_var_choice(env=env_value or "false") - if is_disabled: - logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") - return bool(is_disabled) + self.metrics_disabled = is_metrics_disabled() # adding name,value,timestamp,tags def add_metric( @@ -221,7 +202,7 @@ def flush_metrics(self, raise_on_empty_metrics: bool = False) -> None: timestamp=metric_item["e"], tags=metric_item["t"], ) - else: + elif not self.metrics_disabled: # dd module not found: flush to log, this format can be recognized via datadog log forwarder # https://github.com/Datadog/datadog-lambda-python/blob/main/datadog_lambda/metric.py#L77 for metric_item in metrics: diff --git a/tests/functional/metrics/datadog/test_metrics_datadog.py b/tests/functional/metrics/datadog/test_metrics_datadog.py index eae87d86f6a..631518287a0 100644 --- a/tests/functional/metrics/datadog/test_metrics_datadog.py +++ b/tests/functional/metrics/datadog/test_metrics_datadog.py @@ -336,49 +336,68 @@ def test_namespace_env_var(monkeypatch): assert output[0]["m"] == f"{env_namespace}.item_sold" -def test_metrics_disabled_with_env_var(monkeypatch): +def test_metrics_disabled_with_env_var(monkeypatch, capsys): # GIVEN environment variable is set to disable metrics monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") # WHEN metrics is initialized and adding metrics metrics = DatadogMetrics() metrics.add_metric(name="test_metric", value=1) + metrics.flush_metrics() - # WHEN flushing metrics - metrics_output = metrics.flush_metrics() - - # THEN metrics output should be empty - assert metrics_output is None + # THEN no metrics should have been recorded + captured = capsys.readouterr() + assert not captured.out -def test_metrics_disabled_persists_after_flush(monkeypatch): +def test_metrics_disabled_persists_after_flush(monkeypatch, capsys): # GIVEN environment variable is set to disable metrics monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") metrics = DatadogMetrics() # WHEN multiple operations are performed with flush in between - metrics.add_metric(name="metric1", unit="Count", value=1) - first_flush = metrics.flush_metrics() + metrics.add_metric(name="metric1", value=1) + metrics.flush_metrics() + + # THEN first flush should not emit any metrics + captured = capsys.readouterr() + assert not captured.out - metrics.add_metric(name="metric2", unit="Count", value=2) - second_flush = metrics.flush_metrics() + # WHEN adding and flushing more metrics + metrics.add_metric(name="metric2", value=2) + metrics.flush_metrics() - # THEN all flush operations should return None - assert first_flush is None - assert second_flush is None + # THEN second flush should also not emit any metrics + captured = capsys.readouterr() + assert not captured.out -def test_metrics_disabled_with_namespace(monkeypatch): +def test_metrics_disabled_with_namespace(monkeypatch, capsys): # GIVEN environment variable is set to disable metrics monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") # WHEN metrics is initialized with namespace and service metrics = DatadogMetrics(namespace="test_namespace") metrics.add_metric(name="test_metric", value=1) - metrics_output = metrics.flush_metrics() + metrics.flush_metrics() + + # THEN no metrics should have been recorded + captured = capsys.readouterr() + assert not captured.out + + +def test_metrics_disabled_with_dev_mode_true(monkeypatch, capsys): + # GIVEN dev mode is enabled + monkeypatch.setenv("POWERTOOLS_DEV", "true") + + # WHEN metrics is initialized + metrics = DatadogMetrics(namespace="test") + metrics.add_metric(name="test_metric", value=1) + metrics.flush_metrics() - # THEN metrics should still be disabled - assert metrics_output is None + # THEN no metrics should have been recorded + captured = capsys.readouterr() + assert not captured.out def test_metrics_enabled_with_env_var_false(monkeypatch, capsys): @@ -413,21 +432,6 @@ def test_metrics_enabled_with_env_var_not_set(monkeypatch, capsys): assert "test.test_metric" in metrics_output["m"] -def test_metrics_disabled_with_dev_mode_true(monkeypatch, capsys): - # GIVEN dev mode is enabled - monkeypatch.setenv("POWERTOOLS_DEV", "true") - - # WHEN metrics is initialized - metrics = DatadogMetrics(namespace="test") - metrics.add_metric(name="test_metric", value=1) - - # AND flushing metrics - metrics_output = metrics.flush_metrics() - - # THEN metrics output should be empty - assert metrics_output is None - - def test_metrics_enabled_with_dev_mode_false(monkeypatch, capsys): # GIVEN dev mode is disabled monkeypatch.setenv("POWERTOOLS_DEV", "false") @@ -453,10 +457,12 @@ def test_metrics_disabled_dev_mode_overrides_metrics_disabled(monkeypatch, capsy metrics.add_metric(name="test_metric", value=1) metrics.flush_metrics() - # THEN metrics should be written to stdout (POWERTOOLS_METRICS_DISABLED takes precedence) + # THEN metrics should be written to stdout since POWERTOOLS_METRICS_DISABLED is false output = capsys.readouterr().out + assert output # First verify we have output metrics_output = json.loads(output) - assert metrics_output + assert metrics_output # Then verify it's valid JSON + assert "test.test_metric" in metrics_output["m"] # Verify the metric is present def test_metrics_enabled_with_both_false(monkeypatch, capsys): @@ -475,18 +481,16 @@ def test_metrics_enabled_with_both_false(monkeypatch, capsys): assert metrics_output -def test_metrics_disabled_with_dev_mode_false_and_metrics_disabled_true(monkeypatch): +def test_metrics_disabled_with_dev_mode_false_and_metrics_disabled_true(monkeypatch, capsys): # GIVEN dev mode is false but metrics disabled is true monkeypatch.setenv("POWERTOOLS_DEV", "false") monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") # WHEN metrics is initialized metrics = DatadogMetrics(namespace="test") - metrics.add_metric(name="test_metric", value=1) + metrics.flush_metrics() - # WHEN flushing metrics - metrics_output = metrics.flush_metrics() - - # THEN metrics output should be empty - assert metrics_output is None + # THEN no metrics should have been recorded + captured = capsys.readouterr() + assert not captured.out 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 08747c8808b..29418c42bcf 100644 --- a/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py +++ b/tests/functional/metrics/required_dependencies/test_metrics_cloudwatch_emf.py @@ -1331,49 +1331,54 @@ def lambda_handler(evt, ctx): ) -def test_metrics_disabled_with_env_var(monkeypatch, namespace): +def test_metrics_disabled_with_env_var(monkeypatch, namespace, capsys): # GIVEN environment variable is set to disable metrics monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") # WHEN metrics is initialized and adding metrics metrics = Metrics(namespace=namespace) metrics.add_metric(name="test_metric", unit="Count", value=1) + metrics.flush_metrics() - # WHEN flushing metrics - metrics_output = metrics.flush_metrics() - - # THEN metrics output should be empty - assert metrics_output is None + # THEN no Powertools metrics should be sent to CloudWatch + output = capsys.readouterr() + assert not output.out -def test_metrics_disabled_persists_after_flush(monkeypatch, namespace): +def test_metrics_disabled_persists_after_flush(monkeypatch, capsys, namespace): # GIVEN environment variable is set to disable metrics monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") metrics = Metrics(namespace=namespace) # WHEN multiple operations are performed with flush in between metrics.add_metric(name="metric1", unit="Count", value=1) - first_flush = metrics.flush_metrics() + metrics.flush_metrics() + # THEN first flush should not emit any metrics + captured = capsys.readouterr() + assert not captured.out + + # WHEN adding and flushing more metrics metrics.add_metric(name="metric2", unit="Count", value=2) - second_flush = metrics.flush_metrics() + metrics.flush_metrics() - # THEN all flush operations should return None - assert first_flush is None - assert second_flush is None + # THEN second flush should also not emit any metrics + captured = capsys.readouterr() + assert not captured.out -def test_metrics_disabled_with_namespace_and_service(monkeypatch): +def test_metrics_disabled_with_namespace_and_service(monkeypatch, capsys): # GIVEN environment variable is set to disable metrics monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") # WHEN metrics is initialized with namespace and service metrics = Metrics(namespace="test_namespace", service="test_service") metrics.add_metric(name="test_metric", unit="Count", value=1) - metrics_output = metrics.flush_metrics() + metrics.flush_metrics() - # THEN metrics should still be disabled - assert metrics_output is None + # THEN no metrics should have been recorded + captured = capsys.readouterr() + assert not captured.out def test_metrics_enabled_with_env_var_false(monkeypatch, capsys): @@ -1414,7 +1419,7 @@ def test_metrics_enabled_with_env_var_not_set(monkeypatch, capsys): assert metrics_output["_aws"]["CloudWatchMetrics"][0]["Metrics"][0]["Name"] == "test_metric" -def test_metrics_disabled_with_dev_mode(monkeypatch, namespace): +def test_metrics_disabled_with_dev_mode(monkeypatch, namespace, capsys): # GIVEN environment variable is set to disable metrics monkeypatch.setenv("POWERTOOLS_DEV", "true") @@ -1423,10 +1428,11 @@ def test_metrics_disabled_with_dev_mode(monkeypatch, namespace): metrics.add_metric(name="test_metric", unit="Count", value=1) # AND flushing metrics - metrics_output = metrics.flush_metrics() + metrics.flush_metrics() - # THEN metrics output should be empty - assert metrics_output is None + # THEN no metrics should have been recorded + captured = capsys.readouterr() + assert not captured.out def test_metrics_enabled_with_dev_mode_false(monkeypatch, capsys): @@ -1458,24 +1464,25 @@ def test_metrics_dev_mode_does_not_override_metrics_disabled(monkeypatch, capsys metrics.add_metric(name="test_metric", value=1, unit="Count") metrics.flush_metrics() - # THEN metrics should be written to stdout (POWERTOOLS_METRICS_DISABLED takes precedence) + # THEN metrics should be written to stdout since POWERTOOLS_METRICS_DISABLED is false output = capsys.readouterr().out + assert output # First verify we have output metrics_output = json.loads(output) assert metrics_output + assert "_aws" in metrics_output + assert any(metric["Name"] == "test_metric" for metric in metrics_output["_aws"]["CloudWatchMetrics"][0]["Metrics"]) -def test_metrics_disabled_with_dev_mode_false_and_metrics_disabled_true(monkeypatch): +def test_metrics_disabled_with_dev_mode_false_and_metrics_disabled_true(monkeypatch, capsys): # GIVEN dev mode is false but metrics disabled is true monkeypatch.setenv("POWERTOOLS_DEV", "false") monkeypatch.setenv("POWERTOOLS_METRICS_DISABLED", "true") # WHEN metrics is initialized metrics = Metrics(namespace="test") - metrics.add_metric(name="test_metric", value=1, unit="Count") + metrics.flush_metrics() - # WHEN flushing metrics - metrics_output = metrics.flush_metrics() - - # THEN metrics output should be empty - assert metrics_output is None + # THEN no metrics should have been recorded + captured = capsys.readouterr() + assert not captured.out From 7a1d389e3a41101f0b2e434c893e0e9e7f425687 Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Mon, 10 Feb 2025 12:31:25 -0300 Subject: [PATCH 11/13] remove print --- .../metrics/provider/cloudwatch_emf/cloudwatch.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py index 93f1ccc8115..07b6a224f90 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py @@ -89,8 +89,6 @@ def __init__( self.dimension_set.update(**self.default_dimensions) - logger.info("im here") - def add_metric( self, name: str, From 8b8bc8c505ffb104aa3ee48bf4263cce4cbc675e Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Mon, 10 Feb 2025 20:52:23 +0000 Subject: [PATCH 12/13] Changing metrics flush decision --- aws_lambda_powertools/metrics/functions.py | 39 ++++++++++--------- .../provider/cloudwatch_emf/cloudwatch.py | 3 +- .../metrics/provider/datadog/datadog.py | 3 +- docs/core/metrics.md | 12 +++--- docs/core/metrics/datadog.md | 12 +++--- 5 files changed, 34 insertions(+), 35 deletions(-) diff --git a/aws_lambda_powertools/metrics/functions.py b/aws_lambda_powertools/metrics/functions.py index 3efa70e103f..6c474b62a02 100644 --- a/aws_lambda_powertools/metrics/functions.py +++ b/aws_lambda_powertools/metrics/functions.py @@ -10,7 +10,7 @@ ) 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_truthy_env_var_choice +from aws_lambda_powertools.shared.functions import strtobool logger = logging.getLogger(__name__) @@ -142,24 +142,25 @@ def convert_timestamp_to_emf_format(timestamp: int | datetime) -> int: def is_metrics_disabled() -> bool: - """Checks if metrics have been disabled via POWERTOOLS_METRICS_DISABLED or POWERTOOLS_DEV + """ + Determine if metrics should be disabled based on environment variables. Returns: - bool: True if metrics are disabled, False otherwise + bool: True if metrics are disabled, False otherwise. + + Rules: + - If POWERTOOLS_DEV is True and POWERTOOLS_METRICS_DISABLED is True: Disable metrics + - If POWERTOOLS_METRICS_DISABLED is True: Disable metrics + - If POWERTOOLS_DEV is True and POWERTOOLS_METRICS_DISABLED is not set: Disable metrics """ - # First check POWERTOOLS_METRICS_DISABLED as it should take precedence - if constants.METRICS_DISABLED_ENV in os.environ: - env_value = os.getenv(constants.METRICS_DISABLED_ENV) - is_disabled = resolve_truthy_env_var_choice(env=env_value or "false") - if is_disabled: - logger.debug("Metrics have been disabled via env var POWERTOOLS_METRICS_DISABLED") - return bool(is_disabled) - - # Then check if POWERTOOLS_DEV is enabled - dev_mode_value = os.getenv(constants.POWERTOOLS_DEV_ENV) - dev_mode = resolve_truthy_env_var_choice(env=dev_mode_value or "false") - if dev_mode: - logger.debug("Metrics have been disabled via env var POWERTOOLS_DEV") - return True - - return False + + is_dev_mode = strtobool(os.getenv(constants.POWERTOOLS_DEV_ENV, "false")) + is_metrics_disabled = strtobool(os.getenv(constants.METRICS_DISABLED_ENV, "false")) + + disable_conditions = [ + is_metrics_disabled, + is_metrics_disabled and is_dev_mode, + is_dev_mode and os.getenv(constants.METRICS_DISABLED_ENV) is None, + ] + + return any(disable_conditions) diff --git a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py index 07b6a224f90..65c5b619f57 100644 --- a/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py +++ b/aws_lambda_powertools/metrics/provider/cloudwatch_emf/cloudwatch.py @@ -78,7 +78,6 @@ def __init__( self.default_dimensions = default_dimensions or {} 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.metrics_disabled = is_metrics_disabled() self.metadata_set = metadata_set if metadata_set is not None else {} self.timestamp: int | None = None @@ -379,7 +378,7 @@ def flush_metrics(self, raise_on_empty_metrics: bool = False) -> None: "If application metrics should never be empty, consider using 'raise_on_empty_metrics'", stacklevel=2, ) - elif not self.metrics_disabled: + elif not is_metrics_disabled(): logger.debug("Flushing existing metrics") metrics = self.serialize_metric_set() print(json.dumps(metrics, separators=(",", ":"))) diff --git a/aws_lambda_powertools/metrics/provider/datadog/datadog.py b/aws_lambda_powertools/metrics/provider/datadog/datadog.py index 5122f7e6d3d..3e88523df38 100644 --- a/aws_lambda_powertools/metrics/provider/datadog/datadog.py +++ b/aws_lambda_powertools/metrics/provider/datadog/datadog.py @@ -66,7 +66,6 @@ def __init__( ) self.default_tags = default_tags or {} self.flush_to_log = resolve_env_var_choice(choice=flush_to_log, env=os.getenv(constants.DATADOG_FLUSH_TO_LOG)) - self.metrics_disabled = is_metrics_disabled() # adding name,value,timestamp,tags def add_metric( @@ -202,7 +201,7 @@ def flush_metrics(self, raise_on_empty_metrics: bool = False) -> None: timestamp=metric_item["e"], tags=metric_item["t"], ) - elif not self.metrics_disabled: + elif not is_metrics_disabled(): # dd module not found: flush to log, this format can be recognized via datadog log forwarder # https://github.com/Datadog/datadog-lambda-python/blob/main/datadog_lambda/metric.py#L77 for metric_item in metrics: diff --git a/docs/core/metrics.md b/docs/core/metrics.md index b4ae9357cbe..88f0292231d 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -36,13 +36,13 @@ If you're new to Amazon CloudWatch, there are five terminologies you must be awa Metric has three global settings that will be used across all metrics emitted: -| Setting | Description | Environment variable | Constructor parameter | -| -------------------- | ------------------------------------------------------------------------------- | ------------------------------ | --------------------- | -| **Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` | -| **Service** | Optionally, sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `service` | -| **Disable Powertools Metrics** | Optionally, disables all Powertools metrics. | `POWERTOOLS_METRICS_DISABLED` | N/A | +| Setting | Description | Environment variable | Constructor parameter | +| ------------------------------- | ------------------------------------------------------------------------------- | ------------------------------ | --------------------- | +| **Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` | +| **Service** | Optionally, sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `service` | +| **Disable Powertools Metrics** | Optionally, disables all Powertools metrics. | `POWERTOOLS_METRICS_DISABLED` | N/A | -???+ warning +???+ info `POWERTOOLS_METRICS_DISABLED` will not disable default metrics created by AWS services. ???+ tip diff --git a/docs/core/metrics/datadog.md b/docs/core/metrics/datadog.md index bfde98315dd..c5b9fdc35b8 100644 --- a/docs/core/metrics/datadog.md +++ b/docs/core/metrics/datadog.md @@ -174,13 +174,13 @@ This has the advantage of keeping cold start metric separate from your applicati You can use any of the following environment variables to configure `DatadogMetrics`: -| Setting | Description | Environment variable | Constructor parameter | -| -------------------- | -------------------------------------------------------------------------------- | ------------------------------ | --------------------- | -| **Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` | -| **Flush to log** | Use this when you want to flush metrics to be exported through Datadog Forwarder | `DD_FLUSH_TO_LOG` | `flush_to_log` | -| **Disable Powertools Metrics** | Optionally, disables all Powertools metrics. | `POWERTOOLS_METRICS_DISABLED` | N/A | +| Setting | Description | Environment variable | Constructor parameter | +| ------------------------------ | -------------------------------------------------------------------------------- | ------------------------------ | --------------------- | +| **Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` | +| **Flush to log** | Use this when you want to flush metrics to be exported through Datadog Forwarder | `DD_FLUSH_TO_LOG` | `flush_to_log` | +| **Disable Powertools Metrics** | Optionally, disables all Powertools metrics. | `POWERTOOLS_METRICS_DISABLED` | N/A | -???+ warning +???+ info `POWERTOOLS_METRICS_DISABLED` will not disable default metrics created by AWS services. ## Advanced From 25b70a30eca499f84d0d412ddd89843625356b68 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Mon, 10 Feb 2025 20:53:23 +0000 Subject: [PATCH 13/13] Changing metrics flush decision --- aws_lambda_powertools/metrics/functions.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/aws_lambda_powertools/metrics/functions.py b/aws_lambda_powertools/metrics/functions.py index 6c474b62a02..75bf0855e18 100644 --- a/aws_lambda_powertools/metrics/functions.py +++ b/aws_lambda_powertools/metrics/functions.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging import os from datetime import datetime @@ -12,8 +11,6 @@ from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.functions import strtobool -logger = logging.getLogger(__name__) - def extract_cloudwatch_metric_resolution_value(metric_resolutions: list, resolution: int | MetricResolution) -> int: """Return metric value from CloudWatch metric unit whether that's str or MetricResolution enum