Skip to content

Commit 3b2bbb6

Browse files
hjgracaCopilot
andauthored
feat(metadata): add Lambda Metadata utility for retrieving execution environment metadata (#1157)
Signed-off-by: Henrique Graca <999396+hjgraca@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent c4492d7 commit 3b2bbb6

17 files changed

Lines changed: 1862 additions & 783 deletions

docs/utilities/metadata.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
---
2+
title: Lambda Metadata
3+
description: Utility
4+
---
5+
6+
<!-- markdownlint-disable MD013 -->
7+
The Lambda Metadata utility provides access to the Lambda Metadata Endpoint (LMDS), giving you execution environment metadata like Availability Zone ID.
8+
9+
## Key features
10+
11+
* Retrieve Lambda execution environment metadata
12+
* Automatic caching for the sandbox lifetime
13+
* Thread-safe access
14+
* Native AOT compatible
15+
16+
## Installation
17+
18+
```bash
19+
dotnet add package AWS.Lambda.Powertools.Metadata
20+
```
21+
22+
## Getting started
23+
24+
```csharp
25+
using AWS.Lambda.Powertools.Metadata;
26+
27+
public class Function
28+
{
29+
public string Handler(object input, ILambdaContext context)
30+
{
31+
var azId = LambdaMetadata.AvailabilityZoneId;
32+
return $"Running in AZ: {azId}";
33+
}
34+
}
35+
```
36+
37+
## Available metadata
38+
39+
| Property | Type | Description |
40+
|-----------------------|-----------|--------------------------------------------------------------------------|
41+
| `AvailabilityZoneId` | `string?` | The AZ where the function is running (e.g., `use1-az1`), or `null` when unavailable |
42+
43+
## Error handling
44+
45+
```csharp
46+
using AWS.Lambda.Powertools.Metadata;
47+
using AWS.Lambda.Powertools.Metadata.Exceptions;
48+
49+
try
50+
{
51+
var azId = LambdaMetadata.AvailabilityZoneId;
52+
}
53+
catch (LambdaMetadataException ex)
54+
{
55+
Console.WriteLine($"Failed to get metadata: {ex.Message}");
56+
57+
if (ex.StatusCode != -1)
58+
Console.WriteLine($"HTTP Status: {ex.StatusCode}");
59+
}
60+
```
61+
62+
## Refreshing metadata
63+
64+
Metadata remains constant for the Lambda sandbox lifetime. If you need to force a refresh:
65+
66+
```csharp
67+
LambdaMetadata.Refresh();
68+
```
69+
70+
## Thread safety
71+
72+
`LambdaMetadata.AvailabilityZoneId` is thread-safe. You can access it from multiple concurrent invocations without race conditions.
73+
74+
## Use cases
75+
76+
### Multi-AZ routing
77+
78+
```csharp
79+
using AWS.Lambda.Powertools.Metadata;
80+
81+
public class Function
82+
{
83+
public async Task<string> Handler(OrderRequest request, ILambdaContext context)
84+
{
85+
var endpoint = LambdaMetadata.AvailabilityZoneId switch
86+
{
87+
"use1-az1" => "https://service-az1.internal",
88+
"use1-az2" => "https://service-az2.internal",
89+
_ => "https://service.internal"
90+
};
91+
92+
return await ProcessOrder(request, endpoint);
93+
}
94+
}
95+
```
96+
97+
### Logging
98+
99+
```csharp
100+
using AWS.Lambda.Powertools.Logging;
101+
using AWS.Lambda.Powertools.Metadata;
102+
103+
public class Function
104+
{
105+
public Function()
106+
{
107+
Logger.AppendKey("az_id", LambdaMetadata.AvailabilityZoneId);
108+
}
109+
110+
[Logging]
111+
public string Handler(object input, ILambdaContext context)
112+
{
113+
Logger.LogInformation("Processing request");
114+
return "Success";
115+
}
116+
}
117+
```

libraries/AWS.Lambda.Powertools.sln

Lines changed: 809 additions & 783 deletions
Large diffs are not rendered by default.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<!-- Remaining properties are defined in Directory.Build.props -->
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<PackageId>AWS.Lambda.Powertools.Metadata</PackageId>
8+
<Description>Powertools for AWS Lambda (.NET) - Lambda Metadata package. Provides access to Lambda execution environment metadata from the Lambda Metadata Endpoint (LMDS).</Description>
9+
<AssemblyName>AWS.Lambda.Powertools.Metadata</AssemblyName>
10+
<RootNamespace>AWS.Lambda.Powertools.Metadata</RootNamespace>
11+
<IncludeCommonFiles>true</IncludeCommonFiles>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<!-- Package versions are Centrally managed in Directory.Packages.props file -->
16+
<!-- More info https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management -->
17+
<PackageReference Include="Amazon.Lambda.Core"/>
18+
<ProjectReference Include="..\AWS.Lambda.Powertools.Common\AWS.Lambda.Powertools.Common.csproj" Condition="'$(Configuration)'=='Debug'"/>
19+
</ItemGroup>
20+
21+
</Project>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
namespace AWS.Lambda.Powertools.Metadata.Exceptions;
17+
18+
/// <summary>
19+
/// Exception thrown when the Lambda Metadata Endpoint is unavailable or returns an error.
20+
/// <para>
21+
/// This exception may be thrown when:
22+
/// <list type="bullet">
23+
/// <item><description>The metadata endpoint environment variables are not set</description></item>
24+
/// <item><description>The metadata endpoint returns a non-200 status code</description></item>
25+
/// <item><description>Network errors occur when connecting to the endpoint</description></item>
26+
/// <item><description>The response cannot be parsed</description></item>
27+
/// </list>
28+
/// </para>
29+
/// </summary>
30+
public class LambdaMetadataException : Exception
31+
{
32+
/// <summary>
33+
/// Gets the HTTP status code from the metadata endpoint.
34+
/// Returns -1 if not applicable.
35+
/// </summary>
36+
public int StatusCode { get; }
37+
38+
/// <summary>
39+
/// Constructs a new exception with the specified message.
40+
/// </summary>
41+
/// <param name="message">The error message.</param>
42+
public LambdaMetadataException(string message) : base(message)
43+
{
44+
StatusCode = -1;
45+
}
46+
47+
/// <summary>
48+
/// Constructs a new exception with the specified message and cause.
49+
/// </summary>
50+
/// <param name="message">The error message.</param>
51+
/// <param name="innerException">The underlying cause.</param>
52+
public LambdaMetadataException(string message, Exception innerException) : base(message, innerException)
53+
{
54+
StatusCode = -1;
55+
}
56+
57+
/// <summary>
58+
/// Constructs a new exception with the specified message and HTTP status code.
59+
/// </summary>
60+
/// <param name="message">The error message.</param>
61+
/// <param name="statusCode">The HTTP status code from the metadata endpoint.</param>
62+
public LambdaMetadataException(string message, int statusCode) : base(message)
63+
{
64+
StatusCode = statusCode;
65+
}
66+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
namespace AWS.Lambda.Powertools.Metadata.Internal;
17+
18+
/// <summary>
19+
/// Internal interface for fetching Lambda metadata.
20+
/// </summary>
21+
internal interface IMetadataFetcher
22+
{
23+
/// <summary>
24+
/// Fetches metadata from the Lambda Metadata Endpoint.
25+
/// </summary>
26+
MetadataValues Fetch();
27+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using System.Text.Json.Serialization;
17+
18+
namespace AWS.Lambda.Powertools.Metadata.Internal;
19+
20+
/// <summary>
21+
/// Source-generated JSON serializer context for AOT compatibility.
22+
/// </summary>
23+
[JsonSourceGenerationOptions(
24+
PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified,
25+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
26+
WriteIndented = false)]
27+
[JsonSerializable(typeof(MetadataValues))]
28+
internal partial class LambdaMetadataSerializerContext : JsonSerializerContext
29+
{
30+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using System.Net;
17+
using System.Text.Json;
18+
using AWS.Lambda.Powertools.Metadata.Exceptions;
19+
20+
namespace AWS.Lambda.Powertools.Metadata.Internal;
21+
22+
/// <summary>
23+
/// Fetches metadata from the Lambda Metadata Endpoint (LMDS).
24+
/// </summary>
25+
internal sealed class MetadataFetcher : IMetadataFetcher
26+
{
27+
private const string EnvMetadataApi = "AWS_LAMBDA_METADATA_API";
28+
private const string EnvMetadataToken = "AWS_LAMBDA_METADATA_TOKEN";
29+
private const string ApiVersion = "2026-01-15";
30+
private const string MetadataPath = "/metadata/execution-environment";
31+
32+
private readonly HttpClient _httpClient;
33+
34+
public MetadataFetcher() : this(CreateHttpClient())
35+
{
36+
}
37+
38+
internal MetadataFetcher(HttpClient httpClient)
39+
{
40+
_httpClient = httpClient;
41+
}
42+
43+
private static HttpClient CreateHttpClient()
44+
{
45+
return new HttpClient(new HttpClientHandler
46+
{
47+
AutomaticDecompression = DecompressionMethods.None
48+
})
49+
{
50+
Timeout = TimeSpan.FromSeconds(1)
51+
};
52+
}
53+
54+
public MetadataValues Fetch()
55+
{
56+
var (token, url) = GetEndpointInfo();
57+
58+
try
59+
{
60+
using var request = new HttpRequestMessage(HttpMethod.Get, url);
61+
request.Headers.Add("Authorization", $"Bearer {token}");
62+
63+
using var response = _httpClient.Send(request);
64+
return ProcessResponse(response);
65+
}
66+
catch (LambdaMetadataException)
67+
{
68+
throw;
69+
}
70+
catch (Exception ex)
71+
{
72+
throw new LambdaMetadataException($"Failed to fetch Lambda metadata: {ex.Message}", ex);
73+
}
74+
}
75+
76+
private static (string token, string url) GetEndpointInfo()
77+
{
78+
var token = Environment.GetEnvironmentVariable(EnvMetadataToken);
79+
var api = Environment.GetEnvironmentVariable(EnvMetadataApi);
80+
81+
if (string.IsNullOrEmpty(token))
82+
throw new LambdaMetadataException(
83+
$"Lambda metadata token not available. Ensure {EnvMetadataToken} is set.");
84+
85+
if (string.IsNullOrEmpty(api))
86+
throw new LambdaMetadataException(
87+
$"Lambda metadata API endpoint not available. Ensure {EnvMetadataApi} is set.");
88+
89+
return (token, $"http://{api}/{ApiVersion}{MetadataPath}");
90+
}
91+
92+
private static MetadataValues ProcessResponse(HttpResponseMessage response)
93+
{
94+
if (!response.IsSuccessStatusCode)
95+
{
96+
var error = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
97+
throw new LambdaMetadataException(
98+
$"Metadata request failed with status {(int)response.StatusCode}: {error}",
99+
(int)response.StatusCode);
100+
}
101+
102+
var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
103+
return JsonSerializer.Deserialize(body, LambdaMetadataSerializerContext.Default.MetadataValues)
104+
?? throw new LambdaMetadataException("Failed to deserialize Lambda metadata response.");
105+
}
106+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using System.Runtime.CompilerServices;
17+
18+
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metadata.Tests")]
19+
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.ConcurrencyTests")]
20+
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

0 commit comments

Comments
 (0)