diff --git a/aws_lambda_powertools/metrics/base.py b/aws_lambda_powertools/metrics/base.py index 9d2f07b677f..c8475987550 100644 --- a/aws_lambda_powertools/metrics/base.py +++ b/aws_lambda_powertools/metrics/base.py @@ -4,6 +4,7 @@ import numbers import os import pathlib +import warnings from typing import Dict, List, Union import fastjsonschema @@ -34,7 +35,7 @@ class MetricManager: Environment variables --------------------- - POWERTOOLS_METRICS_NAMESPACE : str + POWERTOOLS_SERVICE_NAME : str metric namespace to be set for all metrics Raises @@ -52,7 +53,7 @@ class MetricManager: def __init__(self, metric_set: Dict[str, str] = None, dimension_set: Dict = None, namespace: str = None): 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.namespace = os.getenv("POWERTOOLS_METRICS_NAMESPACE") or namespace + self.namespace = namespace or os.getenv("POWERTOOLS_SERVICE_NAME") self._metric_units = [unit.value for unit in MetricUnit] self._metric_unit_options = list(MetricUnit.__members__) @@ -70,6 +71,9 @@ def add_namespace(self, name: str): name : str Metric namespace """ + warnings.warn( + "add_namespace method is deprecated. Pass service to Metrics constructor instead", DeprecationWarning + ) if self.namespace is not None: raise UniqueNamespaceError( f"Namespace '{self.namespace}' already set - Only one namespace is allowed across metrics" diff --git a/aws_lambda_powertools/metrics/metric.py b/aws_lambda_powertools/metrics/metric.py index 05a8d4ce76b..4bb67a4c761 100644 --- a/aws_lambda_powertools/metrics/metric.py +++ b/aws_lambda_powertools/metrics/metric.py @@ -21,7 +21,7 @@ class SingleMetric(MetricManager): Environment variables --------------------- - POWERTOOLS_METRICS_NAMESPACE : str + POWERTOOLS_SERVICE_NAME : str metric namespace Example @@ -30,9 +30,8 @@ class SingleMetric(MetricManager): from aws_lambda_powertools.metrics import SingleMetric, MetricUnit import json - metric = Single_Metric() + metric = Single_Metric(service="ServerlessAirline") - metric.add_namespace(name="ServerlessAirline") metric.add_metric(name="ColdStart", unit=MetricUnit.Count, value=1) metric.add_dimension(name="function_version", value=47) @@ -63,7 +62,7 @@ def add_metric(self, name: str, unit: MetricUnit, value: float): @contextmanager -def single_metric(name: str, unit: MetricUnit, value: float): +def single_metric(name: str, unit: MetricUnit, value: float, service: str = None): """Context manager to simplify creation of a single metric Example @@ -72,13 +71,12 @@ def single_metric(name: str, unit: MetricUnit, value: float): from aws_lambda_powertools.metrics import single_metric, MetricUnit - with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1) as metric: - metric.add_namespace(name="ServerlessAirline") + with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1, service="ServerlessAirline") as metric: metric.add_dimension(name="function_version", value=47) **Same as above but set namespace using environment variable** - $ export POWERTOOLS_METRICS_NAMESPACE="ServerlessAirline" + $ export POWERTOOLS_SERVICE_NAME="ServerlessAirline" from aws_lambda_powertools.metrics import single_metric, MetricUnit @@ -93,6 +91,8 @@ def single_metric(name: str, unit: MetricUnit, value: float): `aws_lambda_powertools.helper.models.MetricUnit` value : float Metric value + service: str + Service name used as namespace Yields ------- @@ -106,7 +106,7 @@ def single_metric(name: str, unit: MetricUnit, value: float): """ metric_set = None try: - metric: SingleMetric = SingleMetric() + metric: SingleMetric = SingleMetric(namespace=service) metric.add_metric(name=name, unit=unit, value=value) yield metric logger.debug("Serializing single metric") diff --git a/aws_lambda_powertools/metrics/metrics.py b/aws_lambda_powertools/metrics/metrics.py index 13830411523..3dc3d80961f 100644 --- a/aws_lambda_powertools/metrics/metrics.py +++ b/aws_lambda_powertools/metrics/metrics.py @@ -29,8 +29,7 @@ class Metrics(MetricManager): from aws_lambda_powertools.metrics import Metrics - metrics = Metrics() - metrics.add_namespace(name="ServerlessAirline") + metrics = Metrics(service="ServerlessAirline") metrics.add_metric(name="ColdStart", unit=MetricUnit.Count, value=1) metrics.add_metric(name="BookingConfirmation", unit="Count", value=1) metrics.add_dimension(name="service", value="booking") @@ -48,7 +47,7 @@ def do_something(): Environment variables --------------------- - POWERTOOLS_METRICS_NAMESPACE : str + POWERTOOLS_SERVICE_NAME : str metric namespace Parameters @@ -65,10 +64,13 @@ def do_something(): _metrics = {} _dimensions = {} - def __init__(self): + def __init__( + self, service: str = None, + ): self.metric_set = self._metrics self.dimension_set = self._dimensions - super().__init__(metric_set=self.metric_set, dimension_set=self.dimension_set) + self.service = service + super().__init__(metric_set=self.metric_set, dimension_set=self.dimension_set, namespace=self.service) def clear_metrics(self): logger.debug("Clearing out existing metric set from memory") @@ -84,7 +86,7 @@ def log_metrics(self, lambda_handler: Callable[[Any, Any], Any] = None): ------- **Lambda function using tracer and metrics decorators** - metrics = Metrics() + metrics = Metrics(service="payment") tracer = Tracer(service="payment") @tracer.capture_lambda_handler diff --git a/docs/content/core/metrics.mdx b/docs/content/core/metrics.mdx index b13d0693fab..429ee21649a 100644 --- a/docs/content/core/metrics.mdx +++ b/docs/content/core/metrics.mdx @@ -16,7 +16,7 @@ Metrics creates custom metrics asynchronously via logging metrics to standard ou ## Initialization -Set `POWERTOOLS_METRICS_NAMESPACE` env var as a start - Here is an example using AWS Serverless Application Model (SAM) +Set `POWERTOOLS_SERVICE_NAME` env var as a start - Here is an example using AWS Serverless Application Model (SAM) ```yaml:title=template.yaml Resources: @@ -27,16 +27,22 @@ Resources: Runtime: python3.8 Environment: Variables: - POWERTOOLS_METRICS_NAMESPACE: ServerlessAirline # highlight-line + POWERTOOLS_SERVICE_NAME: ServerlessAirline # highlight-line ``` We recommend you use your application or main service as a metric namespace. +You can explicitly set a namespace name via `service` param or via `POWERTOOLS_SERVICE_NAME` env var. This sets **namespace** key that will be used for all metrics. ```python:title=app.py from aws_lambda_powertools.metrics import Metrics, MetricUnit -metrics = Metrics() -# metrics.add_namespace("ServerlessAirline") # optionally if you set via env var +# POWERTOOLS_SERVICE_NAME defined +metrics = Metrics() # highlight-line + +# Explicit definition +Metrics(service="ServerlessAirline") # sets namespace to "ServerlessAirline" + + ``` You can initialize Metrics anywhere in your code as many time as you need - It'll keep track of your aggregate metrics in memory. @@ -48,7 +54,7 @@ You can create metrics using `add_metric`, and set dimensions for all your aggre ```python:title=app.py from aws_lambda_powertools.metrics import Metrics, MetricUnit -metrics = Metrics() +metrics = Metrics(service="ExampleService") # highlight-start metrics.add_metric(name="ColdStart", unit=MetricUnit.Count, value=1) metrics.add_dimension(name="service", value="booking") @@ -73,7 +79,7 @@ CloudWatch EMF uses the same dimensions across all your metrics. Use `single_met ```python:title=single_metric.py from aws_lambda_powertools.metrics import MetricUnit, single_metric -with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1) as metric: # highlight-line +with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1, service="ExampleService") as metric: # highlight-line metric.add_dimension(name="function_context", value="$LATEST") ... ``` @@ -85,7 +91,7 @@ As you finish adding all your metrics, you need to serialize and flush them to s ```python:title=lambda_handler.py from aws_lambda_powertools.metrics import Metrics, MetricUnit -metrics = Metrics() +metrics = Metrics(service="ExampleService") metrics.add_metric(name="ColdStart", unit="Count", value=1) @metrics.log_metrics # highlight-line @@ -109,7 +115,7 @@ def lambda_handler(evt, ctx): ```python:title=lambda_handler_nested_middlewares.py from aws_lambda_powertools.metrics import Metrics, MetricUnit -metrics = Metrics() +metrics = Metrics(service="ExampleService") metrics.add_metric(name="ColdStart", unit="Count", value=1) # highlight-start @@ -130,7 +136,7 @@ If you prefer not to use `log_metrics` because you might want to encapsulate add import json from aws_lambda_powertools.metrics import Metrics, MetricUnit -metrics = Metrics() +metrics = Metrics(service="ExampleService") metrics.add_metric(name="ColdStart", unit="Count", value=1) metrics.add_dimension(name="service", value="booking") @@ -143,10 +149,10 @@ print(json.dumps(your_metrics_object)) ## Testing your code -Use `POWERTOOLS_METRICS_NAMESPACE` env var when unit testing your code to ensure a metric namespace object is created, and your code doesn't fail validation. +Use `POWERTOOLS_SERVICE_NAME` env var when unit testing your code to ensure a metric namespace object is created, and your code doesn't fail validation. ```bash:title=pytest_metric_namespace.sh -POWERTOOLS_METRICS_NAMESPACE="Example" python -m pytest +POWERTOOLS_SERVICE_NAME="Example" python -m pytest ``` -You can ignore that if you are explicitly creating metric namespace within your own code `metrics.add_namespace()`. +You can ignore this if you are explicitly setting namespace by passing a service name when initializing Metrics: `metrics = Metrics(service=ServiceName)`. diff --git a/docs/content/index.mdx b/docs/content/index.mdx index b1fde04ceed..edd0706289e 100644 --- a/docs/content/index.mdx +++ b/docs/content/index.mdx @@ -36,12 +36,11 @@ _`*` Core utilities are Tracer, Logger and Metrics. Optional utilities may vary Environment variable | Description | Utility ------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------- -**POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics dimensions and structured logging | all +**POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics namespace and structured logging | all **POWERTOOLS_TRACE_DISABLED** | Disables tracing | [Tracing](./core/tracer) **POWERTOOLS_TRACE_MIDDLEWARES** | Creates sub-segment for each custom middleware | [middleware_factory](./utilities/middleware_factory) **POWERTOOLS_LOGGER_LOG_EVENT** | Logs incoming event | [Logging](./core/logger) **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logging](./core/logger) -**POWERTOOLS_METRICS_NAMESPACE** | Metrics namespace | [Metrics](./core/metrics) **LOG_LEVEL** | Sets logging level | [Logging](./core/logger) ## Debug mode diff --git a/example/README.md b/example/README.md index 4fec2cad50e..98e6d9d7339 100644 --- a/example/README.md +++ b/example/README.md @@ -10,7 +10,7 @@ This example uses both [tracing](https://github.com/awslabs/aws-lambda-powertool * **Unit Tests**: We recommend proceeding with the following commands in a virtual environment - **Install deps**: `pip install -r hello_world/requirements.txt && pip install -r requirements-dev.txt` - **Run tests with tracing disabled and namespace set** - - `POWERTOOLS_METRICS_NAMESPACE="Example" POWERTOOLS_TRACE_DISABLED=1 python -m pytest` + - `POWERTOOLS_SERVICE_NAME="Example" POWERTOOLS_TRACE_DISABLED=1 python -m pytest` - Both are necessary because `app.py` initializes them in the global scope, since both Tracer and Metrics will be initialized and configured during import time. For unit tests, we could always patch and explicitly config but env vars do just fine for this example. # Example code diff --git a/example/template.yaml b/example/template.yaml index 47267d729f5..17a02592a9d 100644 --- a/example/template.yaml +++ b/example/template.yaml @@ -24,7 +24,6 @@ Resources: POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default POWERTOOLS_LOGGER_LOG_EVENT: "false" # Logs incoming event, default POWERTOOLS_LOGGER_SAMPLE_RATE: "0" # Debug log sampling percentage, default - POWERTOOLS_METRICS_NAMESPACE: "Example" # Metric Namespace LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default Events: HelloWorld: diff --git a/tests/functional/test_metrics.py b/tests/functional/test_metrics.py index b5c1a3232de..50aacc734e5 100644 --- a/tests/functional/test_metrics.py +++ b/tests/functional/test_metrics.py @@ -162,14 +162,14 @@ def lambda_handler(evt, ctx): def test_namespace_env_var(monkeypatch, capsys, metric, dimension, namespace): - # GIVEN we use POWERTOOLS_METRICS_NAMESPACE - monkeypatch.setenv("POWERTOOLS_METRICS_NAMESPACE", namespace["name"]) + # GIVEN we use POWERTOOLS_SERVICE_NAME + monkeypatch.setenv("POWERTOOLS_SERVICE_NAME", namespace["name"]) # WHEN creating a metric but don't explicitly # add a namespace with single_metric(**metric) as my_metrics: my_metrics.add_dimension(**dimension) - monkeypatch.delenv("POWERTOOLS_METRICS_NAMESPACE") + monkeypatch.delenv("POWERTOOLS_SERVICE_NAME") output = json.loads(capsys.readouterr().out.strip()) expected = serialize_single_metric(metric=metric, dimension=dimension, namespace=namespace) @@ -177,7 +177,7 @@ def test_namespace_env_var(monkeypatch, capsys, metric, dimension, namespace): remove_timestamp(metrics=[output, expected]) # Timestamp will always be different # THEN we should add a namespace implicitly - # with the value of POWERTOOLS_METRICS_NAMESPACE env var + # with the value of POWERTOOLS_SERVICE_NAME env var assert expected["_aws"] == output["_aws"] @@ -412,3 +412,92 @@ def lambda_handler(evt, ctx): # and dimension values hould be serialized as strings for dimension in non_str_dimensions: assert isinstance(output[dimension["name"]], str) + + +def test_add_namespace_warns_for_deprecation(capsys, metrics, dimensions, namespace): + # GIVEN Metrics is initialized + my_metrics = Metrics() + with pytest.deprecated_call(): + my_metrics.add_namespace(**namespace) + + +def test_log_metrics_with_explicit_service(capsys, metrics, dimensions): + # GIVEN Metrics is initialized with service specified + my_metrics = Metrics(service="test_service") + for metric in metrics: + my_metrics.add_metric(**metric) + for dimension in dimensions: + my_metrics.add_dimension(**dimension) + + # WHEN we utilize log_metrics to serialize + # and flush all metrics at the end of a function execution + @my_metrics.log_metrics + def lambda_handler(evt, ctx): + return True + + lambda_handler({}, {}) + + output = json.loads(capsys.readouterr().out.strip()) + expected = serialize_metrics(metrics=metrics, dimensions=dimensions, namespace={"name": "test_service"}) + + remove_timestamp(metrics=[output, expected]) # Timestamp will always be different + + # THEN we should have no exceptions and the namespace should be set to the name provided in the + # service passed to Metrics constructor + assert expected["_aws"] == output["_aws"] + + +def test_log_metrics_with_namespace_overridden(capsys, metrics, dimensions): + # GIVEN Metrics is initialized with service specified + my_metrics = Metrics(service="test_service") + for metric in metrics: + my_metrics.add_metric(**metric) + for dimension in dimensions: + my_metrics.add_dimension(**dimension) + + # WHEN we try to call add_namespace + # THEN we should raise UniqueNamespaceError exception + @my_metrics.log_metrics + def lambda_handler(evt, ctx): + my_metrics.add_namespace(name="new_namespace") + return True + + with pytest.raises(UniqueNamespaceError): + lambda_handler({}, {}) + + with pytest.raises(UniqueNamespaceError): + my_metrics.add_namespace(name="another_new_namespace") + + +def test_single_metric_with_service(capsys, metric, dimension): + # GIVEN we pass service parameter to single_metric + + # WHEN creating a metric + with single_metric(**metric, service="test_service") as my_metrics: + my_metrics.add_dimension(**dimension) + + output = json.loads(capsys.readouterr().out.strip()) + expected = serialize_single_metric(metric=metric, dimension=dimension, namespace={"name": "test_service"}) + + remove_timestamp(metrics=[output, expected]) # Timestamp will always be different + + # THEN namespace should match value passed as service + assert expected["_aws"] == output["_aws"] + + +def test_namespace_var_precedence(monkeypatch, capsys, metric, dimension): + # GIVEN we use POWERTOOLS_SERVICE_NAME + monkeypatch.setenv("POWERTOOLS_SERVICE_NAME", "test_service_env_var") + + # WHEN creating a metric and explicitly set a service name + with single_metric(**metric, service="test_service_explicit") as my_metrics: + my_metrics.add_dimension(**dimension) + monkeypatch.delenv("POWERTOOLS_SERVICE_NAME") + + output = json.loads(capsys.readouterr().out.strip()) + expected = serialize_single_metric(metric=metric, dimension=dimension, namespace={"name": "test_service_explicit"}) + + remove_timestamp(metrics=[output, expected]) # Timestamp will always be different + + # THEN namespace should match the explicitly passed variable and not the env var + assert expected["_aws"] == output["_aws"]