Skip to content

Commit 15af6ca

Browse files
author
Tom McCarthy
authored
Merge pull request #60 from awslabs/improv/metrics_interface
improv: update metrics interface to resemble other core utils
2 parents 38ad17e + d4fc869 commit 15af6ca

File tree

8 files changed

+135
-36
lines changed

8 files changed

+135
-36
lines changed

Diff for: aws_lambda_powertools/metrics/base.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import numbers
55
import os
66
import pathlib
7+
import warnings
78
from typing import Dict, List, Union
89

910
import fastjsonschema
@@ -34,7 +35,7 @@ class MetricManager:
3435
3536
Environment variables
3637
---------------------
37-
POWERTOOLS_METRICS_NAMESPACE : str
38+
POWERTOOLS_SERVICE_NAME : str
3839
metric namespace to be set for all metrics
3940
4041
Raises
@@ -52,7 +53,7 @@ class MetricManager:
5253
def __init__(self, metric_set: Dict[str, str] = None, dimension_set: Dict = None, namespace: str = None):
5354
self.metric_set = metric_set if metric_set is not None else {}
5455
self.dimension_set = dimension_set if dimension_set is not None else {}
55-
self.namespace = os.getenv("POWERTOOLS_METRICS_NAMESPACE") or namespace
56+
self.namespace = namespace or os.getenv("POWERTOOLS_SERVICE_NAME")
5657
self._metric_units = [unit.value for unit in MetricUnit]
5758
self._metric_unit_options = list(MetricUnit.__members__)
5859

@@ -70,6 +71,9 @@ def add_namespace(self, name: str):
7071
name : str
7172
Metric namespace
7273
"""
74+
warnings.warn(
75+
"add_namespace method is deprecated. Pass service to Metrics constructor instead", DeprecationWarning
76+
)
7377
if self.namespace is not None:
7478
raise UniqueNamespaceError(
7579
f"Namespace '{self.namespace}' already set - Only one namespace is allowed across metrics"

Diff for: aws_lambda_powertools/metrics/metric.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class SingleMetric(MetricManager):
2121
2222
Environment variables
2323
---------------------
24-
POWERTOOLS_METRICS_NAMESPACE : str
24+
POWERTOOLS_SERVICE_NAME : str
2525
metric namespace
2626
2727
Example
@@ -30,9 +30,8 @@ class SingleMetric(MetricManager):
3030
3131
from aws_lambda_powertools.metrics import SingleMetric, MetricUnit
3232
import json
33-
metric = Single_Metric()
33+
metric = Single_Metric(service="ServerlessAirline")
3434
35-
metric.add_namespace(name="ServerlessAirline")
3635
metric.add_metric(name="ColdStart", unit=MetricUnit.Count, value=1)
3736
metric.add_dimension(name="function_version", value=47)
3837
@@ -63,7 +62,7 @@ def add_metric(self, name: str, unit: MetricUnit, value: float):
6362

6463

6564
@contextmanager
66-
def single_metric(name: str, unit: MetricUnit, value: float):
65+
def single_metric(name: str, unit: MetricUnit, value: float, service: str = None):
6766
"""Context manager to simplify creation of a single metric
6867
6968
Example
@@ -72,13 +71,12 @@ def single_metric(name: str, unit: MetricUnit, value: float):
7271
7372
from aws_lambda_powertools.metrics import single_metric, MetricUnit
7473
75-
with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1) as metric:
76-
metric.add_namespace(name="ServerlessAirline")
74+
with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1, service="ServerlessAirline") as metric:
7775
metric.add_dimension(name="function_version", value=47)
7876
7977
**Same as above but set namespace using environment variable**
8078
81-
$ export POWERTOOLS_METRICS_NAMESPACE="ServerlessAirline"
79+
$ export POWERTOOLS_SERVICE_NAME="ServerlessAirline"
8280
8381
from aws_lambda_powertools.metrics import single_metric, MetricUnit
8482
@@ -93,6 +91,8 @@ def single_metric(name: str, unit: MetricUnit, value: float):
9391
`aws_lambda_powertools.helper.models.MetricUnit`
9492
value : float
9593
Metric value
94+
service: str
95+
Service name used as namespace
9696
9797
Yields
9898
-------
@@ -106,7 +106,7 @@ def single_metric(name: str, unit: MetricUnit, value: float):
106106
"""
107107
metric_set = None
108108
try:
109-
metric: SingleMetric = SingleMetric()
109+
metric: SingleMetric = SingleMetric(namespace=service)
110110
metric.add_metric(name=name, unit=unit, value=value)
111111
yield metric
112112
logger.debug("Serializing single metric")

Diff for: aws_lambda_powertools/metrics/metrics.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ class Metrics(MetricManager):
2929
3030
from aws_lambda_powertools.metrics import Metrics
3131
32-
metrics = Metrics()
33-
metrics.add_namespace(name="ServerlessAirline")
32+
metrics = Metrics(service="ServerlessAirline")
3433
metrics.add_metric(name="ColdStart", unit=MetricUnit.Count, value=1)
3534
metrics.add_metric(name="BookingConfirmation", unit="Count", value=1)
3635
metrics.add_dimension(name="service", value="booking")
@@ -48,7 +47,7 @@ def do_something():
4847
4948
Environment variables
5049
---------------------
51-
POWERTOOLS_METRICS_NAMESPACE : str
50+
POWERTOOLS_SERVICE_NAME : str
5251
metric namespace
5352
5453
Parameters
@@ -65,10 +64,13 @@ def do_something():
6564
_metrics = {}
6665
_dimensions = {}
6766

68-
def __init__(self):
67+
def __init__(
68+
self, service: str = None,
69+
):
6970
self.metric_set = self._metrics
7071
self.dimension_set = self._dimensions
71-
super().__init__(metric_set=self.metric_set, dimension_set=self.dimension_set)
72+
self.service = service
73+
super().__init__(metric_set=self.metric_set, dimension_set=self.dimension_set, namespace=self.service)
7274

7375
def clear_metrics(self):
7476
logger.debug("Clearing out existing metric set from memory")
@@ -84,7 +86,7 @@ def log_metrics(self, lambda_handler: Callable[[Any, Any], Any] = None):
8486
-------
8587
**Lambda function using tracer and metrics decorators**
8688
87-
metrics = Metrics()
89+
metrics = Metrics(service="payment")
8890
tracer = Tracer(service="payment")
8991
9092
@tracer.capture_lambda_handler

Diff for: docs/content/core/metrics.mdx

+18-12
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Metrics creates custom metrics asynchronously via logging metrics to standard ou
1616

1717
## Initialization
1818

19-
Set `POWERTOOLS_METRICS_NAMESPACE` env var as a start - Here is an example using AWS Serverless Application Model (SAM)
19+
Set `POWERTOOLS_SERVICE_NAME` env var as a start - Here is an example using AWS Serverless Application Model (SAM)
2020

2121
```yaml:title=template.yaml
2222
Resources:
@@ -27,16 +27,22 @@ Resources:
2727
Runtime: python3.8
2828
Environment:
2929
Variables:
30-
POWERTOOLS_METRICS_NAMESPACE: ServerlessAirline # highlight-line
30+
POWERTOOLS_SERVICE_NAME: ServerlessAirline # highlight-line
3131
```
3232

3333
We recommend you use your application or main service as a metric namespace.
34+
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.
3435

3536
```python:title=app.py
3637
from aws_lambda_powertools.metrics import Metrics, MetricUnit
3738

38-
metrics = Metrics()
39-
# metrics.add_namespace("ServerlessAirline") # optionally if you set via env var
39+
# POWERTOOLS_SERVICE_NAME defined
40+
metrics = Metrics() # highlight-line
41+
42+
# Explicit definition
43+
Metrics(service="ServerlessAirline") # sets namespace to "ServerlessAirline"
44+
45+
4046
```
4147

4248
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
4854
```python:title=app.py
4955
from aws_lambda_powertools.metrics import Metrics, MetricUnit
5056

51-
metrics = Metrics()
57+
metrics = Metrics(service="ExampleService")
5258
# highlight-start
5359
metrics.add_metric(name="ColdStart", unit=MetricUnit.Count, value=1)
5460
metrics.add_dimension(name="service", value="booking")
@@ -73,7 +79,7 @@ CloudWatch EMF uses the same dimensions across all your metrics. Use `single_met
7379
```python:title=single_metric.py
7480
from aws_lambda_powertools.metrics import MetricUnit, single_metric
7581

76-
with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1) as metric: # highlight-line
82+
with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1, service="ExampleService") as metric: # highlight-line
7783
metric.add_dimension(name="function_context", value="$LATEST")
7884
...
7985
```
@@ -85,7 +91,7 @@ As you finish adding all your metrics, you need to serialize and flush them to s
8591
```python:title=lambda_handler.py
8692
from aws_lambda_powertools.metrics import Metrics, MetricUnit
8793

88-
metrics = Metrics()
94+
metrics = Metrics(service="ExampleService")
8995
metrics.add_metric(name="ColdStart", unit="Count", value=1)
9096

9197
@metrics.log_metrics # highlight-line
@@ -109,7 +115,7 @@ def lambda_handler(evt, ctx):
109115
```python:title=lambda_handler_nested_middlewares.py
110116
from aws_lambda_powertools.metrics import Metrics, MetricUnit
111117

112-
metrics = Metrics()
118+
metrics = Metrics(service="ExampleService")
113119
metrics.add_metric(name="ColdStart", unit="Count", value=1)
114120

115121
# highlight-start
@@ -130,7 +136,7 @@ If you prefer not to use `log_metrics` because you might want to encapsulate add
130136
import json
131137
from aws_lambda_powertools.metrics import Metrics, MetricUnit
132138

133-
metrics = Metrics()
139+
metrics = Metrics(service="ExampleService")
134140
metrics.add_metric(name="ColdStart", unit="Count", value=1)
135141
metrics.add_dimension(name="service", value="booking")
136142

@@ -143,10 +149,10 @@ print(json.dumps(your_metrics_object))
143149

144150
## Testing your code
145151

146-
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.
152+
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.
147153

148154
```bash:title=pytest_metric_namespace.sh
149-
POWERTOOLS_METRICS_NAMESPACE="Example" python -m pytest
155+
POWERTOOLS_SERVICE_NAME="Example" python -m pytest
150156
```
151157

152-
You can ignore that if you are explicitly creating metric namespace within your own code `metrics.add_namespace()`.
158+
You can ignore this if you are explicitly setting namespace by passing a service name when initializing Metrics: `metrics = Metrics(service=ServiceName)`.

Diff for: docs/content/index.mdx

+1-2
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,11 @@ _`*` Core utilities are Tracer, Logger and Metrics. Optional utilities may vary
3636

3737
Environment variable | Description | Utility
3838
------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------
39-
**POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics dimensions and structured logging | all
39+
**POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics namespace and structured logging | all
4040
**POWERTOOLS_TRACE_DISABLED** | Disables tracing | [Tracing](./core/tracer)
4141
**POWERTOOLS_TRACE_MIDDLEWARES** | Creates sub-segment for each custom middleware | [middleware_factory](./utilities/middleware_factory)
4242
**POWERTOOLS_LOGGER_LOG_EVENT** | Logs incoming event | [Logging](./core/logger)
4343
**POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logging](./core/logger)
44-
**POWERTOOLS_METRICS_NAMESPACE** | Metrics namespace | [Metrics](./core/metrics)
4544
**LOG_LEVEL** | Sets logging level | [Logging](./core/logger)
4645

4746
## Debug mode

Diff for: example/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ This example uses both [tracing](https://github.com/awslabs/aws-lambda-powertool
1010
* **Unit Tests**: We recommend proceeding with the following commands in a virtual environment
1111
- **Install deps**: `pip install -r hello_world/requirements.txt && pip install -r requirements-dev.txt`
1212
- **Run tests with tracing disabled and namespace set**
13-
- `POWERTOOLS_METRICS_NAMESPACE="Example" POWERTOOLS_TRACE_DISABLED=1 python -m pytest`
13+
- `POWERTOOLS_SERVICE_NAME="Example" POWERTOOLS_TRACE_DISABLED=1 python -m pytest`
1414
- 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.
1515

1616
# Example code

Diff for: example/template.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ Resources:
2424
POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default
2525
POWERTOOLS_LOGGER_LOG_EVENT: "false" # Logs incoming event, default
2626
POWERTOOLS_LOGGER_SAMPLE_RATE: "0" # Debug log sampling percentage, default
27-
POWERTOOLS_METRICS_NAMESPACE: "Example" # Metric Namespace
2827
LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default
2928
Events:
3029
HelloWorld:

Diff for: tests/functional/test_metrics.py

+93-4
Original file line numberDiff line numberDiff line change
@@ -162,22 +162,22 @@ def lambda_handler(evt, ctx):
162162

163163

164164
def test_namespace_env_var(monkeypatch, capsys, metric, dimension, namespace):
165-
# GIVEN we use POWERTOOLS_METRICS_NAMESPACE
166-
monkeypatch.setenv("POWERTOOLS_METRICS_NAMESPACE", namespace["name"])
165+
# GIVEN we use POWERTOOLS_SERVICE_NAME
166+
monkeypatch.setenv("POWERTOOLS_SERVICE_NAME", namespace["name"])
167167

168168
# WHEN creating a metric but don't explicitly
169169
# add a namespace
170170
with single_metric(**metric) as my_metrics:
171171
my_metrics.add_dimension(**dimension)
172-
monkeypatch.delenv("POWERTOOLS_METRICS_NAMESPACE")
172+
monkeypatch.delenv("POWERTOOLS_SERVICE_NAME")
173173

174174
output = json.loads(capsys.readouterr().out.strip())
175175
expected = serialize_single_metric(metric=metric, dimension=dimension, namespace=namespace)
176176

177177
remove_timestamp(metrics=[output, expected]) # Timestamp will always be different
178178

179179
# THEN we should add a namespace implicitly
180-
# with the value of POWERTOOLS_METRICS_NAMESPACE env var
180+
# with the value of POWERTOOLS_SERVICE_NAME env var
181181
assert expected["_aws"] == output["_aws"]
182182

183183

@@ -412,3 +412,92 @@ def lambda_handler(evt, ctx):
412412
# and dimension values hould be serialized as strings
413413
for dimension in non_str_dimensions:
414414
assert isinstance(output[dimension["name"]], str)
415+
416+
417+
def test_add_namespace_warns_for_deprecation(capsys, metrics, dimensions, namespace):
418+
# GIVEN Metrics is initialized
419+
my_metrics = Metrics()
420+
with pytest.deprecated_call():
421+
my_metrics.add_namespace(**namespace)
422+
423+
424+
def test_log_metrics_with_explicit_service(capsys, metrics, dimensions):
425+
# GIVEN Metrics is initialized with service specified
426+
my_metrics = Metrics(service="test_service")
427+
for metric in metrics:
428+
my_metrics.add_metric(**metric)
429+
for dimension in dimensions:
430+
my_metrics.add_dimension(**dimension)
431+
432+
# WHEN we utilize log_metrics to serialize
433+
# and flush all metrics at the end of a function execution
434+
@my_metrics.log_metrics
435+
def lambda_handler(evt, ctx):
436+
return True
437+
438+
lambda_handler({}, {})
439+
440+
output = json.loads(capsys.readouterr().out.strip())
441+
expected = serialize_metrics(metrics=metrics, dimensions=dimensions, namespace={"name": "test_service"})
442+
443+
remove_timestamp(metrics=[output, expected]) # Timestamp will always be different
444+
445+
# THEN we should have no exceptions and the namespace should be set to the name provided in the
446+
# service passed to Metrics constructor
447+
assert expected["_aws"] == output["_aws"]
448+
449+
450+
def test_log_metrics_with_namespace_overridden(capsys, metrics, dimensions):
451+
# GIVEN Metrics is initialized with service specified
452+
my_metrics = Metrics(service="test_service")
453+
for metric in metrics:
454+
my_metrics.add_metric(**metric)
455+
for dimension in dimensions:
456+
my_metrics.add_dimension(**dimension)
457+
458+
# WHEN we try to call add_namespace
459+
# THEN we should raise UniqueNamespaceError exception
460+
@my_metrics.log_metrics
461+
def lambda_handler(evt, ctx):
462+
my_metrics.add_namespace(name="new_namespace")
463+
return True
464+
465+
with pytest.raises(UniqueNamespaceError):
466+
lambda_handler({}, {})
467+
468+
with pytest.raises(UniqueNamespaceError):
469+
my_metrics.add_namespace(name="another_new_namespace")
470+
471+
472+
def test_single_metric_with_service(capsys, metric, dimension):
473+
# GIVEN we pass service parameter to single_metric
474+
475+
# WHEN creating a metric
476+
with single_metric(**metric, service="test_service") as my_metrics:
477+
my_metrics.add_dimension(**dimension)
478+
479+
output = json.loads(capsys.readouterr().out.strip())
480+
expected = serialize_single_metric(metric=metric, dimension=dimension, namespace={"name": "test_service"})
481+
482+
remove_timestamp(metrics=[output, expected]) # Timestamp will always be different
483+
484+
# THEN namespace should match value passed as service
485+
assert expected["_aws"] == output["_aws"]
486+
487+
488+
def test_namespace_var_precedence(monkeypatch, capsys, metric, dimension):
489+
# GIVEN we use POWERTOOLS_SERVICE_NAME
490+
monkeypatch.setenv("POWERTOOLS_SERVICE_NAME", "test_service_env_var")
491+
492+
# WHEN creating a metric and explicitly set a service name
493+
with single_metric(**metric, service="test_service_explicit") as my_metrics:
494+
my_metrics.add_dimension(**dimension)
495+
monkeypatch.delenv("POWERTOOLS_SERVICE_NAME")
496+
497+
output = json.loads(capsys.readouterr().out.strip())
498+
expected = serialize_single_metric(metric=metric, dimension=dimension, namespace={"name": "test_service_explicit"})
499+
500+
remove_timestamp(metrics=[output, expected]) # Timestamp will always be different
501+
502+
# THEN namespace should match the explicitly passed variable and not the env var
503+
assert expected["_aws"] == output["_aws"]

0 commit comments

Comments
 (0)