Skip to content

Commit 90fbae6

Browse files
feat: add ldms feature (#8051)
* feat: add ldms service * feat: add ldms service * feat: add ldms service
1 parent 1047397 commit 90fbae6

File tree

14 files changed

+775
-0
lines changed

14 files changed

+775
-0
lines changed

.github/workflows/quality_check.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jobs:
4343
quality_check:
4444
runs-on: ubuntu-latest
4545
strategy:
46+
fail-fast: false
4647
max-parallel: 5
4748
matrix:
4849
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

aws_lambda_powertools/shared/constants.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,12 @@
6767
PRETTY_INDENT: int = 4
6868
COMPACT_INDENT: None = None
6969

70+
# Metadata constants
71+
LAMBDA_METADATA_API_ENV: str = "AWS_LAMBDA_METADATA_API"
72+
LAMBDA_METADATA_TOKEN_ENV: str = "AWS_LAMBDA_METADATA_TOKEN"
73+
METADATA_API_VERSION: str = "2026-01-15"
74+
METADATA_PATH: str = "/metadata/execution-environment"
75+
METADATA_DEFAULT_TIMEOUT_SECS: float = 1.0
76+
7077
# Idempotency constants
7178
IDEMPOTENCY_DISABLED_ENV: str = "POWERTOOLS_IDEMPOTENCY_DISABLED"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
Utility to fetch data from the AWS Lambda Metadata Endpoint
3+
"""
4+
5+
from aws_lambda_powertools.utilities.metadata.exceptions import LambdaMetadataError
6+
from aws_lambda_powertools.utilities.metadata.lambda_metadata import (
7+
LambdaMetadata,
8+
clear_metadata_cache,
9+
get_lambda_metadata,
10+
)
11+
12+
__all__ = [
13+
"LambdaMetadata",
14+
"LambdaMetadataError",
15+
"get_lambda_metadata",
16+
"clear_metadata_cache",
17+
]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""
2+
Lambda Metadata Service exceptions
3+
"""
4+
5+
6+
class LambdaMetadataError(Exception):
7+
"""Raised when the Lambda Metadata Endpoint is unavailable or returns an error."""
8+
9+
def __init__(self, message: str, status_code: int = -1):
10+
self.status_code = status_code
11+
super().__init__(message)
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""
2+
Lambda Metadata Service client
3+
4+
Fetches execution environment metadata from the Lambda Metadata Endpoint,
5+
with caching for the sandbox lifetime.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import logging
11+
import os
12+
import urllib.request
13+
from dataclasses import dataclass, field
14+
from json import JSONDecodeError
15+
from json import loads as json_loads
16+
from typing import Any
17+
18+
from aws_lambda_powertools.shared.constants import (
19+
LAMBDA_INITIALIZATION_TYPE,
20+
LAMBDA_METADATA_API_ENV,
21+
LAMBDA_METADATA_TOKEN_ENV,
22+
METADATA_API_VERSION,
23+
METADATA_DEFAULT_TIMEOUT_SECS,
24+
METADATA_PATH,
25+
POWERTOOLS_DEV_ENV,
26+
)
27+
from aws_lambda_powertools.utilities.metadata.exceptions import LambdaMetadataError
28+
29+
logger = logging.getLogger(__name__)
30+
31+
_cache: dict[str, Any] = {}
32+
33+
34+
@dataclass(frozen=True)
35+
class LambdaMetadata:
36+
"""Lambda execution environment metadata returned by the metadata endpoint."""
37+
38+
availability_zone_id: str | None = None
39+
"""The Availability Zone ID where the function is executing (e.g. ``use1-az1``)."""
40+
41+
_raw: dict[str, Any] = field(default_factory=dict, repr=False)
42+
"""Full raw response for forward-compatibility with future fields."""
43+
44+
45+
def _is_lambda_environment() -> bool:
46+
"""Check whether we are running inside a Lambda execution environment."""
47+
return os.environ.get(LAMBDA_INITIALIZATION_TYPE, "") != ""
48+
49+
50+
def _is_dev_mode() -> bool:
51+
"""Check whether POWERTOOLS_DEV is enabled."""
52+
return os.environ.get(POWERTOOLS_DEV_ENV, "false").strip().lower() in ("true", "1")
53+
54+
55+
def _build_metadata(data: dict[str, Any]) -> LambdaMetadata:
56+
"""Build a LambdaMetadata dataclass from the raw endpoint response."""
57+
return LambdaMetadata(
58+
availability_zone_id=data.get("AvailabilityZoneID"),
59+
_raw=data,
60+
)
61+
62+
63+
def _fetch_metadata(timeout: float = METADATA_DEFAULT_TIMEOUT_SECS) -> dict[str, Any]:
64+
"""
65+
Fetch metadata from the Lambda Metadata Endpoint via HTTP.
66+
67+
Parameters
68+
----------
69+
timeout : float
70+
Request timeout in seconds.
71+
72+
Returns
73+
-------
74+
dict[str, Any]
75+
Parsed JSON response from the metadata endpoint.
76+
77+
Raises
78+
------
79+
LambdaMetadataError
80+
If required environment variables are missing, the endpoint returns
81+
a non-200 status, or the response cannot be parsed.
82+
"""
83+
api = os.environ.get(LAMBDA_METADATA_API_ENV)
84+
token = os.environ.get(LAMBDA_METADATA_TOKEN_ENV)
85+
86+
if not api:
87+
raise LambdaMetadataError(
88+
f"Environment variable {LAMBDA_METADATA_API_ENV} is not set. Ensure {LAMBDA_METADATA_API_ENV} is set.",
89+
)
90+
if not token:
91+
raise LambdaMetadataError(
92+
f"Environment variable {LAMBDA_METADATA_TOKEN_ENV} is not set. Ensure {LAMBDA_METADATA_TOKEN_ENV} is set.",
93+
)
94+
95+
url = f"http://{api}/{METADATA_API_VERSION}{METADATA_PATH}"
96+
logger.debug("Fetching Lambda metadata from: %s", url)
97+
98+
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
99+
100+
try:
101+
with urllib.request.urlopen(req, timeout=timeout) as resp: # nosec B310
102+
status = resp.status
103+
body = resp.read().decode("utf-8")
104+
except urllib.error.HTTPError as exc:
105+
raise LambdaMetadataError(
106+
f"Metadata request failed with status {exc.code}",
107+
status_code=exc.code,
108+
) from exc
109+
except Exception as exc:
110+
raise LambdaMetadataError(f"Failed to fetch Lambda metadata: {exc}") from exc
111+
112+
if status != 200:
113+
raise LambdaMetadataError(
114+
f"Metadata request failed with status {status}",
115+
status_code=status,
116+
)
117+
118+
try:
119+
data: dict[str, Any] = json_loads(body)
120+
except (JSONDecodeError, TypeError) as exc:
121+
raise LambdaMetadataError(f"Failed to parse metadata response: {exc}") from exc
122+
123+
logger.debug("Lambda metadata response: %s", data)
124+
return data
125+
126+
127+
def get_lambda_metadata(*, timeout: float = METADATA_DEFAULT_TIMEOUT_SECS) -> LambdaMetadata:
128+
"""
129+
Retrieve Lambda execution environment metadata.
130+
131+
Returns cached metadata on subsequent calls. When not running in a Lambda
132+
environment (local dev, tests) or when ``POWERTOOLS_DEV`` is enabled,
133+
returns an empty ``LambdaMetadata``.
134+
135+
Parameters
136+
----------
137+
timeout : float
138+
HTTP request timeout in seconds (default 1.0).
139+
140+
Returns
141+
-------
142+
LambdaMetadata
143+
Metadata about the current execution environment.
144+
145+
Raises
146+
------
147+
LambdaMetadataError
148+
If the metadata endpoint is unavailable or returns an error.
149+
150+
Example
151+
-------
152+
>>> from aws_lambda_powertools.utilities.metadata import get_lambda_metadata
153+
>>> metadata = get_lambda_metadata()
154+
>>> metadata.availability_zone_id # e.g. "use1-az1"
155+
"""
156+
if _is_dev_mode() or not _is_lambda_environment():
157+
return LambdaMetadata()
158+
159+
if _cache:
160+
return _build_metadata(_cache)
161+
162+
data = _fetch_metadata(timeout=timeout)
163+
_cache.update(data)
164+
return _build_metadata(_cache)
165+
166+
167+
def clear_metadata_cache() -> None:
168+
"""
169+
Clear the cached metadata.
170+
171+
Useful for testing or when you need to force a fresh fetch
172+
(e.g. after SnapStart restore).
173+
"""
174+
_cache.clear()

docs/utilities/metadata.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
title: Metadata
3+
description: Utility
4+
status: new
5+
---
6+
7+
<!-- markdownlint-disable MD043 -->
8+
9+
The Metadata utility allows you to fetch data from the [AWS Lambda Metadata Endpoint (LMDS)](https://docs.aws.amazon.com/lambda/latest/dg/lambda-metadata-endpoint.html){target="_blank"}. This can be useful for retrieving information about the Lambda execution environment, such as the Availability Zone ID.
10+
11+
## Key features
12+
13+
* Fetch execution environment metadata from the Lambda Metadata Endpoint
14+
* Automatic caching for the duration of the Lambda sandbox
15+
* Graceful fallback to empty metadata outside Lambda (local dev, tests)
16+
* Forward-compatible dataclass that can be extended as new fields are added
17+
18+
## Getting started
19+
20+
### Usage
21+
22+
You can fetch data from the Lambda Metadata Endpoint using the `get_lambda_metadata` function.
23+
24+
???+ tip
25+
Metadata is cached for the duration of the Lambda sandbox, so subsequent calls to `get_lambda_metadata` will return the cached data.
26+
27+
=== "getting_started_metadata.py"
28+
29+
```python hl_lines="2 9 10"
30+
--8<-- "examples/metadata/src/getting_started_metadata.py"
31+
```
32+
33+
You can also fetch metadata eagerly during cold start, so it's ready for subsequent invocations:
34+
35+
=== "getting_started_metadata_eager.py"
36+
37+
```python hl_lines="2 8"
38+
--8<-- "examples/metadata/src/getting_started_metadata_eager.py"
39+
```
40+
41+
### Available metadata
42+
43+
| Property | Type | Description |
44+
| ---------------------- | --------------- | -------------------------------------------------------------- |
45+
| `availability_zone_id` | `str` or `None` | The AZ where the function is running (e.g., `use1-az1`) |
46+
47+
## Testing your code
48+
49+
The metadata endpoint is not available during local development or testing. To ease testing, the `get_lambda_metadata` function automatically detects when it's running in a non-Lambda environment and returns an empty `LambdaMetadata` instance. This allows you to write tests without needing to mock the endpoint.
50+
51+
If you want to mock specific metadata values for testing purposes, you can patch the internal `_fetch_metadata` function and set the required environment variables:
52+
53+
=== "testing_metadata.py"
54+
55+
```python hl_lines="6-8 13-18 21"
56+
--8<-- "examples/metadata/src/testing_metadata.py"
57+
```
58+
59+
We also expose a `clear_metadata_cache` function that can be used to clear the cached metadata, allowing you to test different metadata values within the same execution context.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from aws_lambda_powertools import Logger
2+
from aws_lambda_powertools.utilities.metadata import LambdaMetadata, get_lambda_metadata
3+
from aws_lambda_powertools.utilities.typing import LambdaContext
4+
5+
logger = Logger()
6+
7+
8+
def lambda_handler(event: dict, context: LambdaContext) -> dict:
9+
metadata: LambdaMetadata = get_lambda_metadata()
10+
az_id = metadata.availability_zone_id # e.g., "use1-az1"
11+
12+
logger.append_keys(az_id=az_id)
13+
logger.info("Processing request")
14+
15+
return {"az_id": az_id}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from aws_lambda_powertools import Logger
2+
from aws_lambda_powertools.utilities.metadata import LambdaMetadata, get_lambda_metadata
3+
from aws_lambda_powertools.utilities.typing import LambdaContext
4+
5+
logger = Logger()
6+
7+
# Fetch during cold start — cached for subsequent invocations
8+
metadata: LambdaMetadata = get_lambda_metadata()
9+
10+
11+
def lambda_handler(event: dict, context: LambdaContext) -> dict:
12+
logger.append_keys(az_id=metadata.availability_zone_id)
13+
logger.info("Processing request")
14+
15+
return {"az_id": metadata.availability_zone_id}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from unittest.mock import patch
2+
3+
from aws_lambda_powertools.utilities.metadata import LambdaMetadata, clear_metadata_cache, get_lambda_metadata
4+
5+
6+
def test_handler_uses_metadata(monkeypatch):
7+
# GIVEN a Lambda environment with metadata env vars
8+
monkeypatch.setenv("AWS_LAMBDA_INITIALIZATION_TYPE", "on-demand")
9+
monkeypatch.setenv("AWS_LAMBDA_METADATA_API", "127.0.0.1:1234")
10+
monkeypatch.setenv("AWS_LAMBDA_METADATA_TOKEN", "test-token")
11+
12+
mock_response = {"AvailabilityZoneID": "use1-az1"}
13+
14+
with patch(
15+
"aws_lambda_powertools.utilities.metadata.lambda_metadata._fetch_metadata",
16+
return_value=mock_response,
17+
):
18+
# WHEN calling get_lambda_metadata
19+
metadata: LambdaMetadata = get_lambda_metadata()
20+
21+
# THEN it returns the mocked metadata
22+
assert metadata.availability_zone_id == "use1-az1"
23+
24+
# Clean up cache between tests
25+
clear_metadata_cache()
26+
27+
28+
def test_handler_works_outside_lambda():
29+
# GIVEN no Lambda environment variables are set
30+
# WHEN calling get_lambda_metadata
31+
metadata: LambdaMetadata = get_lambda_metadata()
32+
33+
# THEN it returns empty metadata without errors
34+
assert metadata.availability_zone_id is None

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ nav:
3434
- utilities/idempotency.md
3535
- utilities/data_masking.md
3636
- utilities/feature_flags.md
37+
- utilities/metadata.md
3738
- utilities/streaming.md
3839
- utilities/middleware_factory.md
3940
- utilities/jmespath_functions.md
@@ -244,6 +245,7 @@ plugins:
244245
- utilities/idempotency.md
245246
- utilities/data_masking.md
246247
- utilities/feature_flags.md
248+
- utilities/metadata.md
247249
- utilities/streaming.md
248250
- utilities/middleware_factory.md
249251
- utilities/jmespath_functions.md

0 commit comments

Comments
 (0)