From a8c9511b31f328a78088261fc2e708f480e66107 Mon Sep 17 00:00:00 2001 From: Tom McCarthy Date: Fri, 5 Jun 2020 23:07:17 +0200 Subject: [PATCH 1/6] feat: update Metrics interface to resemble tracer & logger: use "service" as its namespace. --- aws_lambda_powertools/metrics/base.py | 6 +- aws_lambda_powertools/metrics/metric.py | 16 ++-- aws_lambda_powertools/metrics/metrics.py | 13 ++-- docs/content/core/metrics.mdx | 30 +++++--- docs/content/index.mdx | 3 +- example/README.md | 2 +- example/template.yaml | 1 - tests/functional/test_metrics.py | 98 +++++++++++++++++++++++- 8 files changed, 134 insertions(+), 35 deletions(-) diff --git a/aws_lambda_powertools/metrics/base.py b/aws_lambda_powertools/metrics/base.py index 9d2f07b677f..a349aafebe4 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,7 @@ 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..037049e348b 100644 --- a/aws_lambda_powertools/metrics/metrics.py +++ b/aws_lambda_powertools/metrics/metrics.py @@ -1,6 +1,7 @@ import functools import json import logging +import os from typing import Any, Callable from aws_lambda_powertools.metrics.base import MetricManager @@ -29,8 +30,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 +48,7 @@ def do_something(): Environment variables --------------------- - POWERTOOLS_METRICS_NAMESPACE : str + POWERTOOLS_SERVICE_NAME : str metric namespace Parameters @@ -65,10 +65,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") diff --git a/docs/content/core/metrics.mdx b/docs/content/core/metrics.mdx index b13d0693fab..02c34cdc29c 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 +Logger(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..66fe7003996 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,93 @@ 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 we should add a namespace implicitly + # with the value of POWERTOOLS_METRICS_NAMESPACE env var + assert expected["_aws"] == output["_aws"] \ No newline at end of file From bd726582c73de4a1853dbdbba24af0e8df2ecc38 Mon Sep 17 00:00:00 2001 From: Tom McCarthy Date: Fri, 5 Jun 2020 23:17:26 +0200 Subject: [PATCH 2/6] chore: formatting --- aws_lambda_powertools/metrics/base.py | 4 +++- tests/functional/test_metrics.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/metrics/base.py b/aws_lambda_powertools/metrics/base.py index a349aafebe4..c8475987550 100644 --- a/aws_lambda_powertools/metrics/base.py +++ b/aws_lambda_powertools/metrics/base.py @@ -71,7 +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) + 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/tests/functional/test_metrics.py b/tests/functional/test_metrics.py index 66fe7003996..786595c3a12 100644 --- a/tests/functional/test_metrics.py +++ b/tests/functional/test_metrics.py @@ -501,4 +501,4 @@ def test_namespace_var_precedence(monkeypatch, capsys, metric, dimension): # THEN we should add a namespace implicitly # with the value of POWERTOOLS_METRICS_NAMESPACE env var - assert expected["_aws"] == output["_aws"] \ No newline at end of file + assert expected["_aws"] == output["_aws"] From 3fbfe6c07a8dde93cb3f4d65564ac5ba2a028d0b Mon Sep 17 00:00:00 2001 From: Tom McCarthy Date: Fri, 5 Jun 2020 23:18:18 +0200 Subject: [PATCH 3/6] chore: remove unused import --- aws_lambda_powertools/metrics/metrics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aws_lambda_powertools/metrics/metrics.py b/aws_lambda_powertools/metrics/metrics.py index 037049e348b..773dfe28c09 100644 --- a/aws_lambda_powertools/metrics/metrics.py +++ b/aws_lambda_powertools/metrics/metrics.py @@ -1,7 +1,6 @@ import functools import json import logging -import os from typing import Any, Callable from aws_lambda_powertools.metrics.base import MetricManager From 40ad89c3d520c6eb92c7e1b1ce5a451173aa2ea7 Mon Sep 17 00:00:00 2001 From: Tom McCarthy Date: Fri, 5 Jun 2020 23:21:20 +0200 Subject: [PATCH 4/6] chore: Correct test comment --- tests/functional/test_metrics.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/functional/test_metrics.py b/tests/functional/test_metrics.py index 786595c3a12..50aacc734e5 100644 --- a/tests/functional/test_metrics.py +++ b/tests/functional/test_metrics.py @@ -499,6 +499,5 @@ def test_namespace_var_precedence(monkeypatch, capsys, metric, dimension): 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 + # THEN namespace should match the explicitly passed variable and not the env var assert expected["_aws"] == output["_aws"] From a3361117a81f5e918868a4707fa3b7bde694e91e Mon Sep 17 00:00:00 2001 From: Tom McCarthy Date: Sat, 6 Jun 2020 19:34:05 +0200 Subject: [PATCH 5/6] chore: fix typo in metrics doc Co-authored-by: Heitor Lessa --- docs/content/core/metrics.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/core/metrics.mdx b/docs/content/core/metrics.mdx index 02c34cdc29c..429ee21649a 100644 --- a/docs/content/core/metrics.mdx +++ b/docs/content/core/metrics.mdx @@ -40,7 +40,7 @@ from aws_lambda_powertools.metrics import Metrics, MetricUnit metrics = Metrics() # highlight-line # Explicit definition -Logger(service="ServerlessAirline") # sets namespace to "ServerlessAirline" +Metrics(service="ServerlessAirline") # sets namespace to "ServerlessAirline" ``` From 8a4eab767b0d7b5e9e4b7ef4843c5e107af5bf27 Mon Sep 17 00:00:00 2001 From: Tom McCarthy Date: Sat, 6 Jun 2020 19:37:55 +0200 Subject: [PATCH 6/6] chore: correct docstring for log_metrics --- aws_lambda_powertools/metrics/metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/metrics/metrics.py b/aws_lambda_powertools/metrics/metrics.py index 773dfe28c09..3dc3d80961f 100644 --- a/aws_lambda_powertools/metrics/metrics.py +++ b/aws_lambda_powertools/metrics/metrics.py @@ -86,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