Skip to content

Commit 26f8b10

Browse files
rr-on-ghphipag
andauthored
feat(v2): Add GraalVM reachability metadata for core utilities (#1753)
Provides GraalVM Reachability Metadata files and respective Maven configurations / profiles for building a native-image for all core-utilities. Also refactors all unit tests to use Junit Pioneer for mocking environment variables. --------- Co-authored-by: Philipp Page <[email protected]>
1 parent cef1ef1 commit 26f8b10

File tree

60 files changed

+6086
-744
lines changed

Some content is hidden

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

60 files changed

+6086
-744
lines changed

GraalVM.md

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# GraalVM Compatibility for AWS Lambda Powertools Java
2+
3+
## Table of Contents
4+
- [Overview](#overview)
5+
- [Prerequisites](#prerequisites)
6+
- [General Implementation Steps](#general-implementation-steps)
7+
- [Known Issues and Solutions](#known-issues-and-solutions)
8+
- [Reference Implementation](#reference-implementation)
9+
10+
## Overview
11+
This documentation provides guidance for adding GraalVM support for AWS Lambda Powertools Java modules and using the modules in Lambda functions.
12+
13+
## Prerequisites
14+
- GraalVM 21+ installation
15+
- Maven 3.x
16+
17+
## General Implementation Steps
18+
GraalVM native image compilation requires complete knowledge of an application's dynamic features at build time. The GraalVM reachability metadata (GRM) JSON files are essential because Java applications often use features that are determined at runtime, such as reflection, dynamic proxy classes, resource loading, and JNI (Java Native Interface). The metadata files tell GraalVM which classes need reflection access, which resources need to be included in the native image, and which proxy classes need to be generated.
19+
20+
In order to generate the metadata reachability files for Powertools for Lambda, follow these general steps.
21+
22+
1. **Add Maven Profiles**
23+
- Add profile for generating GraalVM reachability metadata files. You can find an example of this in profile `generate-graalvm-files` of this [pom.xml](powertools-common/pom.xml).
24+
- Add another profile for running the tests in the native image. You can find and example of this in profile `graalvm-native` of this [pom.xml](powertools-common/pom.xml).
25+
26+
2. **Generate Reachability Metadata**
27+
- Set the `JAVA_HOME` environment variable to use GraalVM
28+
- Run tests with `-Pgenerate-graalvm-files` profile.
29+
```shell
30+
mvn -Pgenerate-graalvm-files clean test
31+
```
32+
33+
3. **Validate Native Image Tests**
34+
- Set the `JAVA_HOME` environment variable to use GraalVM
35+
- Run tests with `-Pgraalvm-native` profile. This will build a GraalVM native image and run the JUnit tests.
36+
```shell
37+
mvn -Pgraalvm-native clean test
38+
```
39+
40+
4. **Clean Up Metadata**
41+
- GraalVM metadata reachability files generated in Step 2 contains references to the test scoped dependencies as well.
42+
- Remove the references in generated metadata files for the following (and any other references to test scoped resources and classes):
43+
- JUnit
44+
- Mockito
45+
- ByteBuddy
46+
47+
## Known Issues and Solutions
48+
1. **Mockito Compatibility**
49+
- Powertools uses Mockito 5.x which uses “inline mock maker” as the default. This mock maker does not play well with GraalVM. Mockito [recommends](https://github.com/mockito/mockito/releases/tag/v5.0.0) using subclass mock maker with GraalVM. Therefore `generate-graalvm-files` profile uses subclass mock maker instead of inline mock maker.
50+
- Subclass mock maker does not support testing static methods. Tests have therefore been modified to use [JUnit Pioneer](https://junit-pioneer.org/docs/environment-variables/) to inject the environment variables in the scope of the test's execution.
51+
52+
2. **Log4j Compatibility**
53+
- Version 2.22.1 fails with this error
54+
```
55+
java.lang.InternalError: com.oracle.svm.core.jdk.UnsupportedFeatureError: Defining hidden classes at runtime is not supported.
56+
```
57+
- This has been [fixed](https://github.com/apache/logging-log4j2/discussions/2364#discussioncomment-8950077) in Log4j 2.24.x. PT has been updated to use this version of Log4j
58+
59+
## Reference Implementation
60+
Working example is available in the [examples](examples/powertools-examples-core-utilities/sam-graalvm).

docs/FAQs.md

+110-1
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,113 @@ The following example shows how to use the Lambda Powertools Parameters module w
142142
}
143143
```
144144
The `aws-crt-client` was considered for adoption as the default HTTP client in Lambda Powertools for Java as mentioned in [Move SDK http client to CRT](https://github.com/aws-powertools/powertools-lambda-java/issues/1092),
145-
but due to the impact on the developer experience it was decided to stick with the `url-connection-client`.
145+
but due to the impact on the developer experience it was decided to stick with the `url-connection-client`.
146+
147+
## How can I use Powertools for AWS Lambda (Java) with GraalVM?
148+
149+
Powertools core utilities, i.e. [logging](./core/logging.md), [metrics](./core/metrics.md) and [tracing](./core/tracing.md), include the [GraalVM Reachability Metadata (GRM)](https://www.graalvm.org/latest/reference-manual/native-image/metadata/) in the `META-INF` directories of the respective JARs. You can find a working example of Serverless Application Model (SAM) based application in the [examples](../examples/powertools-examples-core-utilities/sam-graalvm/README.md) directory.
150+
151+
Below, you find typical steps you need to follow in a Maven based Java project:
152+
153+
### Set the environment to use GraalVM
154+
155+
```shell
156+
export JAVA_HOME=<path to GraalVM>
157+
```
158+
159+
### Use log4j `>2.24.0`
160+
Log4j version `2.24.0` adds [support for GraalVM](https://github.com/apache/logging-log4j2/issues/1539#issuecomment-2106766878). Depending on your project's dependency hierarchy, older version of log4j might be included in the final dependency graph. Make sure version `>2.24.0` of these dependencies are used by your Maven project:
161+
162+
```xml
163+
<dependencies>
164+
<dependency>
165+
<groupId>org.apache.logging.log4j</groupId>
166+
<artifactId>log4j-api</artifactId>
167+
<version>${log4j.version}</version>
168+
</dependency>
169+
<dependency>
170+
<groupId>org.apache.logging.log4j</groupId>
171+
<artifactId>log4j-core</artifactId>
172+
<version>${log4j.version}</version>
173+
</dependency>
174+
<dependency>
175+
<groupId>org.apache.logging.log4j</groupId>
176+
<artifactId>log4j-slf4j2-impl</artifactId>
177+
<version>${log4j.version}</version>
178+
</dependency>
179+
<dependency>
180+
<groupId>org.apache.logging.log4j</groupId>
181+
<artifactId>log4j-layout-template-json</artifactId>
182+
<version>${log4j.version}</version>
183+
</dependency>
184+
</dependencies>
185+
186+
```
187+
188+
### Add the AWS Lambda Java Runtime Interface Client dependency
189+
190+
The Runtime Interface Client allows your function to receive invocation events from Lambda, send the response back to Lambda, and report errors to the Lambda service. Add the below dependency to your Maven project:
191+
192+
```xml
193+
<dependency>
194+
<groupId>com.amazonaws</groupId>
195+
<artifactId>aws-lambda-java-runtime-interface-client</artifactId>
196+
<version>2.1.1</version>
197+
</dependency>
198+
```
199+
200+
Also include the AWS Lambda GRM files by copying the `com.amazonaws` [directory](../examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/) in your project's `META-INF/native-image` directory
201+
202+
### Build the native image
203+
204+
Use the `native-maven-plugin` to build the native image. You can do this by adding the plugin to your `pom.xml` and creating a build profile called `native-image` that can build the native image of your Lambda function:
205+
206+
```xml
207+
<profiles>
208+
<profile>
209+
<id>native-image</id>
210+
<build>
211+
<plugins>
212+
<plugin>
213+
<groupId>org.graalvm.buildtools</groupId>
214+
<artifactId>native-maven-plugin</artifactId>
215+
<version>0.10.1</version>
216+
<extensions>true</extensions>
217+
<executions>
218+
<execution>
219+
<id>build-native</id>
220+
<goals>
221+
<goal>build</goal>
222+
</goals>
223+
<phase>package</phase>
224+
</execution>
225+
</executions>
226+
<configuration>
227+
<imageName>your-project-name</imageName>
228+
<mainClass>com.amazonaws.services.lambda.runtime.api.client.AWSLambda</mainClass>
229+
<buildArgs>
230+
<!-- required for AWS Lambda Runtime Interface Client -->
231+
<arg>--enable-url-protocols=http</arg>
232+
<arg>--add-opens java.base/java.util=ALL-UNNAMED</arg>
233+
</buildArgs>
234+
</configuration>
235+
</plugin>
236+
</plugins>
237+
</build>
238+
</profile>
239+
</profiles>
240+
```
241+
242+
Create a Docker image using a `Dockerfile` like [this](../examples/powertools-examples-core-utilities/sam-graalvm/Dockerfile) to create an x86 based build image.
243+
244+
```shell
245+
docker build --platform linux/amd64 . -t your-org/your-app-graalvm-builder
246+
```
247+
248+
Create the native image of you Lambda function using the Docker command below.
249+
250+
```shell
251+
docker run --platform linux/amd64 -it -v `pwd`:`pwd` -w `pwd` -v ~/.m2:/root/.m2 your-org/your-app-graalvm-builder mvn clean -Pnative-image package
252+
253+
```
254+
The native image is created in the `target/` directory.

docs/core/logging.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ we provide [built-in JMESPath expressions](#built-in-correlation-id-expressions)
444444

445445
#### Custom keys
446446

447-
** Using StructuredArguments **
447+
**Using StructuredArguments**
448448

449449
To append additional keys in your logs, you can use the `StructuredArguments` class:
450450

@@ -624,7 +624,7 @@ To append additional keys in your logs, you can use the `StructuredArguments` cl
624624
}
625625
```
626626

627-
** Using MDC **
627+
**Using MDC**
628628

629629
Mapped Diagnostic Context (MDC) is essentially a Key-Value store. It is supported by the [SLF4J API](https://www.slf4j.org/manual.html#mdc){target="_blank"},
630630
[logback](https://logback.qos.ch/manual/mdc.html){target="_blank"} and log4j (known as [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html){target="_blank"}). You can use the following standard:
@@ -1048,4 +1048,4 @@ Use the `LambdaEcsEncoder` rather than the `LambdaJsonEncoder` when configuring
10481048
<appender-ref ref="console" />
10491049
</root>
10501050
</configuration>
1051-
```
1051+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#Use the official AWS SAM base image for Java 21
2+
FROM public.ecr.aws/sam/build-java21:latest
3+
4+
#Install GraalVM dependencies
5+
RUN curl -4 -L curl https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-x64_bin.tar.gz | tar -xvz
6+
RUN mv graalvm-jdk-21.* /usr/lib/graalvm
7+
8+
#Make native image and mvn available on CLI
9+
RUN ln -s /usr/lib/graalvm/bin/native-image /usr/bin/native-image
10+
RUN ln -s /usr/lib/maven/bin/mvn /usr/bin/mvn
11+
12+
#Set GraalVM as default
13+
ENV JAVA_HOME=/usr/lib/graalvm
14+
ENV PATH=/usr/lib/graalvm/bin:$PATH
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
build-HelloWorldFunction:
2+
mvn clean package -P native-image
3+
chmod +x target/hello-world
4+
cp target/hello-world $(ARTIFACTS_DIR) # (ARTIFACTS_DIR --> https://github.com/aws/aws-lambda-builders/blob/develop/aws_lambda_builders/workflows/custom_make/DESIGN.md#implementation)
5+
chmod +x src/main/config/bootstrap
6+
cp src/main/config/bootstrap $(ARTIFACTS_DIR)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Powertools for AWS Lambda (Java) - Core Utilities Example with SAM on GraalVM
2+
3+
This project demonstrates the Lambda for Powertools Java module deployed using [Serverless Application Model](https://aws.amazon.com/serverless/sam/) running as a GraalVM native image.
4+
5+
For general information on the deployed example itself, you can refer to the parent [README](../README.md)
6+
7+
## Configuration
8+
9+
- SAM uses [template.yaml](template.yaml) to define the application's AWS resources.
10+
This file defines the Lambda function to be deployed as well as API Gateway for it.
11+
12+
- Set the environment to use GraalVM
13+
14+
```shell
15+
export JAVA_HOME=<path to GraalVM>
16+
```
17+
18+
## Build the sample application
19+
20+
- Build the Docker image that will be used as the environment for SAM build:
21+
22+
```shell
23+
docker build --platform linux/amd64 . -t powertools-examples-core-sam-graalvm
24+
```
25+
26+
- Build the SAM project using the docker image
27+
28+
```shell
29+
sam build --use-container --build-image powertools-examples-core-sam-graalvm
30+
```
31+
32+
#### [Optional] Building with -SNAPSHOT versions of PowerTools
33+
34+
- If you are testing the example with a -SNAPSHOT version of PowerTools, the maven build inside the docker image will fail. This is because the -SNAPSHOT version of the PowerTools library that you are working on is still not available in maven central/snapshot repository.
35+
To get around this, follow these steps:
36+
- Create the native image using the `docker` command below on your development machine. The native image is created in the `target` directory.
37+
- `` docker run --platform linux/amd64 -it -v `pwd`:`pwd` -w `pwd` -v ~/.m2:/root/.m2 powertools-examples-core-sam-graalvm mvn clean -Pnative-image package -DskipTests ``
38+
- Edit the [`Makefile`](Makefile) remove this line
39+
- `mvn clean package -P native-image`
40+
- Build the SAM project using the docker image
41+
- `sam build --use-container --build-image powertools-examples-core-sam-graalvm`
42+
43+
## Deploy the sample application
44+
45+
- SAM deploy
46+
47+
```shell
48+
sam deploy
49+
```
50+
51+
To deploy the example, check out the instructions for getting
52+
started with SAM in [the examples directory](../../README.md)
53+
54+
## Additional notes
55+
56+
You can watch the trace information or log information using the SAM CLI:
57+
58+
```bash
59+
# Tail the logs
60+
sam logs --tail $MY_STACK
61+
62+
# Tail the traces
63+
sam traces --tail
64+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"body": "{\"message\": \"hello world\"}",
3+
"resource": "/{proxy+}",
4+
"path": "/path/to/resource",
5+
"httpMethod": "POST",
6+
"isBase64Encoded": false,
7+
"queryStringParameters": {
8+
"foo": "bar"
9+
},
10+
"pathParameters": {
11+
"proxy": "/path/to/resource"
12+
},
13+
"stageVariables": {
14+
"baz": "qux"
15+
},
16+
"headers": {
17+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
18+
"Accept-Encoding": "gzip, deflate, sdch",
19+
"Accept-Language": "en-US,en;q=0.8",
20+
"Cache-Control": "max-age=0",
21+
"CloudFront-Forwarded-Proto": "https",
22+
"CloudFront-Is-Desktop-Viewer": "true",
23+
"CloudFront-Is-Mobile-Viewer": "false",
24+
"CloudFront-Is-SmartTV-Viewer": "false",
25+
"CloudFront-Is-Tablet-Viewer": "false",
26+
"CloudFront-Viewer-Country": "US",
27+
"Host": "1234567890.execute-api.us-east-1.amazonaws.com",
28+
"Upgrade-Insecure-Requests": "1",
29+
"User-Agent": "Custom User Agent String",
30+
"Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
31+
"X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
32+
"X-Forwarded-For": "127.0.0.1, 127.0.0.2",
33+
"X-Forwarded-Port": "443",
34+
"X-Forwarded-Proto": "https"
35+
},
36+
"requestContext": {
37+
"accountId": "123456789012",
38+
"resourceId": "123456",
39+
"stage": "prod",
40+
"requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
41+
"requestTime": "09/Apr/2015:12:34:56 +0000",
42+
"requestTimeEpoch": 1428582896000,
43+
"identity": {
44+
"cognitoIdentityPoolId": null,
45+
"accountId": null,
46+
"cognitoIdentityId": null,
47+
"caller": null,
48+
"accessKey": null,
49+
"sourceIp": "127.0.0.1",
50+
"cognitoAuthenticationType": null,
51+
"cognitoAuthenticationProvider": null,
52+
"userArn": null,
53+
"userAgent": "Custom User Agent String",
54+
"user": null
55+
},
56+
"path": "/prod/path/to/resource",
57+
"resourcePath": "/{proxy+}",
58+
"httpMethod": "POST",
59+
"apiId": "1234567890",
60+
"protocol": "HTTP/1.1"
61+
}
62+
}
63+

0 commit comments

Comments
 (0)