Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improv: update metrics interface to resemble other core utils #60

Merged
merged 7 commits into from
Jun 6, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions aws_lambda_powertools/metrics/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import numbers
import os
import pathlib
import warnings
from typing import Dict, List, Union

import fastjsonschema
Expand Down Expand Up @@ -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
Expand All @@ -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__)

Expand All @@ -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"
Expand Down
16 changes: 8 additions & 8 deletions aws_lambda_powertools/metrics/metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class SingleMetric(MetricManager):

Environment variables
---------------------
POWERTOOLS_METRICS_NAMESPACE : str
POWERTOOLS_SERVICE_NAME : str
metric namespace

Example
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
-------
Expand All @@ -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")
Expand Down
12 changes: 7 additions & 5 deletions aws_lambda_powertools/metrics/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -48,7 +47,7 @@ def do_something():

Environment variables
---------------------
POWERTOOLS_METRICS_NAMESPACE : str
POWERTOOLS_SERVICE_NAME : str
metric namespace

Parameters
Expand All @@ -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")
Expand Down
30 changes: 18 additions & 12 deletions docs/content/core/metrics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"
to-mc marked this conversation as resolved.
Show resolved Hide resolved


```

You can initialize Metrics anywhere in your code as many time as you need - It'll keep track of your aggregate metrics in memory.
Expand All @@ -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")
Expand All @@ -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")
...
```
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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")

Expand All @@ -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)`.
3 changes: 1 addition & 2 deletions docs/content/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
to-mc marked this conversation as resolved.
Show resolved Hide resolved
**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
Expand Down
2 changes: 1 addition & 1 deletion example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion example/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
97 changes: 93 additions & 4 deletions tests/functional/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,22 +162,22 @@ 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)

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"]


Expand Down Expand Up @@ -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"]