Skip to content

feat: add Metrics middleware using CloudWatch EMF #15

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

Merged
merged 18 commits into from
Apr 9, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion python/.flake8
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[flake8]
exclude = docs, .eggs, setup.py, example, .aws-sam
ignore = E203, E266, W503, BLK100, W291
ignore = E203, E266, W503, BLK100, W291, I004
max-line-length = 120
max-complexity = 18

Expand Down
4 changes: 3 additions & 1 deletion python/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -293,4 +293,6 @@ $RECYCLE.BIN/

# Misc

test_report
test_report
wheelhouse
docs
13 changes: 13 additions & 0 deletions python/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# HISTORY

## April 9th, 2020

**0.6.3**

* Fix `log_metrics` decorator logic not calling the decorated function, and exception handling

## April 8th, 2020

**0.6.1**

* Introduces Metrics middleware to utilise CloudWatch Embedded Metric Format
* Adds deprecation warning for `log_metrics`

## February 20th, 2020

**0.5.0**
Expand Down
28 changes: 20 additions & 8 deletions python/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ lint: format
poetry run flake8

test:
poetry run pytest
poetry run pytest -vvv

test-html:
poetry run pytest --cov-report html
Expand All @@ -25,18 +25,30 @@ pr: lint test
build: pr
poetry run build

docs: dev
poetry run pdoc --html --output-dir docs ./aws_lambda_powertools --force

docs-dev:
poetry run pdoc --http : aws_lambda_powertools

#
# Use `poetry version <major>/<minor></patch>` for version bump
#
release:
release-prod:
poetry config pypi-token.pypi ${PYPI_TOKEN}
@$(MAKE) build
poetry publish
rm -rf dist
poetry publish -n

release-test:
poetry config repositories.testpypi https://test.pypi.org/legacy
poetry config pypi-token.pypi ${PYPI_TEST_TOKEN}
@$(MAKE) build
poetry publish --repository testpypi
rm -rf dist
poetry publish --repository testpypi -n

build-linux-wheels:
poetry build
docker run --env PLAT=manylinux1_x86_64 --rm -it -v ${PWD}:/io -w /io quay.io/pypa/manylinux1_x86_64 /io/build_linux_wheels.sh
cp ./wheelhouse/* dist/ && rm -rf wheelhouse

release:
$(MAKE) build-linux-wheels
$(MAKE) release-test
$(MAKE) release-prod
69 changes: 48 additions & 21 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray,
* Log sampling enables DEBUG log level for a percentage of requests (disabled by default)
- Enable via `POWERTOOLS_LOGGER_SAMPLE_RATE=0.1`, ranges from 0 to 1, where 0.1 is 10% and 1 is 100%

**Metrics**

* Aggregate up to 100 metrics using a single CloudWatch Embedded Metric Format object (large JSON blob)
* Context manager to create an one off metric with a different dimension than metrics already aggregated
* Validate against common metric definitions mistakes (metric unit, values, max dimensions, max metrics, etc)
* No stack, custom resource, data collection needed — Metrics are created async by CloudWatch EMF

**Environment variables** used across suite of utilities

Environment variable | Description | Default | Utility
Expand All @@ -33,6 +40,7 @@ POWERTOOLS_SERVICE_NAME | Sets service name used for tracing namespace, metrics
POWERTOOLS_TRACE_DISABLED | Disables tracing | "false" | tracing
POWERTOOLS_LOGGER_LOG_EVENT | Logs incoming event | "false" | logging
POWERTOOLS_LOGGER_SAMPLE_RATE | Debug log sampling | 0 | logging
POWERTOOLS_METRICS_NAMESPACE | Metrics namespace | None | metrics
LOG_LEVEL | Sets logging level | "INFO" | logging

## Usage
Expand Down Expand Up @@ -148,41 +156,60 @@ def handler(event, context)

#### Custom Metrics async

> **NOTE**: This will **likely change after Beta** in light of [new Amazon CloudWatch embedded metric format](https://aws.amazon.com/about-aws/whats-new/2019/11/amazon-cloudwatch-launches-embedded-metric-format/), meaning we won't need an additional stack and interface could change.
> **NOTE** `log_metric` will be removed once it's GA.

This feature makes use of CloudWatch Embedded Metric Format (EMF) and metrics are created asynchronously by CloudWatch service

> Contrary to `log_metric`, you don't need any custom resource or additional CloudFormation stack anymore.

Metrics middleware validates against the minimum necessary for a metric to be published:

This feature requires [Custom Metrics SAR App](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:374852340823:applications~async-custom-metrics) in order to process canonical metric lines in CloudWatch Logs.
* At least of one Metric and Dimension
* Maximum of 9 dimensions
* Only one Namespace
* [Any Metric unit supported by CloudWatch](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html)

If you're starting from scratch, you may want to see a working example, tune to your needs and deploy within your account - [Serverless Airline Log Processing Stack](https://github.com/aws-samples/aws-serverless-airline-booking/blob/develop/src/backend/log-processing/template.yaml)
**Creating multiple metrics**

`log_metrics` decorator calls the decorated function, so leave that for last decorator or will fail with `SchemaValidationError` if no metrics are recorded.

```python
from aws_lambda_powertools.logging import MetricUnit, log_metric
from aws_lambda_powertools.metrics import Metrics, MetricUnit

def handler(event, context)
log_metric(name="SuccessfulPayment", unit=MetricUnit.Count, value=10, namespace="MyApplication")

# Optional dimensions
log_metric(name="SuccessfulPayment", unit=MetricUnit.Count, value=10, namespace="MyApplication", customer_id="123-abc", charge_id="abc-123")

# Explicit service name
log_metric(service="paymentTest", name="SuccessfulPayment", namespace="MyApplication".....)
...
metrics = Metrics()
metrics.add_namespace(name="ServerlessAirline")
metrics.add_metric(name="ColdStart", unit="Count", value=1)
metrics.add_dimension(name="service", value="booking")

@metrics.log_metrics
@tracer.capture_lambda_handler
def lambda_handler(evt, ctx):
metrics.add_metric(name="BookingConfirmation", unit="Count", value=1)
some_code()
return True

def some_code():
metrics.add_metric(name="some_other_metric", unit=MetricUnit.Seconds, value=1)
...
```

**Exerpt output in CloudWatch Logs**
CloudWatch EMF uses the same dimensions across all metrics. If you have metrics that should have different dimensions, use `single_metric` to create a single metric with any dimension you want. Generally, this would be an edge case since you [pay for unique metric](https://aws.amazon.com/cloudwatch/pricing/)

```
MONITORING|10|Count|SuccessfulPayment|MyApplication|service="payment
MONITORING|10|Count|SuccessfulPayment|MyApplication|customer_id="123-abc",charge_id="abc-123",service="payment
MONITORING|10|Count|SuccessfulPayment|MyApplication|service="paymentTest
```
> unique metric = (metric_name + dimension_name + dimension_value)

```python
from aws_lambda_powertools.metrics import MetricUnit, single_metric

with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1) as metric:
metric.add_dimension(name="function_context", value="$LATEST")
```

## Beta

> **[Progress towards GA](https://github.com/awslabs/aws-lambda-powertools/projects/1)**

This library may change its API/methods or environment variables as it receives feedback from customers. Currently looking for ideas in the following areas before making it stable:

* **Should Tracer patch all possible imported libraries by default or only AWS SDKs?**
- Patching all libraries may have a small performance penalty (~50ms) at cold start
- Alternatively, we could patch only AWS SDK if available and to provide a param to patch multiple `Tracer(modules=("boto3", "requests"))`
* **Create a Tracer provider to support additional tracing**
- Either duck typing or ABC to allow additional tracing providers
2 changes: 2 additions & 0 deletions python/aws_lambda_powertools/logging/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import os
import random
import warnings
from distutils.util import strtobool
from typing import Any, Callable, Dict

Expand Down Expand Up @@ -237,6 +238,7 @@ def log_metric(
keyword arguments as additional dimensions (e.g. customer=customerId)
"""

warnings.warn(message="This method will be removed in GA; use Metrics instead", category=DeprecationWarning)
logger.debug(f"Building new custom metric. Name: {name}, Unit: {unit}, Value: {value}, Dimensions: {dimensions}")
service = os.getenv("POWERTOOLS_SERVICE_NAME") or service
dimensions = __build_dimensions(**dimensions)
Expand Down
17 changes: 17 additions & 0 deletions python/aws_lambda_powertools/metrics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""CloudWatch Embedded Metric Format utility
"""
from aws_lambda_powertools.helper.models import MetricUnit

from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError, UniqueNamespaceError
from .metric import single_metric
from .metrics import Metrics

__all__ = [
"Metrics",
"single_metric",
"MetricUnit",
"MetricUnitError",
"SchemaValidationError",
"MetricValueError",
"UniqueNamespaceError",
]
Loading