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

feat(metrics): add Datadog observability provider #2906

Merged
merged 44 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c940966
add datadog provider
roger-zhangg Aug 1, 2023
8c21a9b
fix poetry lock
roger-zhangg Aug 1, 2023
98f2f05
merging from develop
leandrodamascena Aug 7, 2023
7602e5e
Refactoring Datadog provider with the new BaseProvider
leandrodamascena Aug 9, 2023
68ed6a3
Adding default tags method
leandrodamascena Aug 9, 2023
7790986
Cleaning tests + adding specific comments
leandrodamascena Aug 9, 2023
4acacb7
Fix small things + improving docstring
leandrodamascena Aug 9, 2023
f60a3ba
Fixing minor bugs
leandrodamascena Aug 9, 2023
ccfc641
Merge branch 'develop' into datadog_provider
leandrodamascena Aug 10, 2023
b681d93
Addressing feedback
leandrodamascena Aug 10, 2023
9785e12
rebasing from upstream
leandrodamascena Aug 10, 2023
64e0bbb
Documentation: adding creating metrics
leandrodamascena Aug 10, 2023
84f38d5
Documentation: adding examples
leandrodamascena Aug 10, 2023
ae95c25
Documentation: fixing highlights
leandrodamascena Aug 10, 2023
cf383ac
Documentation: fixing mypy problems 💀
leandrodamascena Aug 10, 2023
a1405af
merging from develop
leandrodamascena Aug 10, 2023
4850ce5
Merge branch 'develop' into datadog_provider
leandrodamascena Aug 11, 2023
15955e7
Addressing Heitor's feedback + improving DX
leandrodamascena Aug 11, 2023
a1c3754
Fix documentantion and add python3.11 as default runtime
leandrodamascena Aug 11, 2023
228001c
Fix documentantion
leandrodamascena Aug 11, 2023
34e9f08
Merge branch 'develop' into datadog_provider
leandrodamascena Aug 14, 2023
4bdf4ff
Moving internal functions to Provider class
leandrodamascena Aug 14, 2023
d8b84de
Adding more information about partners
leandrodamascena Aug 14, 2023
724a2e7
docs(config): collapse by default given nav size
heitorlessa Aug 14, 2023
5e4593c
docs: fix yaml frontmatter issue
heitorlessa Aug 14, 2023
5e95e56
docs: auto-include abbreviations
heitorlessa Aug 14, 2023
6e841c7
docs(nav): move datadog to its own nav
heitorlessa Aug 14, 2023
0467e50
docs(datadog): provide terminologies; feat cleanup
heitorlessa Aug 14, 2023
7c39ded
docs(metrics): correct typo in terminologies
heitorlessa Aug 14, 2023
ed7a567
shorten word
heitorlessa Aug 14, 2023
eb1ee5d
docs(datadog): shorten install
heitorlessa Aug 14, 2023
de11c5c
docs(datadog): simplify add_metrics
heitorlessa Aug 14, 2023
28673c5
docs(datadog): simplify tags, mention new warning
heitorlessa Aug 14, 2023
7888c38
docs(datadog): cleanup default tags
heitorlessa Aug 14, 2023
340b446
docs(datadog): simplify code snippet
heitorlessa Aug 14, 2023
9085e9e
docs(datadog): move forwarder to advanced; cleanup
heitorlessa Aug 14, 2023
f6f20e8
docs(datadog): cleanup flush
heitorlessa Aug 14, 2023
3ed85ee
docs(datadog): correct typo in cold start
heitorlessa Aug 14, 2023
3b0812f
docs: code annotation, move env vars
heitorlessa Aug 14, 2023
f33a719
docs: recommend using Secrets for API Key
heitorlessa Aug 14, 2023
d4c5f8f
Adding tags validation + tests
leandrodamascena Aug 14, 2023
6fd7945
Reverting changes
leandrodamascena Aug 14, 2023
a78b4fc
docs(metrics): add observability providers section
heitorlessa Aug 14, 2023
400486c
Addressing Heitor's feedback
leandrodamascena Aug 14, 2023
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: 2 additions & 0 deletions .markdownlintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
docs/core/metrics/index.md
includes/abbreviations.md
64 changes: 1 addition & 63 deletions aws_lambda_powertools/metrics/functions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import re
from typing import Any, Dict, List
from typing import List

from aws_lambda_powertools.metrics.provider.cloudwatch_emf.exceptions import (
MetricResolutionError,
Expand Down Expand Up @@ -71,64 +70,3 @@ def extract_cloudwatch_metric_unit_value(metric_units: List, metric_valid_option
unit = unit.value

return unit


def serialize_datadog_tags(metric_tags: Dict[str, Any], default_tags: Dict[str, Any]) -> List[str]:
"""
Serialize metric tags into a list of formatted strings for Datadog integration.

This function takes a dictionary of metric-specific tags or default tags.
It parse these tags and converts them into a list of strings in the format "tag_key:tag_value".

Parameters
----------
metric_tags: Dict[str, Any]
A dictionary containing metric-specific tags.
default_tags: Dict[str, Any]
A dictionary containing default tags applicable to all metrics.

Returns:
-------
List[str]
A list of formatted tag strings, each in the "tag_key:tag_value" format.

Example:
>>> metric_tags = {'environment': 'production', 'service': 'web'}
>>> serialize_datadog_tags(metric_tags, None)
['environment:production', 'service:web']
"""
tags = metric_tags or default_tags

return [f"{tag_key}:{tag_value}" for tag_key, tag_value in tags.items()]


def validate_datadog_metric_name(metric_name: str) -> bool:
"""
Validate a metric name according to specific requirements.

Metric names must start with a letter.
Metric names must only contain ASCII alphanumerics, underscores, and periods.
Other characters, including spaces, are converted to underscores.
Unicode is not supported.
Metric names must not exceed 200 characters. Fewer than 100 is preferred from a UI perspective.

More information here: https://docs.datadoghq.com/metrics/custom_metrics/#naming-custom-metrics

Parameters:
----------
metric_name: str
The metric name to be validated.

Returns:
-------
bool
True if the metric name is valid, False otherwise.
"""

# Check if the metric name starts with a letter
# Check if the metric name contains more than 200 characters
# Check if the resulting metric name only contains ASCII alphanumerics, underscores, and periods
if not metric_name[0].isalpha() or len(metric_name) > 200 or not re.match(r"^[a-zA-Z0-9_.]+$", metric_name):
return False

return True
104 changes: 100 additions & 4 deletions aws_lambda_powertools/metrics/provider/datadog/datadog.py
leandrodamascena marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@
import logging
import numbers
import os
import re
import time
import warnings
from typing import Any, Callable, Dict, List, Optional

from aws_lambda_powertools.metrics.exceptions import MetricValueError, SchemaValidationError
from aws_lambda_powertools.metrics.functions import serialize_datadog_tags, validate_datadog_metric_name
from aws_lambda_powertools.metrics.provider import BaseProvider
from aws_lambda_powertools.metrics.provider.datadog.warnings import DatadogDataValidationWarning
from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.functions import resolve_env_var_choice
from aws_lambda_powertools.utilities.typing import LambdaContext

METRIC_NAME_REGEX = re.compile(r"^[a-zA-Z0-9_.]+$")

logger = logging.getLogger(__name__)

# Check if using datadog layer
Expand Down Expand Up @@ -99,13 +102,16 @@ def add_metric(
"""

# validating metric name
if not validate_datadog_metric_name(name):
if not self._validate_datadog_metric_name(name):
docs = "https://docs.datadoghq.com/metrics/custom_metrics/#naming-custom-metrics"
raise SchemaValidationError(
f"Invalid metric name. Please ensure the metric {name} follows the requirements. \n"
f"See Datadog documentation here: \n {docs}",
)

# validating metric tag
self._validate_datadog_tags_name(**tags)
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved

if not isinstance(value, numbers.Real):
raise MetricValueError(f"{value} is not a valid number")

Expand Down Expand Up @@ -158,7 +164,7 @@ def serialize_metric_set(self, metrics: List | None = None) -> List:
"m": metric_name,
"v": single_metric["v"],
"e": single_metric["e"],
"t": serialize_datadog_tags(metric_tags=single_metric["t"], default_tags=self.default_tags),
"t": self._serialize_datadog_tags(metric_tags=single_metric["t"], default_tags=self.default_tags),
},
)

Expand Down Expand Up @@ -233,7 +239,8 @@ def log_metrics(
-------
**Lambda function using tracer and metrics decorators**

from aws_lambda_powertools import DatadogMetrics, Tracer
from aws_lambda_powertools import Tracer
from aws_lambda_powertools.metrics.provider.datadog import DatadogMetrics

metrics = DatadogMetrics(namespace="powertools")
tracer = Tracer(service="payment")
Expand Down Expand Up @@ -292,4 +299,93 @@ def set_default_tags(self, **tags) -> None:
def lambda_handler():
return True
"""
self._validate_datadog_tags_name(**tags)
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved
self.default_tags.update(**tags)

@staticmethod
def _serialize_datadog_tags(metric_tags: Dict[str, Any], default_tags: Dict[str, Any]) -> List[str]:
"""
Serialize metric tags into a list of formatted strings for Datadog integration.

This function takes a dictionary of metric-specific tags or default tags.
It parse these tags and converts them into a list of strings in the format "tag_key:tag_value".

Parameters
----------
metric_tags: Dict[str, Any]
A dictionary containing metric-specific tags.
default_tags: Dict[str, Any]
A dictionary containing default tags applicable to all metrics.

Returns:
-------
List[str]
A list of formatted tag strings, each in the "tag_key:tag_value" format.

Example:
>>> metric_tags = {'environment': 'production', 'service': 'web'}
>>> serialize_datadog_tags(metric_tags, None)
['environment:production', 'service:web']
"""
tags = metric_tags or default_tags

return [f"{tag_key}:{tag_value}" for tag_key, tag_value in tags.items()]

@staticmethod
def _validate_datadog_tags_name(**tags):
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved
"""
Validate a metric tag according to specific requirements.

Metric tags must start with a letter.
Metric tags must not exceed 200 characters. Fewer than 100 is preferred from a UI perspective.

More information here: https://docs.datadoghq.com/getting_started/tagging/#define-tags

Parameters:
----------
tags: Dict
The metric tags to be validated.
"""
for tag_key, tag_value in tags.items():
tag = f"{tag_key}:{tag_value}"
if not tag[0].isalpha() or len(tag) > 200:
docs = "https://docs.datadoghq.com/getting_started/tagging/#define-tags"
warnings.warn(
f"Invalid tag value. Please ensure the specific tag {tag} follows the requirements. \n"
f"May incur data loss for metrics. \n"
f"See Datadog documentation here: \n {docs}",
DatadogDataValidationWarning,
stacklevel=2,
)

@staticmethod
def _validate_datadog_metric_name(metric_name: str) -> bool:
"""
Validate a metric name according to specific requirements.

Metric names must start with a letter.
Metric names must only contain ASCII alphanumerics, underscores, and periods.
Other characters, including spaces, are converted to underscores.
Unicode is not supported.
Metric names must not exceed 200 characters. Fewer than 100 is preferred from a UI perspective.

More information here: https://docs.datadoghq.com/metrics/custom_metrics/#naming-custom-metrics

Parameters:
----------
metric_name: str
The metric name to be validated.

Returns:
-------
bool
True if the metric name is valid, False otherwise.
"""

# Check if the metric name starts with a letter
# Check if the metric name contains more than 200 characters
# Check if the resulting metric name only contains ASCII alphanumerics, underscores, and periods
if not metric_name[0].isalpha() or len(metric_name) > 200 or not METRIC_NAME_REGEX.match(metric_name):
return False

return True
4 changes: 2 additions & 2 deletions aws_lambda_powertools/metrics/provider/datadog/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class DatadogMetrics:
-------
**Creates a few metrics and publish at the end of a function execution**

from aws_lambda_powertools import DatadogMetrics
from aws_lambda_powertools.metrics.provider.datadog import DatadogMetrics

metrics = DatadogMetrics(namespace="ServerlessAirline")

Expand All @@ -33,7 +33,7 @@ def lambda_handler():
Parameters
----------
flush_to_log : bool, optional
Used when using export instead of extension
Used when using export instead of Lambda Extension
namespace : str, optional
Namespace for metrics
provider: DatadogProvider, optional
Expand Down
8 changes: 8 additions & 0 deletions aws_lambda_powertools/metrics/provider/datadog/warnings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class DatadogDataValidationWarning(Warning):
message: str

def __init__(self, message: str):
self.message = message

def __str__(self) -> str:
return self.message
30 changes: 22 additions & 8 deletions docs/core/metrics.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: CloudWatch EMF
title: Amazon CloudWatch EMF Metrics
description: Core utility
---

Expand All @@ -16,7 +16,7 @@ These metrics can be visualized through [Amazon CloudWatch Console](https://cons

## Terminologies

If you're new to Amazon CloudWatch, there are two terminologies you must be aware of before using this utility:
If you're new to Amazon CloudWatch, there are five terminologies you must be aware of before using this utility:

* **Namespace**. It's the highest level container that will group multiple metrics from multiple services for a given application, for example `ServerlessEcommerce`.
* **Dimensions**. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example `ColdStart` metric by Payment `service`.
Expand Down Expand Up @@ -197,9 +197,9 @@ This has the advantage of keeping cold start metric separate from your applicati

The following environment variable is available to configure Metrics at a global scope:

| Setting | Description | Environment variable | Default |
|--------------------|------------------------------------------------------------------------------|-----------------------------------------|---------|
| **Namespace Name** | Sets namespace used for metrics. | `POWERTOOLS_METRICS_NAMESPACE` | `None` |
| Setting | Description | Environment variable | Default |
| ------------------ | -------------------------------- | ------------------------------ | ------- |
| **Namespace Name** | Sets namespace used for metrics. | `POWERTOOLS_METRICS_NAMESPACE` | `None` |

`POWERTOOLS_METRICS_NAMESPACE` is also available on a per-instance basis with the `namespace` parameter, which will consequently override the environment variable value.

Expand Down Expand Up @@ -286,9 +286,9 @@ You can use `EphemeralMetrics` class when looking to isolate multiple instances

`EphemeralMetrics` has only one difference while keeping nearly the exact same set of features:

| Feature | Metrics | EphemeralMetrics |
| ----------------------------------------------------------------------------------------------------------- | ------- | ---------------- |
| **Share data across instances** (metrics, dimensions, metadata, etc.) | Yes | - |
| Feature | Metrics | EphemeralMetrics |
| --------------------------------------------------------------------- | ------- | ---------------- |
| **Share data across instances** (metrics, dimensions, metadata, etc.) | Yes | - |

!!! question "Why not changing the default `Metrics` behaviour to not share data across instances?"

Expand Down Expand Up @@ -327,6 +327,20 @@ These issues are exacerbated when you create **(A)** metric dimensions condition

That is why `Metrics` shares data across instances by default, as that covers 80% of use cases and different personas using Powertools. This allows them to instantiate `Metrics` in multiple places throughout their code - be a separate file, a middleware, or an abstraction that sets default dimensions.

### Observability providers

> An observability provider is an [AWS Lambda Partner](https://docs.aws.amazon.com/lambda/latest/dg/extensions-api-partners.html){target="_blank" rel="nofollow"} offering a platform for logging, metrics, traces, etc.

We provide a thin-wrapper on top of the most requested observability providers. We strive to keep a similar UX as close as possible while keeping our value add features.

!!! tip "Missing your preferred provider? Please create a [feature request](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=feature-request%2Ctriage&projects=&template=feature_request.yml&title=Feature+request%3A+TITLE){target="_blank"}."

Current providers:

| Provider | Notes |
| ------------------------------------- | -------------------------------------------------------- |
| [Datadog](./datadog){target="_blank"} | Uses Datadog SDK and Datadog Lambda Extension by default |

## Testing your code

### Setting environment variables
Expand Down
Loading
Loading