Skip to content

Commit 335eef7

Browse files
authored
Merge pull request #628 from hjgraca/aot(logging)-support-logging
chore: Logging AOT Support
2 parents 5a483b7 + e1d1181 commit 335eef7

File tree

66 files changed

+4743
-1872
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+4743
-1872
lines changed

docs/core/logging.md

+139
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The logging utility provides a Lambda optimized logger with output structured as
1111
* Log Lambda event when instructed (disabled by default)
1212
* Log sampling enables DEBUG log level for a percentage of requests (disabled by default)
1313
* Append additional keys to structured log at any point in time
14+
* Ahead-of-Time compilation to native code support [AOT](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-native-aot.html) from version 1.6.0
1415

1516
## Installation
1617

@@ -22,6 +23,12 @@ Powertools for AWS Lambda (.NET) are available as NuGet packages. You can instal
2223

2324
## Getting started
2425

26+
!!! info
27+
28+
AOT Support
29+
If loooking for AOT specific configurations navigate to the [AOT section](#aot-support)
30+
31+
2532
Logging requires two settings:
2633

2734
Setting | Description | Environment variable | Attribute parameter
@@ -166,6 +173,10 @@ When debugging in non-production environments, you can instruct Logger to log th
166173

167174
You can set a Correlation ID using `CorrelationIdPath` parameter by passing a [JSON Pointer expression](https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-03){target="_blank"}.
168175

176+
!!! Attention
177+
The JSON Pointer expression is `case sensitive`. In the bellow example `/headers/my_request_id_header` would work but `/Headers/my_request_id_header` would not find the element.
178+
179+
169180
=== "Function.cs"
170181

171182
```c# hl_lines="6"
@@ -656,3 +667,131 @@ You can customize the structure (keys and values) of your log entries by impleme
656667
}
657668
}
658669
```
670+
671+
## AOT Support
672+
673+
Logging utility supports native AOT serialization by default without any changes needed.
674+
675+
!!! info
676+
677+
In case you want to use the `LogEvent`, `Custom Log Formatter` features or serialize your own types when Logging events it is required
678+
that you do some changes in your Lambda `Main` method.
679+
680+
!!! info
681+
682+
Starting from version 1.6.0 it is required to update `Amazon.Lambda.Serialization.SystemTextJson` to `version 2.4.3` in your `csproj`.
683+
684+
### Configure
685+
686+
The change needed is to replace `SourceGeneratorLambdaJsonSerializer` with `PowertoolsSourceGeneratorSerializer`.
687+
688+
This change enables Powertools to construct an instance of JsonSerializerOptions that is used to customize the serialization and deserialization of the Lambda JSON events and your own types.
689+
690+
=== "Before"
691+
692+
```csharp
693+
Func<APIGatewayHttpApiV2ProxyRequest, ILambdaContext, Task<APIGatewayHttpApiV2ProxyResponse>> handler = FunctionHandler;
694+
await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer<MyCustomJsonSerializerContext>())
695+
.Build()
696+
.RunAsync();
697+
```
698+
699+
=== "After"
700+
701+
```csharp hl_lines="2"
702+
Func<APIGatewayHttpApiV2ProxyRequest, ILambdaContext, Task<APIGatewayHttpApiV2ProxyResponse>> handler = FunctionHandler;
703+
await LambdaBootstrapBuilder.Create(handler, new PowertoolsSourceGeneratorSerializer<MyCustomJsonSerializerContext>())
704+
.Build()
705+
.RunAsync();
706+
```
707+
708+
For example when you have your own Demo type
709+
710+
```csharp
711+
public class Demo
712+
{
713+
public string Name { get; set; }
714+
public Headers Headers { get; set; }
715+
}
716+
```
717+
718+
To be able to serialize it in AOT you have to have your own `JsonSerializerContext`
719+
720+
```csharp
721+
[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))]
722+
[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))]
723+
[JsonSerializable(typeof(Demo))]
724+
public partial class MyCustomJsonSerializerContext : JsonSerializerContext
725+
{
726+
}
727+
```
728+
729+
When you change to `PowertoolsSourceGeneratorSerializer<MyCustomJsonSerializerContext>` we are
730+
combining your `JsonSerializerContext` types with Powertools `JsonSerializerContext`. This allows Powertools to serialize your types and Lambda events.
731+
732+
### Custom Log Formatter
733+
734+
To be able to use a custom log formatter with AOT we need to pass an instance of ` ILogFormatter` to `PowertoolsSourceGeneratorSerializer`
735+
instead of using the static `Logger.UseFormatter` in the Function contructor.
736+
737+
=== "Function Main method"
738+
739+
```csharp hl_lines="5"
740+
741+
Func<APIGatewayHttpApiV2ProxyRequest, ILambdaContext, Task<APIGatewayHttpApiV2ProxyResponse>> handler = FunctionHandler;
742+
await LambdaBootstrapBuilder.Create(handler,
743+
new PowertoolsSourceGeneratorSerializer<LambdaFunctionJsonSerializerContext>
744+
(
745+
new CustomLogFormatter()
746+
)
747+
)
748+
.Build()
749+
.RunAsync();
750+
751+
```
752+
753+
=== "CustomLogFormatter.cs"
754+
755+
```csharp
756+
public class CustomLogFormatter : ILogFormatter
757+
{
758+
public object FormatLogEntry(LogEntry logEntry)
759+
{
760+
return new
761+
{
762+
Message = logEntry.Message,
763+
Service = logEntry.Service,
764+
CorrelationIds = new
765+
{
766+
AwsRequestId = logEntry.LambdaContext?.AwsRequestId,
767+
XRayTraceId = logEntry.XRayTraceId,
768+
CorrelationId = logEntry.CorrelationId
769+
},
770+
LambdaFunction = new
771+
{
772+
Name = logEntry.LambdaContext?.FunctionName,
773+
Arn = logEntry.LambdaContext?.InvokedFunctionArn,
774+
MemoryLimitInMB = logEntry.LambdaContext?.MemoryLimitInMB,
775+
Version = logEntry.LambdaContext?.FunctionVersion,
776+
ColdStart = logEntry.ColdStart,
777+
},
778+
Level = logEntry.Level.ToString(),
779+
Timestamp = logEntry.Timestamp.ToString("o"),
780+
Logger = new
781+
{
782+
Name = logEntry.Name,
783+
SampleRate = logEntry.SamplingRate
784+
},
785+
};
786+
}
787+
}
788+
```
789+
790+
### Anonymous types
791+
792+
!!! note
793+
794+
Although we support anonymous type serialization by converting to a `Dictionary<string, object>`,
795+
this is not a best practice and is not recommendede when using native AOT.
796+
797+
Recommendation is to use concrete classes and add them to your `JsonSerializerContext`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<AWSProjectType>Lambda</AWSProjectType>
8+
<!-- This property makes the build directory similar to a publish directory and helps the AWS .NET Lambda Mock Test Tool find project dependencies. -->
9+
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
10+
<!-- Generate Native AOT image during publishing to improve cold start time. -->
11+
<PublishAot>true</PublishAot>
12+
<!-- StripSymbols tells the compiler to strip debugging symbols from the final executable if we're on Linux and put them into their own file.
13+
This will greatly reduce the final executable's size.-->
14+
<StripSymbols>true</StripSymbols>
15+
<!-- TrimMode partial will only trim assemblies marked as trimmable. To reduce package size make all assemblies trimmable and set TrimMode to full.
16+
If there are trim warnings during build, you can hit errors at runtime.-->
17+
<TrimMode>partial</TrimMode>
18+
</PropertyGroup>
19+
<ItemGroup>
20+
<PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.10.0"/>
21+
<PackageReference Include="Amazon.Lambda.Core" Version="2.2.0"/>
22+
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.3"/>
23+
<PackageReference Include="AWS.Lambda.Powertools.Logging" Version="1.5.1" />
24+
</ItemGroup>
25+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using Amazon.Lambda.Core;
2+
using Amazon.Lambda.RuntimeSupport;
3+
using Amazon.Lambda.Serialization.SystemTextJson;
4+
using System.Text.Json.Serialization;
5+
using AWS.Lambda.Powertools.Logging;
6+
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
7+
8+
namespace AOT_Logging;
9+
10+
public static class Function
11+
{
12+
/// <summary>
13+
/// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It
14+
/// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and
15+
/// the JSON serializer to use for converting Lambda JSON format to the .NET types.
16+
/// </summary>
17+
private static async Task Main()
18+
{
19+
Func<string, ILambdaContext, string> handler = FunctionHandler;
20+
await LambdaBootstrapBuilder.Create(handler,
21+
new SourceGeneratorLambdaJsonSerializer<LambdaFunctionJsonSerializerContext>())
22+
.Build()
23+
.RunAsync();
24+
}
25+
26+
/// <summary>
27+
/// A simple function that takes a string and does a ToUpper.
28+
///
29+
/// To use this handler to respond to an AWS event, reference the appropriate package from
30+
/// https://github.com/aws/aws-lambda-dotnet#events
31+
/// and change the string input parameter to the desired event type. When the event type
32+
/// is changed, the handler type registered in the main method needs to be updated and the LambdaFunctionJsonSerializerContext
33+
/// defined below will need the JsonSerializable updated. If the return type and event type are different then the
34+
/// LambdaFunctionJsonSerializerContext must have two JsonSerializable attributes, one for each type.
35+
///
36+
// When using Native AOT extra testing with the deployed Lambda functions is required to ensure
37+
// the libraries used in the Lambda function work correctly with Native AOT. If a runtime
38+
// error occurs about missing types or methods the most likely solution will be to remove references to trim-unsafe
39+
// code or configure trimming options. This sample defaults to partial TrimMode because currently the AWS
40+
// SDK for .NET does not support trimming. This will result in a larger executable size, and still does not
41+
// guarantee runtime trimming errors won't be hit.
42+
/// </summary>
43+
/// <param name="input">The event for the Lambda function handler to process.</param>
44+
/// <param name="context">The ILambdaContext that provides methods for logging and describing the Lambda environment.</param>
45+
/// <returns></returns>
46+
[Logging(LogEvent = true, Service = "pt_service", LogLevel = LogLevel.Debug)]
47+
public static string FunctionHandler(string input, ILambdaContext context)
48+
{
49+
Logger.LogInformation("FunctionHandler invocation");
50+
return ToUpper(input);
51+
}
52+
53+
private static string ToUpper(string input)
54+
{
55+
Logger.LogInformation("ToUpper invocation");
56+
57+
var upper = input.ToUpper();
58+
59+
Logger.LogInformation("ToUpper result: {Result}", upper);
60+
61+
return upper;
62+
}
63+
}
64+
65+
/// <summary>
66+
/// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator.
67+
/// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur
68+
/// from the JSON serializer unable to find the serialization information for unknown types.
69+
/// </summary>
70+
[JsonSerializable(typeof(string))]
71+
public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext
72+
{
73+
// By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time
74+
// which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for.
75+
// See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation
76+
}

examples/AOT/src/AOT/Readme.md renamed to examples/AOT/AOT_Logging/src/AOT_Logging/Readme.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# AWS Lambda Native AOT Project with Powertools for AWS Lambda (.NET)
1+
# AWS Lambda Native AOT Project
22

33
This starter project consists of:
44
* Function.cs - contains a class with a `Main` method that starts the bootstrap and a single function handler method.
@@ -71,12 +71,12 @@ If already installed check if new version is available.
7171

7272
Execute unit tests
7373
```
74-
cd "AOT/test/AOT.Tests"
74+
cd "AOT_Logging/test/AOT_Logging.Tests"
7575
dotnet test
7676
```
7777

7878
Deploy function to AWS Lambda
7979
```
80-
cd "AOT/src/AOT"
80+
cd "AOT_Logging/src/AOT_Logging"
8181
dotnet lambda deploy-function
8282
```

examples/AOT/src/AOT/aws-lambda-tools-defaults.json renamed to examples/AOT/AOT_Logging/src/AOT_Logging/aws-lambda-tools-defaults.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
"function-runtime": "dotnet8",
1212
"function-memory-size": 512,
1313
"function-timeout": 30,
14-
"function-handler": "AOT",
14+
"function-handler": "AOT_Logging",
1515
"msbuild-parameters": "--self-contained true"
1616
}

examples/AOT/test/AOT.Tests/AOT.Tests.csproj renamed to examples/AOT/AOT_Logging/test/AOT_Logging.Tests/AOT_Logging.Tests.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
1414
</ItemGroup>
1515
<ItemGroup>
16-
<ProjectReference Include="..\..\src\AOT\AOT.csproj" />
16+
<ProjectReference Include="..\..\src\AOT_Logging\AOT_Logging.csproj" />
1717
</ItemGroup>
1818
</Project>

examples/AOT/test/AOT.Tests/FunctionTest.cs renamed to examples/AOT/AOT_Logging/test/AOT_Logging.Tests/FunctionTest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using Amazon.Lambda.Core;
33
using Amazon.Lambda.TestUtilities;
44

5-
namespace AOT.Tests;
5+
namespace AOT_Logging.Tests;
66

77
public class FunctionTest
88
{

examples/AOT/src/AOT/AOT.csproj renamed to examples/AOT/AOT_Metrics/src/AOT_Metrics/AOT_Metrics.csproj

+2-3
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
<ItemGroup>
2020
<PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.10.0"/>
2121
<PackageReference Include="Amazon.Lambda.Core" Version="2.2.0"/>
22-
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.1"/>
22+
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.3"/>
2323
<PackageReference Include="AWS.Lambda.Powertools.Metrics" Version="1.7.1" />
24-
<PackageReference Include="AWS.Lambda.Powertools.Tracing" Version="1.5.1" />
2524
</ItemGroup>
26-
</Project>
25+
</Project>

examples/AOT/src/AOT/Function.cs renamed to examples/AOT/AOT_Metrics/src/AOT_Metrics/Function.cs

+4-13
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
using Amazon.Lambda.Serialization.SystemTextJson;
44
using System.Text.Json.Serialization;
55
using AWS.Lambda.Powertools.Metrics;
6-
using AWS.Lambda.Powertools.Tracing;
76

8-
namespace AOT;
7+
namespace AOT_Metrics;
98

10-
public class Function
9+
public static class Function
1110
{
1211
/// <summary>
1312
/// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It
@@ -43,27 +42,19 @@ await LambdaBootstrapBuilder.Create(handler,
4342
/// <param name="input">The event for the Lambda function handler to process.</param>
4443
/// <param name="context">The ILambdaContext that provides methods for logging and describing the Lambda environment.</param>
4544
/// <returns></returns>
46-
47-
// You can optionally capture cold start metrics by setting CaptureColdStart parameter to true.
4845
[Metrics(Namespace = "ns", Service = "svc", CaptureColdStart = true)]
49-
[Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)]
5046
public static string FunctionHandler(string input, ILambdaContext context)
5147
{
52-
// You can create metrics using AddMetric
53-
// MetricUnit enum facilitates finding a supported metric unit by CloudWatch.
5448
Metrics.AddMetric("Handler invocation", 1, MetricUnit.Count);
5549
return ToUpper(input);
5650
}
57-
58-
[Tracing(SegmentName = "ToUpper Call")]
51+
5952
private static string ToUpper(string input)
6053
{
6154
Metrics.AddMetric("ToUpper invocation", 1, MetricUnit.Count);
6255

6356
var upper = input.ToUpper();
64-
65-
Tracing.AddAnnotation("Upper text", upper);
66-
57+
6758
// You can add high-cardinality data as part of your Metrics log with AddMetadata method.
6859
// This is useful when you want to search highly contextual information along with your metrics in your logs.
6960
Metrics.AddMetadata("Input Uppercase", upper);

0 commit comments

Comments
 (0)