Skip to content

RFC: Support for external observability providers - Metrics #2015

Closed
@roger-zhangg

Description

@roger-zhangg

Is this related to an existing feature request or issue?

#1433
#2014

Which AWS Lambda Powertools utility does this relate to?

Metrics

Summary

Problem

Customers has been using Lambda Powertools alongside with third party observability provider for Logging, metrics and tracing. Focusing on metrics part (this RFC is a part of support for observability provider. For Logging please check out this RFC), while Powertools for AWS Lambda (Powertools) provided powerful and easy to use metrics class. However, Powertools only supports AWS CloudWatch Embedded Metric Format (EMF), which is unfriendly to third party observability provider, bringing hardship to our customers when trying to ingest this format into other observability solutions, such as DataDog.

Goal

Goal for this RFC is to enable third party observability provider like Datadog, NewRelic, Lumigo, etc. to create their own Powertools specific metric provider and offer to their own customers. e.g., DataDog metric format. And if customer eventually want to add additional value or add features themselves. They can easily do so by overriding a provider too.

Use case

Typical use case of this is utility would be for customers who use Lambda Powertools to collect metrics and want to have them ingested into a third party observability provider.

Proposal

Current metric experience

The current Powertools’ metrics utility creates custom metrics asynchronously by logging metrics to standard output following Amazon CloudWatch Embedded Metric Format (EMF), and these metrics can then be seen in the CloudWatch console. This utility can aggregate up to 100 metrics using a single CloudWatch EMF object (which is in JSON format).

from aws_lambda_powertools import Metrics
from aws_lambda_powertools.metrics import MetricUnit
from aws_lambda_powertools.utilities.typing import LambdaContext


# Current experience in using metrics should not change, i.e. these metrics would still output to CW
metrics = Metrics()

@metrics.log_metrics # ensures metrics are flushed upon request completion/failure
def lambda_handler(event: dict, context: LambdaContext):
    metrics.add_metric(name="SuccessfulBooking", unit=MetricUnit.Count, value=1)

# JSON output
#     "_aws": {
#         "Timestamp": 1656685750622,
#         "CloudWatchMetrics": [
#             {
#                 "Namespace": "ServerlessAirline",
#                 "Dimensions": [
#                     [
#                         "environment",
#                         "service"
#                     ]
#                 ],
#                 "Metrics": [
#                     {
#                         "Name": "TurbineReads",
#                         "Unit": "Count"
#                     }
#                 ]
#             }
#         ]
#     },
#     "environment": "dev",
#     "service": "booking",
#     "TurbineReads": [
#         1.0,
#         8.0
#     ]
# }

Metric provider proposal

For this new utility, we propose a new metrics class for observability providers. With an optional parameter provider to allow developers to pass in observability provider pre-defined or user custom provider. And the output will be in observability provider friendly format. The below code snippet is a rudimentary look at how this utility can be used and how it will function.

The default use case for metrics before is metrics=Metrics()
After we have this provider feature, Customers can still use original CloudWatch Metrics by metrics=Metrics() or metrics=CloudWatchEMF(). They can also use provider for third party provider by e.g.:Metrics=DataDogMetrics()

from aws_lambda_powertools import Metrics
from aws_lambda_powertools.metrics import MetricUnit
from aws_lambda_powertools.utilities.typing import LambdaContext
from aws_lambda_powertools.metrics.provider import DatadogMetrics
from aws_lambda_powertools.metrics.provider import DatadogMetricsProvider

# Use datadog-defined metrics provider
provider = DatadogMetricsProvider()
metrics = DatadogMetrics(provider=provider)

@metrics.log_metrics
def lambda_handler(event: dict, context: LambdaContext):
    metrics.add_metric(name="SuccessfulBooking", value=1, tags={"product":"ticket","ordier":"online"})
    
# JSON output to log
# {
#     "m": "SuccessfulBooking", 
#     "v": 1,
#     "e": 1678509106, 
#     "t": "['product:ticket', 'order:online']"
# }

self-defined metrics provider usage

If the customer would like to use another observability provider, or define their own metrics functions, we will define an interface that the customer can implement and pass in to the Metrics class provider parameter

from aws_lambda_powertools.metrics.provider import MetricsBase, MetricsProviderBase


class DataDogProvider(MetricsProviderBase):
    def __init__(self, namespace):

    def add_metric(self, name: str, value: float, timestamp: Optional[int] = None, tags: Optional[List] = None):
        self.metrics.append({"m": name, "v": int(value), "e": timestamp, "t": tags})

    def serialize(self) -> Dict:
        # logic here is to add dimension and metadata to each metric's tag with "key:value" format
        extra_tags: List = []
        output_list: List = []

        for single_metric in self.metrics:
            output_list.append(
                {
                    "m": f"{self.namespace}.{single_metric['m']}",
                    "v": single_metric["v"],
                    "e": single_metric["e"],
                    "t": single_metric["t"] + extra_tags,
                }
            )
        return {"List": output_list}

    def flush(self, metrics):
        # flush

    def clear(self):


class DataDogMetrics(MetricsBase):
    """Class for datadog metrics standalone class.

    Example
    -------
    dd_provider = DataDogProvider(namespace="default")
    metrics = DataDogMetrics(provider=dd_provider)

    @metrics.log_metrics(capture_cold_start_metric: bool = True, raise_on_empty_metrics: bool = False)
    def lambda_handler(event, context)
        metrics.add_metric(name="item_sold",value=1,tags)
    """

    # `log_metrics` and `_add_cold_start_metric` are directly inherited from `MetricsBase`
    def __init__(self, provider):
        self.provider = provider
        super().__init__()

    def add_metric(self, name: str, value: float, timestamp: Optional[int] = None, tags: Optional[List] = None):
        self.provider.add_metric(name=name, value=value, timestamp=timestamp, tags=tags)

    def flush_metrics(self, raise_on_empty_metrics: bool = False) -> None:
        metrics = self.provider.serialize()
        if not metrics and raise_on_empty_metrics:
            warnings.warn(
                "No application metrics to publish. The cold-start metric may be published if enabled. "
                "If application metrics should never be empty, consider using 'raise_on_empty_metrics'",
                stacklevel=2,
            )
        self.provider.flush(metrics)
        self.provider.clear()
         
    
metrics = DataDogMetrics(provider=DataDogProvider())

@metrics.log_metrics
def lambda_handler(event: dict, context: LambdaContext):
    metrics.add_metric(name="SuccessfulBooking", value=1, tags={"product":"ticket","ordier":"online"})
    # call functions in provider
    metrics.provider.add_tag({"test":True})       

# JSON output to log
# {
#     "m": "SuccessfulBooking", 
#     "v": 1,
#     "e": 1678509106, 
#     "t": "['product:ticket', 'order:online']"
# }

Out of scope

Introduction of third party observability provider dependency to submit metrics to API is out of scope. The configuration included in this project should only support modifying the metrics outputted to log. But on their end, customer can still submit to API if they implement observability provider SDK in metrics' flush function

Potential challenges

How to provide more value to customer with current Metrics provider design

  • Currently we just provided log_metrics decorator and _add_cold_start_metric function in the base metrics provider function.
  • Other than these two are just basic framework of metrics class.
  • What else should we include in the metrics class to provide more value to our customer?

Dependencies and Integrations

Dependencies

No additional dependencies required

Changes and Additions to Public Interfaces

  • Modification in Metrics, MetricsManager Class
    • Add an optional provider parameter in metrics class.
    • Refactor the Metrics Class to enable use of provider. But all currently exist function will have same output.
    • Provide a AWS CloudWatch EMF Metrics provider as default metrics provider.

Performance Impact

Little or no performance impact expected

Backwards Compatibility and Upgrade Path

Full backward compatibility, old metrics codes without provider parameter will get exact same output as before.

Alternative solutions

Acknowledgment

Metadata

Metadata

Type

No type

Projects

Status

Shipped

Relationships

None yet

Development

No branches or pull requests

Issue actions