diff --git a/.github/workflows/aws-lambda-java-core.yml b/.github/workflows/aws-lambda-java-core.yml index 267d901c..c8064513 100644 --- a/.github/workflows/aws-lambda-java-core.yml +++ b/.github/workflows/aws-lambda-java-core.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 uses: actions/setup-java@v4 with: diff --git a/.github/workflows/aws-lambda-java-events-sdk-transformer.yml b/.github/workflows/aws-lambda-java-events-sdk-transformer.yml index 66f6b2bf..285848a9 100644 --- a/.github/workflows/aws-lambda-java-events-sdk-transformer.yml +++ b/.github/workflows/aws-lambda-java-events-sdk-transformer.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 uses: actions/setup-java@v4 with: diff --git a/.github/workflows/aws-lambda-java-events.yml b/.github/workflows/aws-lambda-java-events.yml index 04ab53a5..b3b360b4 100644 --- a/.github/workflows/aws-lambda-java-events.yml +++ b/.github/workflows/aws-lambda-java-events.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 uses: actions/setup-java@v4 with: diff --git a/.github/workflows/aws-lambda-java-log4j2.yml b/.github/workflows/aws-lambda-java-log4j2.yml index 7ae54cbe..03718e60 100644 --- a/.github/workflows/aws-lambda-java-log4j2.yml +++ b/.github/workflows/aws-lambda-java-log4j2.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 uses: actions/setup-java@v4 with: diff --git a/.github/workflows/aws-lambda-java-profiler.yml b/.github/workflows/aws-lambda-java-profiler.yml new file mode 100644 index 00000000..88032095 --- /dev/null +++ b/.github/workflows/aws-lambda-java-profiler.yml @@ -0,0 +1,74 @@ +name: Run integration tests for aws-lambda-java-profiler + +on: + pull_request: + branches: [ '*' ] + paths: + - 'experimental/aws-lambda-java-profiler/**' + - '.github/workflows/aws-lambda-java-profiler.yml' + push: + branches: ['*'] + paths: + - 'experimental/aws-lambda-java-profiler/**' + - '.github/workflows/aws-lambda-java-profiler.yml' + +jobs: + + build: + runs-on: ubuntu-latest + + permissions: + id-token: write + contents: read + + steps: + - uses: actions/checkout@v5 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: corretto + + - name: Issue AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ secrets.AWS_REGION_PROFILER_EXTENSION_INTEGRATION_TEST }} + role-to-assume: ${{ secrets.AWS_ROLE_PROFILER_EXTENSION_INTEGRATION_TEST }} + role-session-name: GitHubActionsRunIntegrationTests + role-duration-seconds: 900 + + - name: Build layer + working-directory: ./experimental/aws-lambda-java-profiler/extension + run: ./build_layer.sh + + - name: Publish layer + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/publish_layer.sh + + - name: Create the bucket layer + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/create_bucket.sh + + - name: Create Java function + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/create_function.sh + + - name: Invoke Java function + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/invoke_function.sh + + - name: Download from s3 + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/download_from_s3.sh + + - name: Upload profiles + uses: actions/upload-artifact@v4 + with: + name: profiles + path: /tmp/s3-artifacts + + - name: cleanup + if: always() + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/cleanup.sh \ No newline at end of file diff --git a/.github/workflows/aws-lambda-java-serialization.yml b/.github/workflows/aws-lambda-java-serialization.yml index c24c48d7..b2700e08 100644 --- a/.github/workflows/aws-lambda-java-serialization.yml +++ b/.github/workflows/aws-lambda-java-serialization.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 uses: actions/setup-java@v4 with: diff --git a/.github/workflows/aws-lambda-java-tests.yml b/.github/workflows/aws-lambda-java-tests.yml index a28bca88..1b818014 100644 --- a/.github/workflows/aws-lambda-java-tests.yml +++ b/.github/workflows/aws-lambda-java-tests.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 uses: actions/setup-java@v4 with: diff --git a/.github/workflows/repo-sync.yml b/.github/workflows/repo-sync.yml index 25f05029..300341c1 100644 --- a/.github/workflows/repo-sync.yml +++ b/.github/workflows/repo-sync.yml @@ -16,7 +16,7 @@ jobs: env: IS_CONFIGURED: ${{ secrets.SOURCE_REPO != '' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 if: ${{ env.IS_CONFIGURED == 'true' }} - uses: repo-sync/github-sync@v2 name: Sync repo to branch diff --git a/.github/workflows/runtime-interface-client_merge_to_main.yml b/.github/workflows/runtime-interface-client_merge_to_main.yml index 7db9d1aa..8909d56b 100644 --- a/.github/workflows/runtime-interface-client_merge_to_main.yml +++ b/.github/workflows/runtime-interface-client_merge_to_main.yml @@ -28,7 +28,7 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 uses: actions/setup-java@v4 @@ -47,6 +47,10 @@ jobs: - name: Available buildx platforms run: echo ${{ steps.buildx.outputs.platforms }} + - name: Build and install serialization dependency locally + working-directory: ./aws-lambda-java-serialization + run: mvn clean install -DskipTests + - name: Test Runtime Interface Client xplatform build - Run 'build' target working-directory: ./aws-lambda-java-runtime-interface-client run: make build @@ -86,6 +90,6 @@ jobs: - name: Upload coverage to Codecov if: env.CODECOV_TOKEN != null - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/runtime-interface-client_pr.yml b/.github/workflows/runtime-interface-client_pr.yml index 35a3334b..35c6ca06 100644 --- a/.github/workflows/runtime-interface-client_pr.yml +++ b/.github/workflows/runtime-interface-client_pr.yml @@ -15,13 +15,21 @@ jobs: smoke-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 uses: actions/setup-java@v4 with: java-version: 8 distribution: corretto + + - name: Build and install core dependency locally + working-directory: ./aws-lambda-java-core + run: mvn clean install + + - name: Build and install serialization dependency locally + working-directory: ./aws-lambda-java-serialization + run: mvn clean install -DskipTests - name: Runtime Interface Client smoke tests - Run 'pr' target working-directory: ./aws-lambda-java-runtime-interface-client @@ -32,7 +40,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 uses: actions/setup-java@v4 @@ -50,6 +58,14 @@ jobs: - name: Available buildx platforms run: echo ${{ steps.buildx.outputs.platforms }} + + - name: Build and install core dependency locally + working-directory: ./aws-lambda-java-core + run: mvn clean install + + - name: Build and install serialization dependency locally + working-directory: ./aws-lambda-java-serialization + run: mvn clean install -DskipTests - name: Test Runtime Interface Client xplatform build - Run 'build' target working-directory: ./aws-lambda-java-runtime-interface-client @@ -65,6 +81,6 @@ jobs: - name: Upload coverage to Codecov if: env.CODECOV_TOKEN != null - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 960055ad..2b5e7833 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -15,10 +15,10 @@ on: - '.github/workflows/samples.yml' jobs: - build-kinesis-sample: + build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 uses: actions/setup-java@v4 with: @@ -28,6 +28,9 @@ jobs: # Install events module - name: Install events with Maven run: mvn -B install --file aws-lambda-java-events/pom.xml + # Install serialization module + - name: Install serialization with Maven + run: mvn -B install --file aws-lambda-java-serialization/pom.xml # Install tests module - name: Install tests with Maven run: mvn -B install --file aws-lambda-java-tests/pom.xml @@ -39,11 +42,25 @@ jobs: custom-serialization: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 + - uses: actions/checkout@v5 + # Set up both Java 8 and 21 + - name: Set up Java 8 and 21 + uses: actions/setup-java@v4 with: - java-version: 21 + java-version: | + 8 + 21 distribution: corretto + + # Install events module using Java 8 + - name: Install events with Maven + run: | + export JAVA_HOME=$JAVA_HOME_8_X64 + mvn -B clean install \ + -Dmaven.compiler.source=1.8 \ + -Dmaven.compiler.target=1.8 \ + --file aws-lambda-java-events/pom.xml + # Build custom-serialization samples - name: install sam uses: aws-actions/setup-sam@v2 @@ -57,4 +74,3 @@ jobs: run: cd samples/custom-serialization/moshi && sam build && sam local invoke -e events/event.json | grep 200 - name: test request-stream-handler run: cd samples/custom-serialization/request-stream-handler && sam build && sam local invoke -e events/event.json | grep 200 - diff --git a/.gitignore b/.gitignore index 371bed6b..9f99cc41 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,10 @@ dependency-reduced-pom.xml # snapshot process aws-lambda-java-runtime-interface-client/pom.xml.versionsBackup + +# profiler +experimental/aws-lambda-java-profiler/integration_tests/helloworld/build +experimental/aws-lambda-java-profiler/extension/build/ +experimental/aws-lambda-java-profiler/integration_tests/helloworld/bin +!experimental/aws-lambda-java-profiler/extension/gradle/wrapper/*.jar +/scratch/ diff --git a/README.md b/README.md index 4f22b2cc..b5153a87 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ public class HandlerStream implements RequestStreamHandler { com.amazonaws aws-lambda-java-core - 1.2.3 + 1.3.0 ``` @@ -75,7 +75,7 @@ public class SqsHandler implements RequestHandler { com.amazonaws aws-lambda-java-events - 3.14.0 + 3.16.0 ``` @@ -139,6 +139,18 @@ See the [README](aws-lambda-java-log4j2/README.md) or the [official documentatio ``` +## Lambda Profiler Extension for Java - aws-lambda-java-profiler + +

+ A flame graph of a Java Lambda function +

+ +This project allows you to profile your Java functions invoke by invoke, with high fidelity, and no code changes. It +uses the [async-profiler](https://github.com/async-profiler/async-profiler) project to produce profiling data and +automatically uploads the data as flame graphs to S3. + +Follow our [Quick Start](experimental/aws-lambda-java-profiler#quick-start) to profile your functions. + ## Java implementation of the Runtime Interface Client API - aws-lambda-java-runtime-interface-client [![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-runtime-interface-client.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-runtime-interface-client) @@ -151,7 +163,7 @@ The purpose of this package is to allow developers to deploy their applications com.amazonaws aws-lambda-java-runtime-interface-client - 2.6.0 + 2.7.0 ``` diff --git a/aws-lambda-java-core/RELEASE.CHANGELOG.md b/aws-lambda-java-core/RELEASE.CHANGELOG.md index ebd0566f..aebc8ecd 100644 --- a/aws-lambda-java-core/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-core/RELEASE.CHANGELOG.md @@ -1,3 +1,11 @@ +### September 3, 2025 +`1.4.0` +- Getter support for x-ray trace ID through the Context object + +### May 26, 2025 +`1.3.0` +- Adding support for multi tenancy ([#545](https://github.com/aws/aws-lambda-java-libs/pull/545)) + ### August 17, 2023 `1.2.3`: - Extended logger interface with level-aware logging backend functions diff --git a/aws-lambda-java-core/pom.xml b/aws-lambda-java-core/pom.xml index 0dd848a9..cca9d0cd 100644 --- a/aws-lambda-java-core/pom.xml +++ b/aws-lambda-java-core/pom.xml @@ -5,7 +5,7 @@ com.amazonaws aws-lambda-java-core - 1.2.3 + 1.4.0 jar AWS Lambda Java Core Library @@ -36,13 +36,6 @@ 1.8 - - - sonatype-nexus-staging - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - dev @@ -115,14 +108,12 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java index a0850e78..ed9311a1 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java @@ -100,4 +100,23 @@ public interface Context { */ LambdaLogger getLogger(); + /** + * + * Returns the tenant ID associated with the request. + * + * @return null by default + */ + default String getTenantId() { + return null; + } + + /** + * + * Returns the X-Ray trace ID associated with the request. + * + * @return null by default + */ + default String getXrayTraceId() { + return null; + } } diff --git a/aws-lambda-java-events-sdk-transformer/pom.xml b/aws-lambda-java-events-sdk-transformer/pom.xml index 6a2b1735..9b6afece 100644 --- a/aws-lambda-java-events-sdk-transformer/pom.xml +++ b/aws-lambda-java-events-sdk-transformer/pom.xml @@ -5,7 +5,7 @@ com.amazonaws aws-lambda-java-events-sdk-transformer - 3.1.0 + 3.1.1 jar AWS Lambda Java Events SDK Transformer Library @@ -63,7 +63,7 @@ com.amazonaws aws-lambda-java-events - 3.11.2 + 3.16.1 provided diff --git a/aws-lambda-java-events/README.md b/aws-lambda-java-events/README.md index db423e07..43c25d76 100644 --- a/aws-lambda-java-events/README.md +++ b/aws-lambda-java-events/README.md @@ -31,6 +31,7 @@ * `CognitoUserPoolPreAuthenticationEvent` * `CognitoUserPoolPreSignUpEvent` * `CognitoUserPoolPreTokenGenerationEvent` +* `CognitoUserPoolPreTokenGenerationEventV2` * `CognitoUserPoolVerifyAuthChallengeResponseEvent` * `ConfigEvent` * `ConnectEvent` @@ -73,7 +74,7 @@ com.amazonaws aws-lambda-java-events - 3.14.0 + 3.16.0 ... diff --git a/aws-lambda-java-events/RELEASE.CHANGELOG.md b/aws-lambda-java-events/RELEASE.CHANGELOG.md index 7ddf2bff..a4bcd10a 100644 --- a/aws-lambda-java-events/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-events/RELEASE.CHANGELOG.md @@ -1,3 +1,13 @@ +### June 17, 2025 +`3.16.0`: +- Add Schema metadata related attributes in KafkaEvent ([#548](https://github.com/aws/aws-lambda-java-libs/pull/548)) + +### January 31, 2025 +`3.15.0`: +- Fix `CognitoUserPoolPreTokenGenerationEventV2` model ([#519](https://github.com/aws/aws-lambda-java-libs/pull/519)) +- Add RotationToken to SecretsManagerRotationEvent ([#520](https://github.com/aws/aws-lambda-java-libs/pull/520)) + + ### September 13, 2024 `3.14.0`: - Fix name of s3Bucket field of Task class in S3BatchEventV2 ([#506](https://github.com/aws/aws-lambda-java-libs/pull/506)) diff --git a/aws-lambda-java-events/pom.xml b/aws-lambda-java-events/pom.xml index ec3806fb..925273e9 100644 --- a/aws-lambda-java-events/pom.xml +++ b/aws-lambda-java-events/pom.xml @@ -5,7 +5,7 @@ com.amazonaws aws-lambda-java-events - 3.14.0 + 3.16.1 jar AWS Lambda Java Events Library diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEventV2.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEventV2.java index c7250570..9faeb970 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEventV2.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEventV2.java @@ -127,8 +127,8 @@ public static class AccessTokenGeneration { @Builder(setterPrefix = "with") @NoArgsConstructor public static class GroupOverrideDetails { - private Map groupsToOverride; - private Map iamRolesToOverride; + private String[] groupsToOverride; + private String[] iamRolesToOverride; private String preferredRole; } } \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ConnectEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ConnectEvent.java index 38547ac2..e9487561 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ConnectEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ConnectEvent.java @@ -59,7 +59,7 @@ public static class ContactData implements Serializable, Cloneable { private String initiationMethod; private String instanceArn; private String previousContactId; - private String queue; + private Queue queue; private SystemEndpoint systemEndpoint; } @@ -80,4 +80,13 @@ public static class SystemEndpoint implements Serializable, Cloneable { private String address; private String type; } + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Queue implements Serializable, Cloneable { + private String name; + private String ARN; + } + } diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KafkaEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KafkaEvent.java index dd051d48..aa6c00de 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KafkaEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KafkaEvent.java @@ -43,6 +43,8 @@ public static class KafkaEventRecord { private String key; private String value; private List> headers; + private SchemaMetadata keySchemaMetadata; + private SchemaMetadata valueSchemaMetadata; } @Data @@ -59,4 +61,13 @@ public String toString() { return topic + "-" + partition; } } + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder(setterPrefix = "with") + public static class SchemaMetadata { + private String schemaId; + private String dataFormat; + } } diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SecretsManagerRotationEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SecretsManagerRotationEvent.java index 4634c515..3e8df5bc 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SecretsManagerRotationEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SecretsManagerRotationEvent.java @@ -35,5 +35,6 @@ public class SecretsManagerRotationEvent { private String step; private String secretId; private String clientRequestToken; + private String rotationToken; } diff --git a/aws-lambda-java-runtime-interface-client/Dockerfile.rie b/aws-lambda-java-runtime-interface-client/Dockerfile.rie new file mode 100644 index 00000000..66a01c83 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/Dockerfile.rie @@ -0,0 +1,8 @@ +FROM public.ecr.aws/lambda/java:21 + +COPY target/aws-lambda-java-runtime-interface-client-*.jar ${LAMBDA_TASK_ROOT}/ +COPY target/aws-lambda-java-core-*.jar ${LAMBDA_TASK_ROOT}/ +COPY target/aws-lambda-java-serialization-*.jar ${LAMBDA_TASK_ROOT}/ +COPY test-handlers/EchoHandler.class ${LAMBDA_TASK_ROOT}/ + +CMD ["EchoHandler::handleRequest"] \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/Makefile b/aws-lambda-java-runtime-interface-client/Makefile index b3a20421..6c3a268f 100644 --- a/aws-lambda-java-runtime-interface-client/Makefile +++ b/aws-lambda-java-runtime-interface-client/Makefile @@ -65,6 +65,10 @@ publish: test-publish: ./ric-dev-environment/test-platform-specific-jar-snapshot.sh +.PHONY: test-rie +test-rie: + ./scripts/test-rie.sh "EchoHandler::handleRequest" + define HELP_MESSAGE Usage: $ make [TARGETS] @@ -74,5 +78,5 @@ TARGETS dev Run all development tests after a change. pr Perform all checks before submitting a Pull Request. test Run the Unit tests. - + test-rie Build and test RIC locally with Lambda Runtime Interface Emulator. (Requires building the project first) endef diff --git a/aws-lambda-java-runtime-interface-client/README.md b/aws-lambda-java-runtime-interface-client/README.md index 8a95e7de..67a5972d 100644 --- a/aws-lambda-java-runtime-interface-client/README.md +++ b/aws-lambda-java-runtime-interface-client/README.md @@ -70,7 +70,7 @@ pom.xml com.amazonaws aws-lambda-java-runtime-interface-client - 2.6.0 + 2.7.0 @@ -138,6 +138,49 @@ This command invokes the function running in the container image and returns a r *Alternately, you can also include RIE as a part of your base image. See the AWS documentation on how to [Build RIE into your base image](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html#images-test-alternative).* +### Automated Local Testing + +For developers working on this runtime interface client, we provide an automated testing script that handles RIE setup, dependency management, and Docker orchestration. + +*Prerequisites:* +- Build the project first: `mvn clean install` +- Docker must be installed and running + +*To run automated tests:* + +```shell script +make test-rie +``` + +This single command will: +- Automatically download required dependencies (aws-lambda-java-core, aws-lambda-java-serialization) +- Build a Docker image with RIE pre-installed +- Compile and run a test Lambda function (EchoHandler) +- Execute the function and validate the response +- Clean up containers automatically + +The test uses a simple EchoHandler that returns the input event, making it easy to verify the runtime interface client is working correctly. + +## Test Coverage + +This project uses JaCoCo for code coverage analysis. To exclude classes from JaCoCo coverage, add them to the `jacoco-maven-plugin` configuration: + +```xml + + org.jacoco + jacoco-maven-plugin + + + **/*Exception.class + **/dto/*.class + **/YourClassName.class + + + +``` + +This project excludes by default: exceptions, interfaces, DTOs, constants, and runtime-only classes. + ### Troubleshooting While running integration tests, you might encounter the Docker Hub rate limit error with the following body: @@ -160,7 +203,7 @@ platform-specific JAR by setting the ``. com.amazonaws aws-lambda-java-runtime-interface-client - 2.6.0 + 2.7.0 linux-x86_64 ``` diff --git a/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md b/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md index 6a781b27..ac073ae8 100644 --- a/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md @@ -1,3 +1,7 @@ +### May 21, 2025 +`2.7.0` +- Adding support for multi tenancy ([#540](https://github.com/aws/aws-lambda-java-libs/pull/540)) + ### August 7, 2024 `2.6.0` - Runtime API client improvements: use Lambda-Runtime-Function-Error-Type for reporting errors in format "Runtime." diff --git a/aws-lambda-java-runtime-interface-client/pom.xml b/aws-lambda-java-runtime-interface-client/pom.xml index b5ea2721..c854fabc 100644 --- a/aws-lambda-java-runtime-interface-client/pom.xml +++ b/aws-lambda-java-runtime-interface-client/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.amazonaws aws-lambda-java-runtime-interface-client - 2.6.0 + 2.8.4 jar AWS Lambda Java Runtime Interface Client @@ -61,12 +61,12 @@ com.amazonaws aws-lambda-java-core - 1.2.3 + 1.4.0 com.amazonaws aws-lambda-java-serialization - 1.1.2 + 1.1.6 @@ -87,6 +87,12 @@ 4.11.0 test + + org.mockito + mockito-junit-jupiter + 4.11.0 + test + com.squareup.okhttp3 mockwebserver @@ -208,6 +214,24 @@ org.jacoco jacoco-maven-plugin ${jacoco.maven.plugin.version} + + + + **/*Exception.class + + **/Resource.class + + **/dto/*.class + + **/ReservedRuntimeEnvironmentVariables.class + **/RapidErrorType.class + + **/FrameType.class + **/StructuredLogMessage.class + + **/AWSLambda.class + + default-prepare-agent @@ -341,16 +365,52 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central + + org.codehaus.mojo + build-helper-maven-plugin + 3.4.0 + + + attach-platform-artifacts + package + + attach-artifact + + + + + ${project.build.directory}/${project.build.finalName}-linux-x86_64.jar + jar + linux-x86_64 + + + ${project.build.directory}/${project.build.finalName}-linux-aarch_64.jar + jar + linux-aarch_64 + + + ${project.build.directory}/${project.build.finalName}-linux_musl-x86_64.jar + jar + linux_musl-x86_64 + + + ${project.build.directory}/${project.build.finalName}-linux_musl-aarch_64.jar + jar + linux_musl-aarch_64 + + + + + + diff --git a/aws-lambda-java-runtime-interface-client/scripts/test-rie.sh b/aws-lambda-java-runtime-interface-client/scripts/test-rie.sh new file mode 100755 index 00000000..b69c967a --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/scripts/test-rie.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +SERIALIZATION_ROOT="$(dirname "$PROJECT_ROOT")/aws-lambda-java-serialization" + +if ! ls "$PROJECT_ROOT"/target/aws-lambda-java-runtime-interface-client-*.jar >/dev/null 2>&1; then + echo "RIC jar not found. Please build the project first with 'mvn package'." + exit 1 +fi + +IMAGE_TAG="java-ric-rie-test" + +HANDLER="${1:-EchoHandler::handleRequest}" + +echo "Starting RIE test setup for Java..." + +# Build local dependencies if not present +CORE_ROOT="$(dirname "$PROJECT_ROOT")/aws-lambda-java-core" +if ! ls "$PROJECT_ROOT"/target/aws-lambda-java-core-*.jar >/dev/null 2>&1; then + echo "Building local aws-lambda-java-core..." + (cd "$CORE_ROOT" && mvn package -DskipTests) + cp "$CORE_ROOT"/target/aws-lambda-java-core-*.jar "$PROJECT_ROOT/target/" +fi + +if ! ls "$PROJECT_ROOT"/target/aws-lambda-java-serialization-*.jar >/dev/null 2>&1; then + echo "Building local aws-lambda-java-serialization..." + (cd "$SERIALIZATION_ROOT" && mvn package -DskipTests) + cp "$SERIALIZATION_ROOT"/target/aws-lambda-java-serialization-*.jar "$PROJECT_ROOT/target/" +fi + +echo "Compiling EchoHandler..." +javac -source 21 -target 21 -cp "$(ls "$PROJECT_ROOT"/target/aws-lambda-java-runtime-interface-client-*.jar):$(ls "$PROJECT_ROOT"/target/aws-lambda-java-core-*.jar):$(ls "$PROJECT_ROOT"/target/aws-lambda-java-serialization-*.jar)" \ + -d "$PROJECT_ROOT/test-handlers/" "$PROJECT_ROOT/test-handlers/EchoHandler.java" + +echo "Building test Docker image..." +docker build -t "$IMAGE_TAG" -f "$PROJECT_ROOT/Dockerfile.rie" "$PROJECT_ROOT" + +echo "Starting test container on port 9000..." +echo "" +echo "In another terminal, invoke with:" +echo "curl -s -X POST -H 'Content-Type: application/json' \"http://localhost:9000/2015-03-31/functions/function/invocations\" -d '{\"message\":\"test\"}'" +echo "" + +exec docker run -it -p 9000:8080 -e _HANDLER="$HANDLER" "$IMAGE_TAG" \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java index 986f8b7b..2eeb14e3 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java @@ -2,6 +2,7 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ + package com.amazonaws.services.lambda.runtime.api.client; import com.amazonaws.services.lambda.crac.Core; @@ -35,7 +36,6 @@ import java.security.Security; import java.util.Properties; - /** * The entrypoint of this class is {@link AWSLambda#startRuntime}. It performs two main tasks: * @@ -137,6 +137,42 @@ private static LambdaRequestHandler findRequestHandler(final String handlerStrin return requestHandler; } + private static LambdaRequestHandler getLambdaRequestHandlerObject(String handler, LambdaContextLogger lambdaLogger) throws ClassNotFoundException, IOException { + UnsafeUtil.disableIllegalAccessWarning(); + + System.setOut(new PrintStream(new LambdaOutputStream(System.out), false, "UTF-8")); + System.setErr(new PrintStream(new LambdaOutputStream(System.err), false, "UTF-8")); + setupRuntimeLogger(lambdaLogger); + + runtimeClient = new LambdaRuntimeApiClientImpl(LambdaEnvironment.RUNTIME_API); + + String taskRoot = System.getProperty("user.dir"); + String libRoot = "/opt/java"; + // Make system classloader the customer classloader's parent to ensure any aws-lambda-java-core classes + // are loaded from the system classloader. + customerClassLoader = new CustomerClassLoader(taskRoot, libRoot, ClassLoader.getSystemClassLoader()); + Thread.currentThread().setContextClassLoader(customerClassLoader); + + // Load the user's handler + LambdaRequestHandler requestHandler = null; + try { + requestHandler = findRequestHandler(handler, customerClassLoader); + } catch (UserFault userFault) { + lambdaLogger.log(userFault.reportableError(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + LambdaError error = new LambdaError( + LambdaErrorConverter.fromUserFault(userFault), + RapidErrorType.BadFunctionCode); + runtimeClient.reportInitError(error); + System.exit(1); + } + + if (INIT_TYPE_SNAP_START.equals(AWS_LAMBDA_INITIALIZATION_TYPE)) { + onInitComplete(lambdaLogger); + } + + return requestHandler; + } + public static void setupRuntimeLogger(LambdaLogger lambdaLogger) throws ClassNotFoundException { ReflectUtil.setStaticField( @@ -176,55 +212,27 @@ private static LogSink createLogSink() { } } - public static void main(String[] args) { - startRuntime(args[0]); - } + public static void main(String[] args) throws Throwable { + try (LambdaContextLogger logger = initLogger()) { + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandlerObject(args[0], logger); + startRuntimeLoop(lambdaRequestHandler, logger); - private static void startRuntime(String handler) { - try (LogSink logSink = createLogSink()) { - LambdaContextLogger logger = new LambdaContextLogger( - logSink, - LogLevel.fromString(LambdaEnvironment.LAMBDA_LOG_LEVEL), - LogFormat.fromString(LambdaEnvironment.LAMBDA_LOG_FORMAT) - ); - startRuntime(handler, logger); - } catch (Throwable t) { + } catch (IOException | ClassNotFoundException t) { throw new Error(t); } } - private static void startRuntime(String handler, LambdaContextLogger lambdaLogger) throws Throwable { - UnsafeUtil.disableIllegalAccessWarning(); - - System.setOut(new PrintStream(new LambdaOutputStream(System.out), false, "UTF-8")); - System.setErr(new PrintStream(new LambdaOutputStream(System.err), false, "UTF-8")); - setupRuntimeLogger(lambdaLogger); + private static LambdaContextLogger initLogger() { + LogSink logSink = createLogSink(); + LambdaContextLogger logger = new LambdaContextLogger( + logSink, + LogLevel.fromString(LambdaEnvironment.LAMBDA_LOG_LEVEL), + LogFormat.fromString(LambdaEnvironment.LAMBDA_LOG_FORMAT)); - runtimeClient = new LambdaRuntimeApiClientImpl(LambdaEnvironment.RUNTIME_API); - - String taskRoot = System.getProperty("user.dir"); - String libRoot = "/opt/java"; - // Make system classloader the customer classloader's parent to ensure any aws-lambda-java-core classes - // are loaded from the system classloader. - customerClassLoader = new CustomerClassLoader(taskRoot, libRoot, ClassLoader.getSystemClassLoader()); - Thread.currentThread().setContextClassLoader(customerClassLoader); + return logger; + } - // Load the user's handler - LambdaRequestHandler requestHandler; - try { - requestHandler = findRequestHandler(handler, customerClassLoader); - } catch (UserFault userFault) { - lambdaLogger.log(userFault.reportableError(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); - LambdaError error = new LambdaError( - LambdaErrorConverter.fromUserFault(userFault), - RapidErrorType.BadFunctionCode); - runtimeClient.reportInitError(error); - System.exit(1); - return; - } - if (INIT_TYPE_SNAP_START.equals(AWS_LAMBDA_INITIALIZATION_TYPE)) { - onInitComplete(lambdaLogger); - } + private static void startRuntimeLoop(LambdaRequestHandler requestHandler, LambdaContextLogger lambdaLogger) throws Throwable { boolean shouldExit = false; while (!shouldExit) { UserFault userFault = null; @@ -240,7 +248,7 @@ private static void startRuntime(String handler, LambdaContextLogger lambdaLogge payload = requestHandler.call(request); runtimeClient.reportInvocationSuccess(request.getId(), payload.toByteArray()); // clear interrupted flag in case if it was set by user's code - boolean ignored = Thread.interrupted(); + Thread.interrupted(); } catch (UserFault f) { shouldExit = f.fatal; userFault = f; @@ -278,6 +286,7 @@ static void onInitComplete(final LambdaContextLogger lambdaLogger) throws IOExce RapidErrorType.BeforeCheckpointError)); System.exit(64); } + try { Core.getGlobalContext().afterRestore(null); } catch (Exception restoreExc) { diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java index 096bb862..2876499e 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java @@ -581,6 +581,8 @@ public ByteArrayOutputStream call(InvocationRequest request) throws Error, Excep cognitoIdentity, LambdaEnvironment.FUNCTION_VERSION, request.getInvokedFunctionArn(), + request.getTenantId(), + request.getXrayTraceId(), clientContext ); diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java index 2ce3b844..20b77262 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java @@ -22,6 +22,8 @@ public class LambdaContext implements Context { private final long deadlineTimeInMs; private final CognitoIdentity cognitoIdentity; private final ClientContext clientContext; + private final String tenantId; + private final String xrayTraceId; private final LambdaLogger logger; public LambdaContext( @@ -34,6 +36,8 @@ public LambdaContext( CognitoIdentity identity, String functionVersion, String invokedFunctionArn, + String tenantId, + String xrayTraceId, ClientContext clientContext ) { this.memoryLimit = memoryLimit; @@ -46,6 +50,8 @@ public LambdaContext( this.clientContext = clientContext; this.functionVersion = functionVersion; this.invokedFunctionArn = invokedFunctionArn; + this.tenantId = tenantId; + this.xrayTraceId = xrayTraceId; this.logger = com.amazonaws.services.lambda.runtime.LambdaRuntime.getLogger(); } @@ -91,6 +97,14 @@ public int getRemainingTimeInMillis() { return delta > 0 ? delta : 0; } + public String getTenantId() { + return tenantId; + } + + public String getXrayTraceId() { + return xrayTraceId; + } + public LambdaLogger getLogger() { return logger; } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java index b98721eb..f463e7ee 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java @@ -41,6 +41,7 @@ private StructuredLogMessage createLogMessage(String message, LogLevel logLevel) if (lambdaContext != null) { msg.AWSRequestId = lambdaContext.getAwsRequestId(); + msg.tenantId = lambdaContext.getTenantId(); } return msg; } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java index 693eb015..dd356912 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java @@ -7,9 +7,11 @@ import com.amazonaws.services.lambda.runtime.logging.LogFormat; import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import java.io.Closeable; +import java.io.IOException; import static java.nio.charset.StandardCharsets.UTF_8; -public class LambdaContextLogger extends AbstractLambdaLogger { +public class LambdaContextLogger extends AbstractLambdaLogger implements Closeable { // If a null string is passed in, replace it with "null", // replicating the behavior of System.out.println(null); private static final byte[] NULL_BYTES_VALUE = "null".getBytes(UTF_8); @@ -29,4 +31,10 @@ protected void logMessage(byte[] message, LogLevel logLevel) { sink.log(logLevel, this.logFormat, message); } } + + @Override + public void close() throws IOException { + sink.close(); + + } } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java index 5299bffa..0ae19961 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java @@ -12,4 +12,5 @@ class StructuredLogMessage { public String message; public LogLevel level; public String AWSRequestId; + public String tenantId; } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest.java index 7bdc2500..656945b4 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest.java @@ -40,6 +40,11 @@ public class InvocationRequest { */ private String cognitoIdentity; + /** + * The tenant ID associated with the request. + */ + private String tenantId; + private byte[] content; public String getId() { @@ -94,6 +99,14 @@ public void setCognitoIdentity(String cognitoIdentity) { this.cognitoIdentity = cognitoIdentity; } + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + public byte[] getContent() { return content; } diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp index 7fe47aa4..f0679661 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp @@ -20,6 +20,7 @@ static jfieldID contentField; static jfieldID clientContextField; static jfieldID cognitoIdentityField; static jfieldID xrayTraceIdField; +static jfieldID tenantIdField; jint JNI_OnLoad(JavaVM* vm, void* reserved) { @@ -41,6 +42,7 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { xrayTraceIdField = env->GetFieldID(invocationRequestClass , "xrayTraceId", "Ljava/lang/String;"); clientContextField = env->GetFieldID(invocationRequestClass , "clientContext", "Ljava/lang/String;"); cognitoIdentityField = env->GetFieldID(invocationRequestClass , "cognitoIdentity", "Ljava/lang/String;"); + tenantIdField = env->GetFieldID(invocationRequestClass, "tenantId", "Ljava/lang/String;"); return JNI_VERSION; } @@ -106,6 +108,10 @@ JNIEXPORT jobject JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_ CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, cognitoIdentityField, env->NewStringUTF(response.cognito_identity.c_str()))); } + if(response.tenant_id != ""){ + CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, tenantIdField, env->NewStringUTF(response.tenant_id.c_str()))); + } + bytes = reinterpret_cast(response.payload.c_str()); CHECK_EXCEPTION(env, jArray = env->NewByteArray(response.payload.length())); CHECK_EXCEPTION(env, env->SetByteArrayRegion(jArray, 0, response.payload.length(), bytes)); diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/include/aws/lambda-runtime/runtime.h b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/include/aws/lambda-runtime/runtime.h index 94e1e22c..d7db5f18 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/include/aws/lambda-runtime/runtime.h +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/include/aws/lambda-runtime/runtime.h @@ -61,6 +61,11 @@ struct invocation_request { */ std::chrono::time_point deadline; + /** + * Tenant ID of the current invocation. + */ + std::string tenant_id; + /** * The number of milliseconds left before lambda terminates the current execution. */ diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/src/runtime.cpp b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/src/runtime.cpp index 91750840..eeaf0e7b 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/src/runtime.cpp +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/src/runtime.cpp @@ -40,6 +40,7 @@ static constexpr auto CLIENT_CONTEXT_HEADER = "lambda-runtime-client-context"; static constexpr auto COGNITO_IDENTITY_HEADER = "lambda-runtime-cognito-identity"; static constexpr auto DEADLINE_MS_HEADER = "lambda-runtime-deadline-ms"; static constexpr auto FUNCTION_ARN_HEADER = "lambda-runtime-invoked-function-arn"; +static constexpr auto TENANT_ID_HEADER = "lambda-runtime-aws-tenant-id"; enum Endpoints { INIT, @@ -301,6 +302,10 @@ runtime::next_outcome runtime::get_next() req.payload.c_str(), static_cast(req.get_time_remaining().count())); } + + if (resp.has_header(TENANT_ID_HEADER)) { + req.tenant_id = resp.get_header(TENANT_ID_HEADER); + } return next_outcome(req); } diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoaderTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoaderTest.java new file mode 100644 index 00000000..38147d21 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoaderTest.java @@ -0,0 +1,153 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; + +import static org.junit.jupiter.api.Assertions.*; + +class ClasspathLoaderTest { + + @Test + void testLoadAllClassesWithNoClasspath() throws IOException { + String originalClasspath = System.getProperty("java.class.path"); + try { + System.clearProperty("java.class.path"); + ClasspathLoader.main(new String[]{}); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithEmptyClasspath() { + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", ""); + assertThrows(FileNotFoundException.class, () -> + ClasspathLoader.main(new String[]{})); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithInvalidPath() { + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", "nonexistent/path"); + assertThrows(FileNotFoundException.class, () -> + ClasspathLoader.main(new String[]{})); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithValidJar(@TempDir Path tempDir) throws IOException { + File jarFile = createSimpleJar(tempDir, "test.jar", "TestClass"); + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", jarFile.getAbsolutePath()); + ClasspathLoader.main(new String[]{}); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithDirectory(@TempDir Path tempDir) throws IOException { + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", tempDir.toString()); + ClasspathLoader.main(new String[]{}); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithMultipleEntries(@TempDir Path tempDir) throws IOException { + File jarFile1 = createSimpleJar(tempDir, "test1.jar", "TestClass1"); + File jarFile2 = createSimpleJar(tempDir, "test2.jar", "TestClass2"); + + String originalClasspath = System.getProperty("java.class.path"); + try { + String newClasspath = jarFile1.getAbsolutePath() + + File.pathSeparator + + jarFile2.getAbsolutePath(); + System.setProperty("java.class.path", newClasspath); + ClasspathLoader.main(new String[]{}); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithBlocklistedClass(@TempDir Path tempDir) throws IOException { + File jarFile = tempDir.resolve("blocklist-test.jar").toFile(); + + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile))) { + JarEntry blockedEntry = new JarEntry("META-INF/versions/9/module-info.class"); + jos.putNextEntry(blockedEntry); + jos.write("dummy content".getBytes()); + jos.closeEntry(); + + JarEntry normalEntry = new JarEntry("com/test/Normal.class"); + jos.putNextEntry(normalEntry); + jos.write("dummy content".getBytes()); + jos.closeEntry(); + } + + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", jarFile.getAbsolutePath()); + ClasspathLoader.main(new String[]{}); + // The test passes if no exception is thrown and the blocklisted class is skipped + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + private File createSimpleJar(Path tempDir, String jarName, String className) throws IOException { + File jarFile = tempDir.resolve(jarName).toFile(); + + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile))) { + // Add a simple non-class file to make it a valid jar + JarEntry entry = new JarEntry("com/test/" + className + ".txt"); + jos.putNextEntry(entry); + jos.write("test content".getBytes()); + jos.closeEntry(); + } + + return jarFile; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoaderTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoaderTest.java index 0169d0d6..71fb013f 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoaderTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoaderTest.java @@ -1,4 +1,7 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client; diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfoTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfoTest.java new file mode 100644 index 00000000..e134ddc8 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfoTest.java @@ -0,0 +1,132 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class HandlerInfoTest { + + @Test + void testConstructor() { + Class testClass = String.class; + String methodName = "testMethod"; + + HandlerInfo info = new HandlerInfo(testClass, methodName); + + assertNotNull(info); + assertEquals(testClass, info.clazz); + assertEquals(methodName, info.methodName); + } + + @Test + void testFromStringWithoutMethod() throws Exception { + String handler = "java.lang.String"; + HandlerInfo info = HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()); + + assertEquals(String.class, info.clazz); + assertNull(info.methodName); + } + + @Test + void testFromStringWithMethod() throws Exception { + String handler = "java.lang.String::length"; + HandlerInfo info = HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()); + + assertEquals(String.class, info.clazz); + assertEquals("length", info.methodName); + } + + @Test + void testFromStringWithEmptyClass() { + String handler = "::method"; + + assertThrows(HandlerInfo.InvalidHandlerException.class, () -> + HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()) + ); + } + + @Test + void testFromStringWithEmptyMethod() { + String handler = "java.lang.String::"; + + assertThrows(HandlerInfo.InvalidHandlerException.class, () -> + HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()) + ); + } + + @Test + void testFromStringWithNonexistentClass() { + String handler = "com.nonexistent.TestClass::method"; + + assertThrows(ClassNotFoundException.class, () -> + HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()) + ); + } + + @Test + void testFromStringWithNullHandler() { + assertThrows(NullPointerException.class, () -> + HandlerInfo.fromString(null, ClassLoader.getSystemClassLoader()) + ); + } + + @Test + void testClassNameWithoutMethod() { + String handler = "java.lang.String"; + String className = HandlerInfo.className(handler); + + assertEquals("java.lang.String", className); + } + + @Test + void testClassNameWithMethod() { + String handler = "java.lang.String::length"; + String className = HandlerInfo.className(handler); + + assertEquals("java.lang.String", className); + } + + @Test + void testClassNameWithEmptyString() { + String handler = ""; + String className = HandlerInfo.className(handler); + + assertEquals("", className); + } + + @Test + void testClassNameWithOnlyDelimiter() { + String handler = "::"; + String className = HandlerInfo.className(handler); + + assertEquals("", className); + } + + @Test + void testInvalidHandlerExceptionSerialVersionUID() { + assertEquals(-1L, HandlerInfo.InvalidHandlerException.serialVersionUID); + } + + @Test + void testFromStringWithInnerClass() throws Exception { + // Create a custom class loader that can load our test class + ClassLoader cl = new ClassLoader() { + @Override + public Class loadClass(String name) throws ClassNotFoundException { + if (name.equals("com.test.OuterClass$InnerClass")) { + throw new ClassNotFoundException("Test class not found"); + } + return super.loadClass(name); + } + }; + + String handler = "com.test.OuterClass$InnerClass::method"; + assertThrows(ClassNotFoundException.class, () -> + HandlerInfo.fromString(handler, cl) + ); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java new file mode 100644 index 00000000..d86b7385 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java @@ -0,0 +1,142 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class LambdaRequestHandlerTest { + + private InvocationRequest mockRequest; + + @BeforeEach + void setUp() { + mockRequest = mock(InvocationRequest.class); + } + + @Test + void testInitErrorHandler() { + String className = "com.example.TestClass"; + Exception testException = new RuntimeException("initialization error"); + + LambdaRequestHandler handler = LambdaRequestHandler.initErrorHandler(testException, className); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Error loading class " + className + ": initialization error", fault.msg); + assertEquals("java.lang.RuntimeException", fault.exception); + assertTrue(fault.fatal); + } + + @Test + void testClassNotFound() { + String className = "com.example.MissingClass"; + Exception testException = new ClassNotFoundException("class not found"); + + LambdaRequestHandler handler = LambdaRequestHandler.classNotFound(testException, className); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Class not found: " + className, fault.msg); + assertEquals("java.lang.ClassNotFoundException", fault.exception); + assertFalse(fault.fatal); + } + + @Test + void testUserFaultHandlerConstructor() { + UserFault testFault = new UserFault("test message", "TestException", "test trace"); + LambdaRequestHandler.UserFaultHandler handler = new LambdaRequestHandler.UserFaultHandler(testFault); + + assertNotNull(handler); + assertSame(testFault, handler.fault); + } + + @Test + void testUserFaultHandlerCallThrowsFault() { + UserFault testFault = new UserFault("test message", "TestException", "test trace"); + LambdaRequestHandler.UserFaultHandler handler = new LambdaRequestHandler.UserFaultHandler(testFault); + + UserFault thrownFault = assertThrows(UserFault.class, () -> handler.call(mockRequest)); + assertSame(testFault, thrownFault); + } + + @Test + void testInitErrorHandlerWithNullMessage() { + String className = "com.example.TestClass"; + Exception testException = new RuntimeException(); + + LambdaRequestHandler handler = LambdaRequestHandler.initErrorHandler(testException, className); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Error loading class " + className, fault.msg); + assertEquals("java.lang.RuntimeException", fault.exception); + assertTrue(fault.fatal); + } + + @Test + void testInitErrorHandlerWithNullClassName() { + Exception testException = new RuntimeException("test error"); + + LambdaRequestHandler handler = LambdaRequestHandler.initErrorHandler(testException, null); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Error loading class null: test error", fault.msg); + assertEquals("java.lang.RuntimeException", fault.exception); + assertTrue(fault.fatal); + } + + @Test + void testClassNotFoundWithNullClassName() { + Exception testException = new ClassNotFoundException("test error"); + + LambdaRequestHandler handler = LambdaRequestHandler.classNotFound(testException, null); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Class not found: null", fault.msg); + assertEquals("java.lang.ClassNotFoundException", fault.exception); + assertFalse(fault.fatal); + } + + @Test + void testUserFaultHandlerCallWithNullRequest() { + UserFault testFault = new UserFault("test message", "TestException", "test trace"); + LambdaRequestHandler.UserFaultHandler handler = new LambdaRequestHandler.UserFaultHandler(testFault); + + UserFault thrownFault = assertThrows(UserFault.class, () -> handler.call(null)); + assertSame(testFault, thrownFault); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoaderTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoaderTest.java new file mode 100644 index 00000000..c2c88797 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoaderTest.java @@ -0,0 +1,153 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Type; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PojoSerializerLoaderTest { + + @Mock + private CustomPojoSerializer mockSerializer; + + @AfterEach + @BeforeEach + void setUp() throws Exception { + resetStaticFields(); + } + + private void resetStaticFields() throws Exception { + Field serializerField = PojoSerializerLoader.class.getDeclaredField("customPojoSerializer"); + serializerField.setAccessible(true); + serializerField.set(null, null); + + Field initializedField = PojoSerializerLoader.class.getDeclaredField("initialized"); + initializedField.setAccessible(true); + initializedField.set(null, false); + } + + + private void setMockSerializer(CustomPojoSerializer serializer) throws Exception { + Field serializerField = PojoSerializerLoader.class.getDeclaredField("customPojoSerializer"); + serializerField.setAccessible(true); + serializerField.set(null, serializer); + } + + @Test + void testGetCustomerSerializerNoSerializerAvailable() throws Exception { + PojoSerializer serializer = PojoSerializerLoader.getCustomerSerializer(String.class); + assertNull(serializer); + Field initializedField = PojoSerializerLoader.class.getDeclaredField("initialized"); + initializedField.setAccessible(true); + assert((Boolean) initializedField.get(null)); + } + + @Test + void testGetCustomerSerializerWithValidSerializer() throws Exception { + setMockSerializer(mockSerializer); + String testInput = "test input"; + String testOutput = "test output"; + Type testType = String.class; + when(mockSerializer.fromJson(any(InputStream.class), eq(testType))).thenReturn(testOutput); + when(mockSerializer.fromJson(eq(testInput), eq(testType))).thenReturn(testOutput); + + PojoSerializer serializer = PojoSerializerLoader.getCustomerSerializer(testType); + assertNotNull(serializer); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(testInput.getBytes()); + Object result1 = serializer.fromJson(inputStream); + assertEquals(testOutput, result1); + + Object result2 = serializer.fromJson(testInput); + assertEquals(testOutput, result2); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + serializer.toJson(testInput, outputStream); + verify(mockSerializer).toJson(eq(testInput), any(OutputStream.class), eq(testType)); + } + + @Test + void testGetCustomerSerializerCachingBehavior() throws Exception { + setMockSerializer(mockSerializer); + + Type testType = String.class; + PojoSerializer serializer1 = PojoSerializerLoader.getCustomerSerializer(testType); + PojoSerializer serializer2 = PojoSerializerLoader.getCustomerSerializer(testType); + + assertNotNull(serializer1); + assertNotNull(serializer2); + + String testInput = "test"; + serializer1.fromJson(testInput); + serializer2.fromJson(testInput); + + verify(mockSerializer, times(2)).fromJson(eq(testInput), eq(testType)); + } + + @Test + void testGetCustomerSerializerDifferentTypes() throws Exception { + setMockSerializer(mockSerializer); + + PojoSerializer stringSerializer = PojoSerializerLoader.getCustomerSerializer(String.class); + PojoSerializer integerSerializer = PojoSerializerLoader.getCustomerSerializer(Integer.class); + + assertNotNull(stringSerializer); + assertNotNull(integerSerializer); + + String testString = "test"; + Integer testInt = 123; + + stringSerializer.fromJson(testString); + integerSerializer.fromJson(testInt.toString()); + + verify(mockSerializer).fromJson(eq(testString), eq(String.class)); + verify(mockSerializer).fromJson(eq(testInt.toString()), eq(Integer.class)); + } + + @Test + void testGetCustomerSerializerNullType() throws Exception { + setMockSerializer(mockSerializer); + + PojoSerializer serializer = PojoSerializerLoader.getCustomerSerializer(null); + assertNotNull(serializer); + + String testInput = "test"; + serializer.fromJson(testInput); + verify(mockSerializer).fromJson(eq(testInput), eq(null)); + } + + @Test + void testGetCustomerSerializerExceptionHandling() throws Exception { + setMockSerializer(mockSerializer); + + doThrow(new RuntimeException("Test exception")) + .when(mockSerializer) + .fromJson(any(String.class), any(Type.class)); + + PojoSerializer serializer = PojoSerializerLoader.getCustomerSerializer(String.class); + assertNotNull(serializer); + assertThrows(RuntimeException.class, () -> serializer.fromJson("test")); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundExceptionTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundExceptionTest.java new file mode 100644 index 00000000..38d33f63 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundExceptionTest.java @@ -0,0 +1,59 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import org.junit.jupiter.api.Test; + +import com.amazonaws.services.lambda.runtime.api.client.TooManyServiceProvidersFoundException; + +import static org.junit.jupiter.api.Assertions.*; + +class TooManyServiceProvidersFoundExceptionTest { + + @Test + void testDefaultConstructor() { + TooManyServiceProvidersFoundException exception = new TooManyServiceProvidersFoundException(); + + assertNotNull(exception); + assertNull(exception.getMessage()); + assertNull(exception.getCause()); + } + + @Test + void testMessageConstructor() { + String errorMessage = "Too many service providers found"; + TooManyServiceProvidersFoundException exception = + new TooManyServiceProvidersFoundException(errorMessage); + + assertNotNull(exception); + assertEquals(errorMessage, exception.getMessage()); + assertNull(exception.getCause()); + } + + @Test + void testCauseConstructor() { + Throwable cause = new IllegalStateException("Original error"); + TooManyServiceProvidersFoundException exception = + new TooManyServiceProvidersFoundException(cause); + + assertNotNull(exception); + assertEquals(cause.toString(), exception.getMessage()); + assertSame(cause, exception.getCause()); + } + + @Test + void testMessageAndCauseConstructor() { + String errorMessage = "Too many service providers found"; + Throwable cause = new IllegalStateException("Original error"); + TooManyServiceProvidersFoundException exception = + new TooManyServiceProvidersFoundException(errorMessage, cause); + + assertNotNull(exception); + assertEquals(errorMessage, exception.getMessage()); + assertSame(cause, exception.getCause()); + } + +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/UserFaultTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/UserFaultTest.java index 5a57e6e0..479162ad 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/UserFaultTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/UserFaultTest.java @@ -124,4 +124,42 @@ public void testCircularSuppressedExceptionReference() { assertEquals(expectedStackTrace, stackTrace); } } + + private Exception createExceptionWithStackTrace() { + try { + throw new RuntimeException("Test exception"); + } catch (RuntimeException e) { + return e; + } + } + + @Test + void testMakeInitErrorUserFault() { + String className = "com.example.TestClass"; + Exception testException = createExceptionWithStackTrace(); + + UserFault initFault = UserFault.makeInitErrorUserFault(testException, className); + UserFault notFoundFault = UserFault.makeClassNotFoundUserFault(testException, className); + + assertNotNull(initFault.trace); + assertNotNull(notFoundFault.trace); + + assertFalse(initFault.trace.contains("com.amazonaws.services.lambda.runtime")); + assertFalse(notFoundFault.trace.contains("com.amazonaws.services.lambda.runtime")); + } + + @Test + void testMakeClassNotFoundUserFault() { + String className = "com.example.MissingClass"; + Exception testException = new ClassNotFoundException("Class not found in classpath"); + + UserFault fault = UserFault.makeClassNotFoundUserFault(testException, className); + + assertNotNull(fault); + assertEquals("Class not found: com.example.MissingClass", fault.msg); + assertEquals("java.lang.ClassNotFoundException", fault.exception); + assertNotNull(fault.trace); + assertFalse(fault.fatal); + assertTrue(fault.trace.contains("ClassNotFoundException")); + } } diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java index 19744dd5..f7da7619 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java @@ -18,6 +18,8 @@ public class LambdaContextTest { private static final String INVOKED_FUNCTION_ARN = "invoked-function-arn"; private static final LambdaClientContext CLIENT_CONTEXT = new LambdaClientContext(); public static final int MEMORY_LIMIT = 128; + public static final String TENANT_ID = "tenant-id"; + public static final String X_RAY_TRACE_ID = "x-ray-trace-id"; @Test public void getRemainingTimeInMillis() { @@ -54,6 +56,6 @@ public void getRemainingTimeInMillis_Deadline() throws InterruptedException { private LambdaContext createContextWithDeadline(long deadlineTimeInMs) { return new LambdaContext(MEMORY_LIMIT, deadlineTimeInMs, REQUEST_ID, LOG_GROUP_NAME, LOG_STREAM_NAME, - FUNCTION_NAME, IDENTITY, FUNCTION_VERSION, INVOKED_FUNCTION_ARN, CLIENT_CONTEXT); + FUNCTION_NAME, IDENTITY, FUNCTION_VERSION, INVOKED_FUNCTION_ARN, TENANT_ID, X_RAY_TRACE_ID, CLIENT_CONTEXT); } } diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java index 8630d5fe..91ce9d2a 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java @@ -29,6 +29,27 @@ void testFormattingWithLambdaContext() { null, null, "function-arn", + null, + null, + null + ); + assertFormatsString("test log", LogLevel.WARN, context); + } + + @Test + void testFormattingWithTenantIdInLambdaContext() { + LambdaContext context = new LambdaContext( + 0, + 0, + "request-id", + null, + null, + "function-name", + null, + null, + "function-arn", + "tenant-id", + "xray-trace-id", null ); assertFormatsString("test log", LogLevel.WARN, context); @@ -52,6 +73,7 @@ void assert_expected_log_message(StructuredLogMessage result, String message, Lo if (context != null) { assertEquals(context.getAwsRequestId(), result.AWSRequestId); + assertEquals(context.getTenantId(), result.tenantId); } } } diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java index 1b6f3136..473e2aef 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java @@ -15,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.ErrorRequest; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.StackElement; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayErrorCause; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayException; @@ -312,27 +313,62 @@ public void restoreNextWrongStatusCodeTest() { } @Test - public void nextTest() { + public void nextWithoutTenantIdHeaderTest() { try { - MockResponse mockResponse = new MockResponse(); - mockResponse.setResponseCode(HTTP_ACCEPTED); - mockResponse.setHeader("lambda-runtime-aws-request-id", "1234567890"); - mockResponse.setHeader("Content-Type", "application/json"); + MockResponse mockResponse = buildMockResponseForNextInvocation(); mockWebServer.enqueue(mockResponse); - lambdaRuntimeApiClientImpl.nextInvocation(); - RecordedRequest recordedRequest = mockWebServer.takeRequest(); - HttpUrl actualUrl = recordedRequest.getRequestUrl(); - String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/invocation/next"; - assertEquals(expectedUrl, actualUrl.toString()); + InvocationRequest invocationRequest = lambdaRuntimeApiClientImpl.nextInvocation(); + verifyNextInvocationRequest(); + assertNull(invocationRequest.getTenantId()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void nextWithTenantIdHeaderTest() { + try { + MockResponse mockResponse = buildMockResponseForNextInvocation(); + String expectedTenantId = "my-tenant-id"; + mockResponse.setHeader("lambda-runtime-aws-tenant-id", expectedTenantId); + mockWebServer.enqueue(mockResponse); + + InvocationRequest invocationRequest = lambdaRuntimeApiClientImpl.nextInvocation(); + verifyNextInvocationRequest(); + assertEquals(expectedTenantId, invocationRequest.getTenantId()); - String actualBody = recordedRequest.getBody().readUtf8(); - assertEquals("", actualBody); } catch(Exception e) { fail(); } } + @Test + public void nextWithEmptyTenantIdHeaderTest() { + try { + MockResponse mockResponse = buildMockResponseForNextInvocation(); + mockResponse.setHeader("lambda-runtime-aws-tenant-id", ""); + mockWebServer.enqueue(mockResponse); + + InvocationRequest invocationRequest = lambdaRuntimeApiClientImpl.nextInvocation(); + verifyNextInvocationRequest(); + assertNull(invocationRequest.getTenantId()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void nextWithNullTenantIdHeaderTest() { + try { + MockResponse mockResponse = buildMockResponseForNextInvocation(); + assertThrows(NullPointerException.class, () -> { + mockResponse.setHeader("lambda-runtime-aws-tenant-id", null); + }); + } catch(Exception e) { + fail(); + } + } @Test public void createUrlMalformedTest() { @@ -376,6 +412,24 @@ public void lambdaReportErrorXRayHeaderTooLongTest() { } } + private MockResponse buildMockResponseForNextInvocation() { + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockResponse.setHeader("lambda-runtime-aws-request-id", "1234567890"); + mockResponse.setHeader("Content-Type", "application/json"); + return mockResponse; + } + + private void verifyNextInvocationRequest() throws Exception { + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/invocation/next"; + assertEquals(expectedUrl, actualUrl.toString()); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("", actualBody); + } + private String getHostnamePort() { return mockWebServer.getHostName() + ":" + mockWebServer.getPort(); } diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverterTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverterTest.java new file mode 100644 index 00000000..f94bc0c5 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverterTest.java @@ -0,0 +1,112 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi.converters; + +import com.amazonaws.services.lambda.runtime.api.client.UserFault; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.ErrorRequest; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class LambdaErrorConverterTest { + + @Test + void testFromUserFaultWithMessageAndException() { + UserFault userFault = new UserFault("Test error message", "TestException", "Test stack trace"); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertEquals("Test error message", errorRequest.errorMessage); + assertEquals("TestException", errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromUserFaultWithNullValues() { + UserFault userFault = new UserFault(null, null, null); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertNull(errorRequest.errorMessage); + assertNull(errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromUserFaultWithFatalError() { + UserFault userFault = new UserFault("Fatal error", "FatalException", "Test stack trace", true); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertEquals("Fatal error", errorRequest.errorMessage); + assertEquals("FatalException", errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromUserFaultCreatedFromException() { + Exception exception = new RuntimeException("Test exception message"); + UserFault userFault = UserFault.makeUserFault(exception); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertEquals("Test exception message", errorRequest.errorMessage); + assertEquals("java.lang.RuntimeException", errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromUserFaultCreatedFromMessage() { + UserFault userFault = UserFault.makeUserFault("Simple message"); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertEquals("Simple message", errorRequest.errorMessage); + assertNull(errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromThrowableWithMessage() { + Exception exception = new RuntimeException("Test exception message"); + ErrorRequest errorRequest = LambdaErrorConverter.fromThrowable(exception); + + assertNotNull(errorRequest); + assertEquals("Test exception message", errorRequest.errorMessage); + assertEquals("java.lang.RuntimeException", errorRequest.errorType); + assertNotNull(errorRequest.stackTrace); + assertTrue(errorRequest.stackTrace.length > 0); + } + + @Test + void testFromThrowableWithNullMessage() { + Exception exception = new RuntimeException(); + ErrorRequest errorRequest = LambdaErrorConverter.fromThrowable(exception); + + assertNotNull(errorRequest); + assertEquals("java.lang.RuntimeException", errorRequest.errorMessage); + assertEquals("java.lang.RuntimeException", errorRequest.errorType); + assertNotNull(errorRequest.stackTrace); + assertTrue(errorRequest.stackTrace.length > 0); + } + + @Test + void testFromThrowableStackTraceContent() { + Exception exception = new RuntimeException("Test message"); + ErrorRequest errorRequest = LambdaErrorConverter.fromThrowable(exception); + + String[] stackTrace = errorRequest.stackTrace; + assertNotNull(stackTrace); + assertTrue(stackTrace.length > 0); + + boolean foundTestClass = false; + for (String traceLine : stackTrace) { + if (traceLine.contains(LambdaErrorConverterTest.class.getSimpleName())) { + foundTestClass = true; + break; + } + } + assertTrue(foundTestClass); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStreamTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStreamTest.java new file mode 100644 index 00000000..30146ea8 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStreamTest.java @@ -0,0 +1,81 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.io.OutputStream; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +public class LambdaOutputStreamTest { + + @Mock + private OutputStream mockInnerStream; + + private LambdaOutputStream lambdaOutputStream; + + @BeforeEach + void setUp() { + lambdaOutputStream = new LambdaOutputStream(mockInnerStream); + } + + @Test + void writeSingleByte() throws IOException { + int testByte = 65; // 'A' + lambdaOutputStream.write(testByte); + verify(mockInnerStream).write(new byte[]{(byte) testByte}, 0, 1); + } + + @Test + void writeByteArray() throws IOException { + byte[] testBytes = "test".getBytes(); + lambdaOutputStream.write(testBytes); + verify(mockInnerStream).write(testBytes, 0, testBytes.length); + } + + @Test + void writeOffsetLength() throws IOException { + byte[] testBytes = "test".getBytes(); + int offset = 1; + int length = 2; + lambdaOutputStream.write(testBytes, offset, length); + verify(mockInnerStream).write(testBytes, offset, length); + } + + @Test + void throwWriteSingleByte() throws IOException { + doThrow(new IOException("Test exception")) + .when(mockInnerStream) + .write(any(byte[].class), anyInt(), anyInt()); + assertThrows(IOException.class, () -> lambdaOutputStream.write(65)); + } + + @Test + void throwWriteByteArray() throws IOException { + byte[] testBytes = "test".getBytes(); + doThrow(new IOException("Test exception")) + .when(mockInnerStream) + .write(any(byte[].class), anyInt(), anyInt()); + assertThrows(IOException.class, () -> lambdaOutputStream.write(testBytes)); + } + + @Test + void throwWriteOffsetLength() throws IOException { + byte[] testBytes = "test".getBytes(); + doThrow(new IOException("Test exception")) + .when(mockInnerStream) + .write(any(byte[].class), anyInt(), anyInt()); + assertThrows(IOException.class, () -> lambdaOutputStream.write(testBytes, 1, 2)); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtilTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtilTest.java new file mode 100644 index 00000000..b1f0592f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtilTest.java @@ -0,0 +1,56 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.util; + +import org.junit.jupiter.api.Test; +import java.lang.reflect.Field; +import static org.junit.jupiter.api.Assertions.*; + +public class UnsafeUtilTest { + + @Test + void testTheUnsafeIsInitialized() { + assertNotNull(UnsafeUtil.TheUnsafe); + } + + @Test + void testThrowException() { + Exception testException = new Exception("Test exception"); + + try { + UnsafeUtil.throwException(testException); + fail("Should have thrown an exception"); + } catch (Throwable e) { + assertEquals("Test exception", e.getMessage()); + assertSame(testException, e); + } + } + + @Test + void testDisableIllegalAccessWarning() { + assertDoesNotThrow(() -> UnsafeUtil.disableIllegalAccessWarning()); + try { + Class illegalAccessLoggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger"); + Field loggerField = illegalAccessLoggerClass.getDeclaredField("logger"); + loggerField.setAccessible(true); + Object loggerValue = loggerField.get(null); + assertNull(loggerValue); + } catch (ClassNotFoundException e) { + assertTrue(true); + } catch (NoSuchFieldException e) { + assertTrue(true); + } catch (Exception e) { + fail("Unexpected exception: " + e.getMessage()); + } + } + + @Test + void testPrivateConstructor() { + assertThrows(IllegalAccessException.class, () -> { + UnsafeUtil.class.getDeclaredConstructor().newInstance(); + }); + } +} diff --git a/aws-lambda-java-runtime-interface-client/test-handlers/EchoHandler.java b/aws-lambda-java-runtime-interface-client/test-handlers/EchoHandler.java new file mode 100644 index 00000000..cb324e7f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test-handlers/EchoHandler.java @@ -0,0 +1,20 @@ +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import java.util.Map; +import java.util.HashMap; + +public class EchoHandler implements RequestHandler, Map> { + + @Override + public Map handleRequest(Map event, Context context) { + context.getLogger().log("Processing event: " + event); + + Map response = new HashMap<>(event); + response.put("timestamp", System.currentTimeMillis()); + response.put("requestId", context.getAwsRequestId()); + response.put("functionName", context.getFunctionName()); + response.put("remainingTimeInMillis", context.getRemainingTimeInMillis()); + + return response; + } +} \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml index cdc27a65..2a71cb1b 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml @@ -43,6 +43,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install -DargLineForReflectionTestOnly="") - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml index 67dd7617..db8bf2ba 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml @@ -42,6 +42,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install -DargLineForReflectionTestOnly="") - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml index 04c486a8..e3773cf8 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml @@ -37,6 +37,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install -DmultiArch=false -DargLineForReflectionTestOnly="") - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml index 8222bb41..a9836fc6 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml @@ -41,6 +41,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install -DargLineForReflectionTestOnly="") - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.centos.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.centos.yml index d718c264..74d12b01 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.centos.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.centos.yml @@ -41,6 +41,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install) - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml index d2772fbf..222d14a3 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml @@ -42,6 +42,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install) - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml index 2a90017b..ce153c54 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml @@ -44,6 +44,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install) - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml b/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml index 2e240fe3..46d191a7 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml +++ b/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml @@ -15,7 +15,7 @@ com.amazonaws aws-lambda-java-runtime-interface-client - 2.6.0 + 2.8.4 @@ -50,4 +50,3 @@ - diff --git a/aws-lambda-java-serialization/pom.xml b/aws-lambda-java-serialization/pom.xml index 07ccecc8..2f8e7613 100644 --- a/aws-lambda-java-serialization/pom.xml +++ b/aws-lambda-java-serialization/pom.xml @@ -4,7 +4,7 @@ com.amazonaws aws-lambda-java-serialization - 1.1.5 + 1.1.6 jar AWS Lambda Java Runtime Serialization diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializers.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializers.java index 4173211e..3b10b198 100644 --- a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializers.java +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializers.java @@ -118,6 +118,7 @@ public class LambdaEventSerializers { ConnectEventMixin.ContactDataMixin.class), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$CustomerEndpoint", ConnectEventMixin.CustomerEndpointMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Queue", ConnectEventMixin.QueueMixin.class), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$SystemEndpoint", ConnectEventMixin.SystemEndpointMixin.class), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbEvent", @@ -170,6 +171,7 @@ public class LambdaEventSerializers { new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Details"), new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$ContactData"), new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$CustomerEndpoint"), + new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Queue"), new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$SystemEndpoint"))), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbEvent", Arrays.asList( @@ -214,7 +216,10 @@ public class LambdaEventSerializers { */ private static final Map NAMING_STRATEGY_MAP = Stream.of( new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.SNSEvent", - new PropertyNamingStrategy.PascalCaseStrategy())) + new PropertyNamingStrategy.PascalCaseStrategy()), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Queue", + new PropertyNamingStrategy.PascalCaseStrategy()) + ) .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)); /** diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ConnectEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ConnectEventMixin.java index 529a33b3..1645fdae 100644 --- a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ConnectEventMixin.java +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ConnectEventMixin.java @@ -65,8 +65,8 @@ public abstract class ContactDataMixin { @JsonProperty("PreviousContactId") abstract void setPreviousContactId(String previousContactId); // needed because Jackson expects "queue" instead of "Queue" - @JsonProperty("Queue") abstract String getQueue(); - @JsonProperty("Queue") abstract void setQueue(String queue); + @JsonProperty("Queue") abstract Map getQueue(); + @JsonProperty("Queue") abstract void setQueue(Map queue); // needed because Jackson expects "systemEndpoint" instead of "SystemEndpoint" @JsonProperty("SystemEndpoint") abstract Map getSystemEndpoint(); @@ -95,4 +95,9 @@ public abstract class SystemEndpointMixin { @JsonProperty("Type") abstract String getType(); @JsonProperty("Type") abstract void setType(String type); } + + public abstract class QueueMixin { + @JsonProperty("Name") abstract String getName(); + @JsonProperty("Name") abstract void setName(String name); + } } diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SecretsManagerRotationEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SecretsManagerRotationEventMixin.java index ab94be20..1b862e8c 100644 --- a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SecretsManagerRotationEventMixin.java +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SecretsManagerRotationEventMixin.java @@ -21,4 +21,8 @@ public abstract class SecretsManagerRotationEventMixin { // needed because Jackson expects "clientRequestToken" instead of "ClientRequestToken" @JsonProperty("ClientRequestToken") abstract String getClientRequestToken(); @JsonProperty("ClientRequestToken") abstract void setClientRequestToken(String clientRequestToken); + + // needed because Jackson expects "rotationToken" instead of "RotationToken" + @JsonProperty("RotationToken") abstract String getRotationToken(); + @JsonProperty("RotationToken") abstract void setRotationToken(String rotationToken); } diff --git a/aws-lambda-java-tests/pom.xml b/aws-lambda-java-tests/pom.xml index eb1f95b0..da07133c 100644 --- a/aws-lambda-java-tests/pom.xml +++ b/aws-lambda-java-tests/pom.xml @@ -4,7 +4,7 @@ com.amazonaws aws-lambda-java-tests - 1.1.1 + 1.1.2 jar AWS Lambda Java Tests @@ -40,12 +40,12 @@ com.amazonaws aws-lambda-java-serialization - 1.1.5 + 1.1.6 com.amazonaws aws-lambda-java-events - 3.14.0 + 3.16.1 org.junit.jupiter @@ -65,7 +65,7 @@ org.apache.commons commons-lang3 - 3.12.0 + 3.18.0 diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventLoaderTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventLoaderTest.java index 4aa920f8..752b84e2 100644 --- a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventLoaderTest.java +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventLoaderTest.java @@ -333,6 +333,14 @@ public void testLoadConnectEvent() { assertThat(contactData.getSystemEndpoint()) .returns("+21234567890",from(ConnectEvent.SystemEndpoint::getAddress)) .returns("TELEPHONE_NUMBER",from(ConnectEvent.SystemEndpoint::getType)); + + assertThat(contactData.getQueue()) + .isNotNull() + .returns("SampleQueue", from(ConnectEvent.Queue::getName)) + .returns("arn:aws:connect:eu-central-1:123456789012:instance/9308c2a1-9bc6-4cea-8290-6c0b4a6d38fa", + from(ConnectEvent.Queue::getARN) + ); + } @Test @@ -369,7 +377,9 @@ public void testLoadSecretsManagerRotationEvent() { assertThat(event) .returns("123e4567-e89b-12d3-a456-426614174000", from(SecretsManagerRotationEvent::getClientRequestToken)) .returns("arn:aws:secretsmanager:eu-central-1:123456789012:secret:/powertools/secretparam-xBPaJ5", from(SecretsManagerRotationEvent::getSecretId)) - .returns("CreateSecret", from(SecretsManagerRotationEvent::getStep)); + .returns("CreateSecret", from(SecretsManagerRotationEvent::getStep)) + .returns("8a4cc1ac-82ea-47c7-bd9f-aeb370b1b6a6", from(SecretsManagerRotationEvent::getRotationToken)); +; } @Test @@ -418,6 +428,17 @@ public void testLoadCognitoUserPoolPreTokenGenerationEventV2() { CognitoUserPoolPreTokenGenerationEventV2.Request request = event.getRequest(); String[] requestScopes = request.getScopes(); assertThat("aws.cognito.signin.user.admin").isEqualTo(requestScopes[0]); + + CognitoUserPoolPreTokenGenerationEventV2.Response response = event.getResponse(); + String[] groupsToOverride = response.getClaimsAndScopeOverrideDetails().getGroupOverrideDetails().getGroupsToOverride(); + String[] iamRolesToOverride = response.getClaimsAndScopeOverrideDetails().getGroupOverrideDetails().getIamRolesToOverride(); + String preferredRole = response.getClaimsAndScopeOverrideDetails().getGroupOverrideDetails().getPreferredRole(); + + assertThat("group-99").isEqualTo(groupsToOverride[0]); + assertThat("group-98").isEqualTo(groupsToOverride[1]); + assertThat("arn:aws:iam::123456789012:role/sns_caller99").isEqualTo(iamRolesToOverride[0]); + assertThat("arn:aws:iam::123456789012:role/sns_caller98").isEqualTo(iamRolesToOverride[1]); + assertThat("arn:aws:iam::123456789012:role/sns_caller_99").isEqualTo(preferredRole); } @Test diff --git a/aws-lambda-java-tests/src/test/resources/cognito_user_pool_pre_token_generation_event_v2.json b/aws-lambda-java-tests/src/test/resources/cognito_user_pool_pre_token_generation_event_v2.json index 43f8e0f7..eb46b8cb 100644 --- a/aws-lambda-java-tests/src/test/resources/cognito_user_pool_pre_token_generation_event_v2.json +++ b/aws-lambda-java-tests/src/test/resources/cognito_user_pool_pre_token_generation_event_v2.json @@ -21,13 +21,19 @@ "groupConfiguration": { "groupsToOverride": ["group-1", "group-2", "group-3"], "iamRolesToOverride": ["arn:aws:iam::123456789012:role/sns_caller1", "arn:aws:iam::123456789012:role/sns_caller2", "arn:aws:iam::123456789012:role/sns_caller3"], - "preferredRole": ["arn:aws:iam::123456789012:role/sns_caller"] + "preferredRole": "arn:aws:iam::123456789012:role/sns_caller" }, "scopes": [ "aws.cognito.signin.user.admin", "openid", "email", "phone" ] }, "response": { - "claimsAndScopeOverrideDetails": [] + "claimsAndScopeOverrideDetails": { + "groupOverrideDetails": { + "groupsToOverride": ["group-99", "group-98"], + "iamRolesToOverride": ["arn:aws:iam::123456789012:role/sns_caller99", "arn:aws:iam::123456789012:role/sns_caller98"], + "preferredRole": "arn:aws:iam::123456789012:role/sns_caller_99" + } + } } } \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/connect_event.json b/aws-lambda-java-tests/src/test/resources/connect_event.json index a9e04f7f..b71bf669 100644 --- a/aws-lambda-java-tests/src/test/resources/connect_event.json +++ b/aws-lambda-java-tests/src/test/resources/connect_event.json @@ -22,7 +22,10 @@ } }, "PreviousContactId": "4ca32fbd-8f92-46af-92a5-6b0f970f0efe", - "Queue": null, + "Queue": { + "Name": "SampleQueue", + "ARN": "arn:aws:connect:eu-central-1:123456789012:instance/9308c2a1-9bc6-4cea-8290-6c0b4a6d38fa" + }, "SystemEndpoint": { "Address": "+21234567890", "Type": "TELEPHONE_NUMBER" diff --git a/aws-lambda-java-tests/src/test/resources/secrets_rotation_event.json b/aws-lambda-java-tests/src/test/resources/secrets_rotation_event.json index 38440fac..e8d80b57 100644 --- a/aws-lambda-java-tests/src/test/resources/secrets_rotation_event.json +++ b/aws-lambda-java-tests/src/test/resources/secrets_rotation_event.json @@ -1,5 +1,6 @@ { "Step" : "CreateSecret", "SecretId" : "arn:aws:secretsmanager:eu-central-1:123456789012:secret:/powertools/secretparam-xBPaJ5", - "ClientRequestToken" : "123e4567-e89b-12d3-a456-426614174000" + "ClientRequestToken" : "123e4567-e89b-12d3-a456-426614174000", + "RotationToken": "8a4cc1ac-82ea-47c7-bd9f-aeb370b1b6a6" } \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/.gitignore b/experimental/aws-lambda-java-profiler/.gitignore new file mode 100644 index 00000000..4c3fb86d --- /dev/null +++ b/experimental/aws-lambda-java-profiler/.gitignore @@ -0,0 +1,3 @@ +*.zip +/.idea/ +/target/ diff --git a/experimental/aws-lambda-java-profiler/.mvn/wrapper/maven-wrapper.properties b/experimental/aws-lambda-java-profiler/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..48a56c99 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip diff --git a/experimental/aws-lambda-java-profiler/README.md b/experimental/aws-lambda-java-profiler/README.md new file mode 100644 index 00000000..ccc66399 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/README.md @@ -0,0 +1,114 @@ +

+ AWS Lambda service icon +

+ +

AWS Lambda Profiler Extension for Java

+ +The Lambda profiler extension allows you to profile your Java functions invoke by invoke, with high fidelity, and no +code changes. It uses the [async-profiler](https://github.com/async-profiler/async-profiler) project to produce +profiling data and automatically uploads the data as HTML flame graphs to S3. + +

+ A flame graph of a Java Lambda function +

+ +## Current status +**This is an alpha release and not yet ready for production use.** We're especially interested in early feedback on usability, features, performance, and compatibility. Please send feedback by opening a [GitHub issue](https://github.com/aws/aws-lambda-java-libs/issues/new). + +The profiler has been tested with Lambda managed runtimes for Java 17 and Java 21. + +## How to use the Lambda Profiler + +To use the profiler you need to + +1. Build the extension in this repo +2. Deploy it as a Lambda Layer and attach the layer to your function +3. Create an S3 bucket for the results, or reuse an existing one +4. Give your function permission to write to the bucket +5. Configure the required environment variables. + +The above assumes you're using the ZIP deployment method with managed runtimes. If you deploy your functions as container images instead, you will need to include the profiler in your Dockerfile at `/opt/extensions/` rather than using a Lambda layer. + +### Quick Start + +The following [Quick Start](#quick-start) gives AWS CLI commands you can run to get started (MacOS/Linux). There are also [examples](examples) using infrastructure as code for you to refer to. + +1. Clone the repo + + ```bash + git clone https://github.com/aws/aws-lambda-java-libs + ``` + +2. Build the extension + + ```bash + cd aws-lambda-java-libs/experimental/aws-lambda-java-profiler/extension + ./build_layer.sh + ``` + +3. Run the `update-function.sh` script which will create a new S3 bucket, Lambda layer and all the configuration required. + + ```bash + cd .. + ./update-function.sh YOUR_FUNCTION_NAME + ``` + +4. Invoke your function and review the flame graph in S3 using your browser. + +### Configuration + +#### Required Environment Variables + +| Name | Value | +|-----------------------------------------|-----------------------------------------------------------------------------------------------| +| AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME | Your unique bucket name | +| JAVA_TOOL_OPTIONS | -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler-extension.jar | + +#### Optional Environment Variables + +| Name | Default Value | Options | +|------------------------------------------|-----------------------------------------------------------|--------------------------------| +| AWS_LAMBDA_PROFILER_START_COMMAND | start,event=wall,interval=1us | | +| AWS_LAMBDA_PROFILER_STOP_COMMAND | stop,file=%s,include=*AWSLambda.main,include=start_thread | file=%s is required | +| AWS_LAMBDA_PROFILER_DEBUG | false | true - to enable debug logging | +| AWS_LAMBDA_PROFILER_COMMUNICATION_PORT | 1234 | a valid port number | + +### How does it work? + +In `/src` is the code for a Java agent. It's entry point `AgentEntry.premain()` is executed as the runtime starts up. +The environment variable `JAVA_TOOL_OPTIONS` is used to specify which `.jar` file the agent is in. The `MANIFEST.MF` file is used to specify the pre-main class. + +When the agent is constructed, it starts the profiler and registers itself as a Lambda extension for `INVOKE` request. + +A new thread is created to handle calling `/next` and uploading the results of the profiler to S3. The bucket to upload +the result to is configurable using an environment variable. + +### Troubleshooting + +- Ensure the Lambda function execution role has the necessary permissions to write to the S3 bucket. +- Verify that the environment variables are set correctly in your Lambda function configuration. +- Check CloudWatch logs for any error messages from the extension. +- The profiler extension uses dependencies such as `com.amazonaws:aws-lambda-java-core`, `com.amazonaws:aws-lambda-java-events` and `software.amazon.awssdk:s3`. If you're using the same dependencies in your Lambda function, make sure that the versions match those used by the extension as mismatched versions can lead to compatibility issues. + +## Contributing + +Contributions to improve the Java profiler extension are welcome. Please see [CONTRIBUTING.md](../../CONTRIBUTING.md) for more information on how to report bugs or submit pull requests. + +Issues or contributions to the [async-profiler](https://github.com/async-profiler/async-profiler) itself should be submitted to that project. + +### Security + +If you discover a potential security issue in this project we ask that you notify AWS Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public GitHub issue. + +### Code of conduct + +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). See [CODE_OF_CONDUCT.md](doc/CODE_OF_CONDUCT.md) for more details. + +## License + +This project is licensed under the [Apache 2.0](../../LICENSE) License. It uses the following projects: + +- [async-profiler](https://github.com/async-profiler/async-profiler) (Apache 2.0 license) +- [AWS SDK for Java 2.0](https://github.com/aws/aws-sdk-java-v2) (Apache 2.0 license) +- Other libraries in this repository (Apache 2.0 license) + diff --git a/experimental/aws-lambda-java-profiler/RELEASE.CHANGELOG.md b/experimental/aws-lambda-java-profiler/RELEASE.CHANGELOG.md new file mode 100644 index 00000000..f2f14ae4 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/RELEASE.CHANGELOG.md @@ -0,0 +1,7 @@ +### March 31, 2025 +`0.1.1` [link to tag](https://github.com/aws/aws-lambda-java-libs/releases/tag/profiler-extension-0.1.1) +- fix: use PROFILER_STOP_COMMAND in Shutdown hooks ([#537](https://github.com/aws/aws-lambda-java-libs/pull/537)) + +### March 18, 2025 +`0.1.0` [link to tag](https://github.com/aws/aws-lambda-java-libs/releases/tag/profiler-extension-0.1.0) +- Initial release \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/docs/Arch_AWS-Lambda_64.svg b/experimental/aws-lambda-java-profiler/docs/Arch_AWS-Lambda_64.svg new file mode 100644 index 00000000..496ef0e7 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/docs/Arch_AWS-Lambda_64.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Lambda_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph-small.png b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph-small.png new file mode 100644 index 00000000..81ae8cba Binary files /dev/null and b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph-small.png differ diff --git a/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph.png b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph.png new file mode 100644 index 00000000..26d11c31 Binary files /dev/null and b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph.png differ diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/.gitignore b/experimental/aws-lambda-java-profiler/examples/cdk/.gitignore new file mode 100644 index 00000000..1db21f16 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/.gitignore @@ -0,0 +1,13 @@ +.classpath.txt +target +.classpath +.project +.idea +.settings +.vscode +*.iml + +# CDK asset staging directory +.cdk.staging +cdk.out + diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/README.md b/experimental/aws-lambda-java-profiler/examples/cdk/README.md new file mode 100644 index 00000000..516ef71a --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/README.md @@ -0,0 +1,18 @@ +# Welcome to your CDK Java project! + +This is a blank project for CDK development with Java. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +It is a [Maven](https://maven.apache.org/) based project, so you can open this project with any Maven compatible Java IDE to build and run tests. + +## Useful commands + + * `mvn package` compile and run tests + * `cdk ls` list all stacks in the app + * `cdk synth` emits the synthesized CloudFormation template + * `cdk deploy` deploy this stack to your default AWS account/region + * `cdk diff` compare deployed stack with current state + * `cdk docs` open CDK documentation + +Enjoy! diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/cdk.json b/experimental/aws-lambda-java-profiler/examples/cdk/cdk.json new file mode 100644 index 00000000..e94ff851 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/cdk.json @@ -0,0 +1,68 @@ +{ + "app": "mvn -e -q compile exec:java", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "target", + "pom.xml", + "src/test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false + } +} diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml b/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml new file mode 100644 index 00000000..01bbf0d6 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + com.myorg + example-cdk-profiler-layer + 0.1 + + + UTF-8 + 2.155.0 + [10.0.0,11.0.0) + 5.7.1 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + com.myorg.InfraApp + + + + + + + + software.amazon.awscdk + aws-cdk-lib + ${cdk.version} + + + + software.constructs + constructs + ${constructs.version} + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraApp.java b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraApp.java new file mode 100644 index 00000000..1232c1b8 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraApp.java @@ -0,0 +1,42 @@ +package com.myorg; + +import software.amazon.awscdk.App; +import software.amazon.awscdk.Environment; +import software.amazon.awscdk.StackProps; + +import java.util.Arrays; + +public class InfraApp { + public static void main(final String[] args) { + App app = new App(); + + new InfraStack(app, "InfraStack", StackProps.builder() + // If you don't specify 'env', this stack will be environment-agnostic. + // Account/Region-dependent features and context lookups will not work, + // but a single synthesized template can be deployed anywhere. + + // Uncomment the next block to specialize this stack for the AWS Account + // and Region that are implied by the current CLI configuration. + /* + .env(Environment.builder() + .account(System.getenv("CDK_DEFAULT_ACCOUNT")) + .region(System.getenv("CDK_DEFAULT_REGION")) + .build()) + */ + + // Uncomment the next block if you know exactly what Account and Region you + // want to deploy the stack to. + /* + .env(Environment.builder() + .account("123456789012") + .region("us-east-1") + .build()) + */ + + // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html + .build()); + + app.synth(); + } +} + diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraStack.java b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraStack.java new file mode 100644 index 00000000..79773e39 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraStack.java @@ -0,0 +1,53 @@ +package com.myorg; + +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.lambda.Code; +import software.amazon.awscdk.services.lambda.Function; +import software.amazon.awscdk.services.lambda.LayerVersion; +import software.amazon.awscdk.services.s3.Bucket; +import software.constructs.Construct; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static software.amazon.awscdk.services.lambda.Architecture.*; +import static software.amazon.awscdk.services.lambda.Runtime.*; + +public class InfraStack extends Stack { + public InfraStack(final Construct scope, final String id) { + this(scope, id, null); + } + + public InfraStack(final Construct scope, final String id, final StackProps props) { + super(scope, id, props); + + var resultsBucketName = UUID.randomUUID().toString(); + var resultsBucket = Bucket.Builder.create(this, "profiler-results-bucket") + .bucketName(resultsBucketName) + .build(); + + var layerVersion = LayerVersion.Builder.create(this, "async-profiler-layer") + .compatibleArchitectures(List.of(ARM_64, X86_64)) + .compatibleRuntimes(List.of(JAVA_11, JAVA_17, JAVA_21)) + .code(Code.fromAsset("../../target/extension.zip")) + .build(); + + var environmentVariables = Map.of("JAVA_TOOL_OPTIONS", "-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler.jar", + "AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME", resultsBucketName); + + var function = Function.Builder.create(this, "example-profiler-function") + .runtime(JAVA_21) + .handler("helloworld.App") + .code(Code.fromAsset("../function/profiling-example/target/Helloworld-1.0.jar")) + .memorySize(2048) + .layers(List.of(layerVersion)) + .environment(environmentVariables) + .timeout(Duration.seconds(30)) + .build(); + + resultsBucket.grantPut(function); + } +} diff --git a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml new file mode 100644 index 00000000..c7465bfd --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml @@ -0,0 +1,63 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 21 + 21 + + + + + com.amazonaws + aws-lambda-java-core + 1.2.2 + + + com.amazonaws + aws-lambda-java-events + 3.11.0 + + + com.hkupty.penna + penna-core + 0.8.0 + + + org.slf4j + slf4j-api + 2.0.13 + + + + junit + junit + 4.13.2 + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + + + package + + shade + + + + + + + diff --git a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/main/java/helloworld/App.java b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/main/java/helloworld/App.java new file mode 100644 index 00000000..c58f55a1 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/main/java/helloworld/App.java @@ -0,0 +1,53 @@ +package helloworld; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + + private static Logger logger = LoggerFactory.getLogger(App.class); + + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + headers.put("X-Custom-Header", "application/json"); + + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() + .withHeaders(headers); + try { + final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); + String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); + logger.info(output); + + return response + .withStatusCode(200) + .withBody(output); + } catch (IOException e) { + return response + .withBody("{}") + .withStatusCode(500); + } + } + + private String getPageContents(String address) throws IOException{ + URL url = new URL(address); + try(BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { + return br.lines().collect(Collectors.joining(System.lineSeparator())); + } + } +} diff --git a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/test/java/helloworld/AppTest.java b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/test/java/helloworld/AppTest.java new file mode 100644 index 00000000..240323bb --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/test/java/helloworld/AppTest.java @@ -0,0 +1,22 @@ +package helloworld; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class AppTest { + @Test + public void successfulResponse() { + App app = new App(); + APIGatewayProxyResponseEvent result = app.handleRequest(null, null); + assertEquals(200, result.getStatusCode().intValue()); + assertEquals("application/json", result.getHeaders().get("Content-Type")); + String content = result.getBody(); + assertNotNull(content); + assertTrue(content.contains("\"message\"")); + assertTrue(content.contains("\"hello world\"")); + assertTrue(content.contains("\"location\"")); + } +} diff --git a/experimental/aws-lambda-java-profiler/extension/build.gradle b/experimental/aws-lambda-java-profiler/extension/build.gradle new file mode 100644 index 00000000..387bb352 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id "com.gradleup.shadow" version "8.3.3" +} + +repositories { + mavenCentral() +} + +sourceCompatibility = 11 +targetCompatibility = 11 + +dependencies { + implementation 'com.amazonaws:aws-lambda-java-core:1.2.3' + implementation 'com.amazonaws:aws-lambda-java-events:3.11.5' + implementation("tools.profiler:async-profiler:3.0") + implementation("software.amazon.awssdk:s3:2.31.2") { + exclude group: 'software.amazon.awssdk', module: 'netty-nio-client' + } +} + +jar { + manifest { + attributes 'Main-Class': 'com.amazonaws.services.lambda.extension.ExtensionMain' + attributes 'Premain-Class': 'com.amazonaws.services.lambda.extension.PreMain' + attributes 'Can-Redefine-Class': true + } +} + +shadowJar { + archiveFileName = "profiler-extension.jar" +} + +build.dependsOn jar diff --git a/experimental/aws-lambda-java-profiler/extension/build_layer.sh b/experimental/aws-lambda-java-profiler/extension/build_layer.sh new file mode 100755 index 00000000..cfb381cf --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/build_layer.sh @@ -0,0 +1,13 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +./gradlew :shadowJar + +chmod +x extensions/profiler-extension +archive="extension.zip" +if [ -f "$archive" ] ; then + rm "$archive" +fi + +zip "$archive" -j build/libs/profiler-extension.jar +zip "$archive" extensions/* \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/extension/extensions/profiler-extension b/experimental/aws-lambda-java-profiler/extension/extensions/profiler-extension new file mode 100755 index 00000000..ef9a5e47 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/extensions/profiler-extension @@ -0,0 +1,6 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +set -euo pipefail +exec -- java -jar /opt/profiler-extension.jar \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.jar b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..a4b76b95 Binary files /dev/null and b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.jar differ diff --git a/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.properties b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..df97d72b --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/experimental/aws-lambda-java-profiler/extension/gradlew b/experimental/aws-lambda-java-profiler/extension/gradlew new file mode 100755 index 00000000..f5feea6d --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/experimental/aws-lambda-java-profiler/extension/gradlew.bat b/experimental/aws-lambda-java-profiler/extension/gradlew.bat new file mode 100644 index 00000000..9b42019c --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionClient.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionClient.java new file mode 100644 index 00000000..60c13a81 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionClient.java @@ -0,0 +1,73 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Optional; + +/** + * Utility class that takes care of registration of extension, fetching the next event, initializing + * and exiting with error + */ +public class ExtensionClient { + private static final String EXTENSION_NAME = "profiler-extension"; + private static final String BASEURL = String + .format("http://%s/2020-01-01/extension", System.getenv("AWS_LAMBDA_RUNTIME_API")); + private static final String BODY = "{" + + " \"events\": [" + + " \"INVOKE\"," + + " \"SHUTDOWN\"" + + " ]" + + " }"; + private static final String LAMBDA_EXTENSION_IDENTIFIER = "Lambda-Extension-Identifier"; + private static final HttpClient client = HttpClient.newBuilder().build(); + + public static String registerExtension() { + final String registerUrl = String.format("%s/register", BASEURL); + HttpRequest request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(BODY)) + .header("Content-Type", "application/json") + .header("Lambda-Extension-Name", EXTENSION_NAME) + .uri(URI.create(registerUrl)) + .build(); + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + // Get extension ID from the response headers + Optional lambdaExtensionHeader = response.headers().firstValue("lambda-extension-identifier"); + if (lambdaExtensionHeader.isPresent()) { + return lambdaExtensionHeader.get(); + } + } + catch (Exception e) { + Logger.error("could not register the extension"); + e.printStackTrace(); + } + throw new RuntimeException("Error while registering extension"); + } + + public static String getNext(final String extensionId) { + try { + final String nextEventUrl = String.format("%s/event/next", BASEURL); + HttpRequest request = HttpRequest.newBuilder() + .GET() + .header(LAMBDA_EXTENSION_IDENTIFIER, extensionId) + .uri(URI.create(nextEventUrl)) + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + return response.body(); + } else { + Logger.error("invalid status code returned while processing event = " + response.statusCode()); + } + } + catch (Exception e) { + Logger.error("could not get /next event"); + e.printStackTrace(); + } + + return null; + } +} diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionMain.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionMain.java new file mode 100644 index 00000000..18115a9f --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionMain.java @@ -0,0 +1,136 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.URI; +import java.util.UUID; + +public class ExtensionMain { + + private static final HttpClient client = HttpClient.newBuilder().build(); + private static String previousFileSuffix = null; + private static boolean coldstart = true; + private static final String REQUEST_ID = "requestId"; + private static final String EVENT_TYPE = "eventType"; + private static final String INTERNAL_COMMUNICATION_PORT = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_COMMUNICATION_PORT", "1234"); + public static final String HEADER_NAME = "X-FileName"; + + private static S3Manager s3Manager; + + public static void main(String[] args) { + final String extension = ExtensionClient.registerExtension(); + Logger.debug("Extension registration complete, extensionID: " + extension); + s3Manager = new S3Manager(); + while (true) { + try { + String response = ExtensionClient.getNext(extension); + if (response != null && !response.isEmpty()) { + final String eventType = extractInfo(EVENT_TYPE, response); + Logger.debug("eventType = " + eventType); + if (eventType != null) { + switch (eventType) { + case "INVOKE": + handleInvoke(response); + break; + case "SHUTDOWN": + handleShutDown(); + break; + default: + Logger.error("invalid event type received " + eventType); + } + } + } + } catch (Exception e) { + Logger.error("error while processing extension -" + e.getMessage()); + e.printStackTrace(); + } + } + } + + private static void handleShutDown() { + Logger.debug("handling SHUTDOWN event, flushing the last profile"); + try { + // no need to stop the profiler as it has been stopped by the shutdown hook + s3Manager.upload(previousFileSuffix, true); + } catch (Exception e) { + Logger.error("could not upload the file"); + throw e; + } + System.exit(0); + } + + public static void handleInvoke(String payload) { + final String requestId = extractInfo(REQUEST_ID, payload); + final String randomSuffix = UUID.randomUUID().toString().substring(0,5); + Logger.debug("handling INVOKE event, requestID = " + requestId); + if (!coldstart) { + try { + stopProfiler(previousFileSuffix); + s3Manager.upload(previousFileSuffix, false); + startProfiler(); + } catch (Exception e) { + Logger.error("could not start the profiler"); + throw e; + } + } + coldstart = false; + previousFileSuffix = extractInfo(REQUEST_ID, payload) + "-" + randomSuffix; + } + + private static String extractInfo(String info, String jsonString) { + String prefix = "\"" + info + "\":\""; + String suffix = "\""; + + int startIndex = jsonString.indexOf(prefix); + if (startIndex == -1) { + return null; // requestId not found + } + + startIndex += prefix.length(); + int endIndex = jsonString.indexOf(suffix, startIndex); + + if (endIndex == -1) { + return null; // Malformed JSON + } + + return jsonString.substring(startIndex, endIndex); + } + + private static void startProfiler() { + try { + String url = String.format("http://localhost:%s/profiler/start", INTERNAL_COMMUNICATION_PORT); + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + Logger.debug("profiler successfully started"); + } + } catch(Exception e) { + Logger.error("could not start the profiler"); + e.printStackTrace(); + } + } + + private static void stopProfiler(String fileNameSuffix) { + try { + String url = String.format("http://localhost:%s/profiler/stop", INTERNAL_COMMUNICATION_PORT); + HttpRequest request = HttpRequest.newBuilder() + .GET() + .setHeader(HEADER_NAME, fileNameSuffix) + .uri(URI.create(url)) + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + Logger.debug("profiler successfully stopped"); + } + } catch(Exception e) { + Logger.error("could not stop the profiler"); + e.printStackTrace(); + } + } +} diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Logger.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Logger.java new file mode 100644 index 00000000..e064da10 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Logger.java @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +public class Logger { + + private static final boolean IS_DEBUG_ENABLED = initializeDebugFlag(); + private static final String PREFIX = "[PROFILER] "; + + private static boolean initializeDebugFlag() { + String envValue = System.getenv("AWS_LAMBDA_PROFILER_DEBUG"); + return "true".equalsIgnoreCase(envValue) || "1".equals(envValue); + } + + public static void debug(String message) { + if(IS_DEBUG_ENABLED) { + System.out.println(PREFIX + message); + } + } + + public static void error(String message) { + System.out.println(PREFIX + message); + } + +} \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java new file mode 100644 index 00000000..c0522641 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java @@ -0,0 +1,110 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.instrument.Instrumentation; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import one.profiler.AsyncProfiler; + +public class PreMain { + + private static final String DEFAULT_AWS_LAMBDA_PROFILER_START_COMMAND = "start,event=wall,interval=1us"; + private static final String DEFAULT_AWS_LAMBDA_PROFILER_STOP_COMMAND = "stop,file=%s,include=*AWSLambda.main,include=start_thread"; + private static final String PROFILER_START_COMMAND = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_START_COMMAND", DEFAULT_AWS_LAMBDA_PROFILER_START_COMMAND); + private static final String PROFILER_STOP_COMMAND = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_STOP_COMMAND", DEFAULT_AWS_LAMBDA_PROFILER_STOP_COMMAND); + private static final String INTERNAL_COMMUNICATION_PORT = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_COMMUNICATION_PORT", "1234"); + + public static void premain(String agentArgs, Instrumentation inst) { + Logger.debug("premain is starting"); + if(!createFileIfNotExist("/tmp/aws-lambda-java-profiler")) { + Logger.debug("starting the profiler for coldstart"); + startProfiler(); + registerShutdownHook(); + try { + Integer port = Integer.parseInt(INTERNAL_COMMUNICATION_PORT); + Logger.debug("using profile communication port = " + port); + HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); + server.createContext("/profiler/start", new StartProfiler()); + server.createContext("/profiler/stop", new StopProfiler()); + server.setExecutor(null); // Use the default executor + server.start(); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + + private static boolean createFileIfNotExist(String filePath) { + File file = new File(filePath); + try { + return file.createNewFile(); + } catch (IOException e) { + System.out.println(e); + return false; + } + } + + public static class StopProfiler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + Logger.debug("hit /profiler/stop"); + final String fileName = exchange.getRequestHeaders().getFirst(ExtensionMain.HEADER_NAME); + stopProfiler(fileName); + String response = "ok"; + exchange.sendResponseHeaders(200, response.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes(StandardCharsets.UTF_8)); + } + } + } + + public static class StartProfiler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + Logger.debug("hit /profiler/start"); + startProfiler(); + String response = "ok"; + exchange.sendResponseHeaders(200, response.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes(StandardCharsets.UTF_8)); + } + } + } + + + public static void stopProfiler(String fileNameSuffix) { + try { + final String fileName = String.format("/tmp/profiling-data-%s.html", fileNameSuffix); + Logger.debug("stopping the profiler with filename = " + fileName + " with command = " + PROFILER_STOP_COMMAND); + AsyncProfiler.getInstance().execute(String.format(PROFILER_STOP_COMMAND, fileName)); + } catch(Exception e) { + Logger.error("could not stop the profiler"); + e.printStackTrace(); + } + } + + public static void startProfiler() { + try { + Logger.debug("staring the profiler with command = " + PROFILER_START_COMMAND); + AsyncProfiler.getInstance().execute(PROFILER_START_COMMAND); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static void registerShutdownHook() { + Logger.debug("registering shutdown hook"); + Thread shutdownHook = new Thread(new ShutdownHook(PROFILER_STOP_COMMAND)); + Runtime.getRuntime().addShutdownHook(shutdownHook); + } + +} \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java new file mode 100644 index 00000000..3b55984c --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import java.io.File; +import java.time.format.DateTimeFormatter; +import java.time.Instant; +import java.time.LocalDate; + +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; + +public class S3Manager { + + private static final String RESULTS_BUCKET = "AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME"; + private static final String FUNCTION_NAME = System.getenv().getOrDefault("AWS_LAMBDA_FUNCTION_NAME", "function"); + private S3Client s3Client; + private String bucketName; + + public S3Manager() { + final String bucketName = System.getenv(RESULTS_BUCKET); + Logger.debug("creating S3Manager with bucketName = " + bucketName); + if (null == bucketName || bucketName.isEmpty()) { + throw new IllegalArgumentException("please set the bucket name using AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME environment variable"); + } + this.s3Client = S3Client.builder().build(); + this.bucketName = bucketName; + Logger.debug("S3Manager successfully created"); + } + + public void upload(String fileName, boolean isShutDownEvent) { + try { + final String suffix = isShutDownEvent ? "shutdown" : fileName; + final String key = buildKey(FUNCTION_NAME, fileName); + Logger.debug("uploading profile to key = " + key); + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + File file = new File(String.format("/tmp/profiling-data-%s.html", suffix)); + if (file.exists()) { + Logger.debug("file size is " + file.length()); + RequestBody requestBody = RequestBody.fromFile(file); + PutObjectResponse response = s3Client.putObject(putObjectRequest, requestBody); + Logger.debug("profile uploaded successfully. ETag: " + response.eTag()); + if(file.delete()) { + Logger.debug("file deleted"); + } + } else { + throw new IllegalArgumentException("could not find the profile to upload"); + } + } catch (Exception e) { + Logger.error("could not upload the profile"); + e.printStackTrace(); + } + } + + private String buildKey(String functionName, String fileName) { + final LocalDate currentDate = LocalDate.now(); + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd"); + final String formattedDate = currentDate.format(formatter); + return String.format("%s/%s/%s", formattedDate, functionName, fileName); + } + +} \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ShutdownHook.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ShutdownHook.java new file mode 100644 index 00000000..a36584bc --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ShutdownHook.java @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import one.profiler.AsyncProfiler; + +public class ShutdownHook implements Runnable { + + private String stopCommand; + + public ShutdownHook(String stopCommand) { + this.stopCommand = stopCommand; + } + + @Override + public void run() { + Logger.debug("running ShutdownHook"); + try { + final String fileName = "/tmp/profiling-data-shutdown.html"; + Logger.debug("stopping the profiler"); + AsyncProfiler.getInstance().execute(String.format(this.stopCommand, fileName)); + } catch (Exception e) { + Logger.error("could not stop the profiler"); + } + } +} \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/cleanup.sh b/experimental/aws-lambda-java-profiler/integration_tests/cleanup.sh new file mode 100755 index 00000000..d58142a0 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/cleanup.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Set variables +LAYER_ARN=$(cat /tmp/layer_arn) +FUNCTION_NAME="aws-lambda-java-profiler-function-${GITHUB_RUN_ID}" +ROLE_NAME="aws-lambda-java-profiler-role-${GITHUB_RUN_ID}" + +# Function to check if a command was successful +check_success() { + if [ $? -eq 0 ]; then + echo "Success: $1" + else + echo "Error: Failed to $1" + exit 1 + fi +} + +# Delete Lambda Layer +echo "Deleting Lambda Layer..." +aws lambda delete-layer-version --layer-name $(echo $LAYER_ARN | cut -d: -f7) --version-number $(echo $LAYER_ARN | cut -d: -f8) +check_success "delete Lambda Layer" + +# Delete Lambda Function +echo "Deleting Lambda Function..." +aws lambda delete-function --function-name $FUNCTION_NAME +check_success "delete Lambda Function" + +# Delete IAM Role +echo "Deleting IAM Role..." +# First, detach all policies from the role +for policy in $(aws iam list-attached-role-policies --role-name $ROLE_NAME --query 'AttachedPolicies[*].PolicyArn' --output text); do + aws iam detach-role-policy --role-name $ROLE_NAME --policy-arn $policy + check_success "detach policy $policy from role $ROLE_NAME" +done + +# Remove s3 inline policy +aws iam delete-role-policy --role-name $ROLE_NAME --policy-name "s3PutObject" +check_success "deleted inline policy" + + +# Then delete the role +aws iam delete-role --role-name $ROLE_NAME +check_success "delete IAM Role" + +echo "All deletions completed successfully." \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/create_bucket.sh b/experimental/aws-lambda-java-profiler/integration_tests/create_bucket.sh new file mode 100755 index 00000000..0ba50b73 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/create_bucket.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +PROFILER_RESULTS_BUCKET_NAME="aws-lambda-java-profiler-bucket-${GITHUB_RUN_ID}" + +# Create the S3 bucket +aws s3 mb s3://"$PROFILER_RESULTS_BUCKET_NAME" + +# Check if the bucket was created successfully +if [ $? -eq 0 ]; then + echo "Bucket '$PROFILER_RESULTS_BUCKET_NAME' created successfully." +else + echo "Error: Failed to create bucket '$PROFILER_RESULTS_BUCKET_NAME'." + exit 1 +fi \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh b/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh new file mode 100755 index 00000000..114909d0 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# Set variables +FUNCTION_NAME="aws-lambda-java-profiler-function-${GITHUB_RUN_ID}" +ROLE_NAME="aws-lambda-java-profiler-role-${GITHUB_RUN_ID}" +HANDLER="helloworld.Handler::handleRequest" +RUNTIME="java21" +LAYER_ARN=$(cat /tmp/layer_arn) + +JAVA_TOOL_OPTIONS="-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler-extension.jar" +AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME="aws-lambda-java-profiler-bucket-${GITHUB_RUN_ID}" + +# Compile the Hello World project +cd integration_tests/helloworld +gradle :buildZip +cd ../.. + +# Create IAM role for Lambda +ROLE_ARN=$(aws iam create-role \ + --role-name $ROLE_NAME \ + --assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}' \ + --query 'Role.Arn' \ + --output text) + +# Attach basic Lambda execution policy to the role +aws iam attach-role-policy \ + --role-name $ROLE_NAME \ + --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + +# Attach s3:PutObject policy to the role so we can upload profiles +POLICY_DOCUMENT=$(cat < $new_filename" + else + echo "No change: $filename" + fi + fi +done + +echo "All files processed." \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle new file mode 100644 index 00000000..927317f8 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'java' + +repositories { + mavenCentral() +} + +sourceCompatibility = 21 +targetCompatibility = 21 + +dependencies { + implementation ( + 'com.amazonaws:aws-lambda-java-core:1.2.3', + 'com.amazonaws:aws-lambda-java-events:3.11.0', + 'org.slf4j:slf4j-api:2.0.13' + ) +} + +task buildZip(type: Zip) { + archiveBaseName = "code" + from compileJava + from processResources + into('lib') { + from configurations.runtimeClasspath + } +} + +build.dependsOn buildZip \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/java/helloworld/Handler.java b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/java/helloworld/Handler.java new file mode 100644 index 00000000..a29cae18 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/java/helloworld/Handler.java @@ -0,0 +1,53 @@ +package helloworld; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.List; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.amazonaws.services.lambda.runtime.Context; + +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Handler implements RequestHandler { + + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + long start = System.currentTimeMillis(); + List result = slowRecursiveFunction(0, 5); + long end = System.currentTimeMillis(); + long duration = end - start; + + System.out.println("Function execution time: " + duration + " ms"); + System.out.println("Result size: " + result.size()); + System.out.println("First few elements: " + result.subList(0, Math.min(10, result.size()))); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withBody("ok"); + + } + + private static List slowRecursiveFunction(int n, int depth) { + List result = new ArrayList<>(); + if (depth == 0) { + return result; + } + long startTime = System.currentTimeMillis(); + while (System.currentTimeMillis() - startTime < 100) { + // nothing to do here + } + result.add(n); + result.addAll(slowRecursiveFunction(n + 2, depth - 1)); + return result; + } +} diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/resources/wrapper.sh b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/resources/wrapper.sh new file mode 100644 index 00000000..b54b7767 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/resources/wrapper.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# the path to the interpreter and all of the originally intended arguments +args=("$@") + +# the extra options to pass to the interpreter +echo "${args[@]}" + +# start the runtime with the extra options +exec "${args[@]}" \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh new file mode 100755 index 00000000..741eec14 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# Set variables +FUNCTION_NAME="aws-lambda-java-profiler-function-${GITHUB_RUN_ID}" +PAYLOAD='{"key": "value"}' + +echo "Invoking Lambda function: $FUNCTION_NAME" + +# Invoke the Lambda function synchronously and capture the response +RESPONSE=$(aws lambda invoke \ + --function-name "$FUNCTION_NAME" \ + --payload "$PAYLOAD" \ + --cli-binary-format raw-in-base64-out \ + --log-type Tail \ + output.json) + +# Extract the status code and log result from the response +STATUS_CODE=$(echo "$RESPONSE" | jq -r '.StatusCode') +LOG_RESULT=$(echo "$RESPONSE" | jq -r '.LogResult') + +echo "Function invocation completed with status code: $STATUS_CODE" + +# Decode and display the logs +if [ -n "$LOG_RESULT" ]; then + echo "Function logs:" + echo "$LOG_RESULT" | base64 --decode +else + echo "No logs available." +fi + +# Display the function output +echo "Function output:" +cat output.json + +echo "$LOG_RESULT" | base64 --decode | grep "starting the profiler for coldstart" || exit 1 +echo "$LOG_RESULT" | base64 --decode | grep -v "uploading" || exit 1 + +# Clean up the output file +rm output.json + + +# Invoke it a second time for warm start +echo "Invoking Lambda function: $FUNCTION_NAME" + +# Invoke the Lambda function synchronously and capture the response +RESPONSE=$(aws lambda invoke \ + --function-name "$FUNCTION_NAME" \ + --payload "$PAYLOAD" \ + --cli-binary-format raw-in-base64-out \ + --log-type Tail \ + output.json) + +# Extract the status code and log result from the response +STATUS_CODE=$(echo "$RESPONSE" | jq -r '.StatusCode') +LOG_RESULT=$(echo "$RESPONSE" | jq -r '.LogResult') + +echo "Function invocation completed with status code: $STATUS_CODE" + +# Decode and display the logs +if [ -n "$LOG_RESULT" ]; then + echo "Function logs:" + echo "$LOG_RESULT" | base64 --decode +else + echo "No logs available." +fi + +# Display the function output +echo "Function output:" +cat output.json + +echo "$LOG_RESULT" | base64 --decode | grep "uploading" || exit 1 + +# Clean up the output file +rm output.json diff --git a/experimental/aws-lambda-java-profiler/integration_tests/publish_layer.sh b/experimental/aws-lambda-java-profiler/integration_tests/publish_layer.sh new file mode 100755 index 00000000..879944e8 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/publish_layer.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Set variables +LAYER_NAME="aws-lambda-java-profiler-test" +DESCRIPTION="AWS Lambda Java Profiler Test Layer" +ZIP_FILE="./extension/extension.zip" +RUNTIME="java21" +ARCHITECTURE="x86_64" + +# Check if AWS CLI is installed +if ! command -v aws &> /dev/null; then + echo "AWS CLI is not installed. Please install it first." + exit 1 +fi + +# Check if the ZIP file exists +if [ ! -f "$ZIP_FILE" ]; then + echo "ZIP file $ZIP_FILE not found. Please make sure it exists." + exit 1 +fi + +# Publish the layer +echo "Publishing layer $LAYER_NAME..." +RESPONSE=$(aws lambda publish-layer-version \ + --layer-name "$LAYER_NAME" \ + --description "$DESCRIPTION" \ + --zip-file "fileb://$ZIP_FILE" \ + --compatible-runtimes "$RUNTIME" \ + --compatible-architectures "$ARCHITECTURE") + +# Check if the layer was published successfully +if [ $? -eq 0 ]; then + LAYER_VERSION=$(echo $RESPONSE | jq -r '.Version') + LAYER_ARN=$(echo $RESPONSE | jq -r '.LayerVersionArn') + echo "Layer published successfully!" + echo "Layer Version: $LAYER_VERSION" + echo "Layer ARN: $LAYER_ARN" + echo $LAYER_ARN > /tmp/layer_arn +else + echo "Failed to publish layer. Please check your AWS credentials and permissions." + exit 1 +fi \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/update-function.sh b/experimental/aws-lambda-java-profiler/update-function.sh new file mode 100755 index 00000000..e849246a --- /dev/null +++ b/experimental/aws-lambda-java-profiler/update-function.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# Check if a function name was provided +if [ $# -eq 0 ]; then + echo "Please provide a function name as an argument." + echo "Usage: $0 " + exit 1 +fi + +FUNCTION_NAME="$1" + +# Generate a random lowercase S3 bucket name +RANDOM_SUFFIX=$(uuidgen | tr '[:upper:]' '[:lower:]' | cut -d'-' -f1) +BUCKET_NAME="my-bucket-${RANDOM_SUFFIX}" +echo "Generated bucket name: $BUCKET_NAME" + +# Create the S3 bucket with the random name +aws s3 mb "s3://$BUCKET_NAME" + +# Create a Lambda layer +aws lambda publish-layer-version \ + --layer-name profiler-layer \ + --description "Profiler Layer" \ + --license-info "MIT" \ + --zip-file fileb://extension/extension.zip \ + --compatible-runtimes java11 java17 java21 \ + --compatible-architectures "arm64" "x86_64" + +# Assign the layer to the function +aws lambda update-function-configuration \ + --function-name "$FUNCTION_NAME" \ + --layers $(aws lambda list-layer-versions --layer-name profiler-layer --query 'LayerVersions[0].LayerVersionArn' --output text) + +# Wait for the function to be updated +aws lambda wait function-updated \ + --function-name "$FUNCTION_NAME" + +# Get existing environment variables (handle null case) +EXISTING_VARS=$(aws lambda get-function-configuration --function-name "$FUNCTION_NAME" --query "Environment.Variables" --output json 2>/dev/null) +if [[ -z "$EXISTING_VARS" || "$EXISTING_VARS" == "null" ]]; then + EXISTING_VARS="{}" +fi + +# Define new environment variables in JSON format +NEW_VARS=$(jq -n --arg bucket "$BUCKET_NAME" \ + --arg java_opts "-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler-extension.jar" \ + '{AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME: $bucket, JAVA_TOOL_OPTIONS: $java_opts}') + +# Merge existing and new variables (compact JSON output) +UPDATED_VARS=$(echo "$EXISTING_VARS" | jq -c --argjson new_vars "$NEW_VARS" '. + $new_vars') + +# Convert JSON to "Key=Value" format for AWS CLI +ENV_VARS_FORMATTED=$(echo "$UPDATED_VARS" | jq -r 'to_entries | map("\(.key)=\(.value)") | join(",")') + +# Update Lambda function with correct format +aws lambda update-function-configuration \ + --function-name "$FUNCTION_NAME" \ + --environment "Variables={$ENV_VARS_FORMATTED}" + +# Update the function's permissions to write to the S3 bucket +# Get the function's execution role +ROLE_NAME=$(aws lambda get-function --function-name "$FUNCTION_NAME" --query 'Configuration.Role' --output text | awk -F'/' '{print $NF}') + +# Create a policy document +cat << EOF > s3-write-policy.json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:PutObject" + ], + "Resource": [ + "arn:aws:s3:::$BUCKET_NAME", + "arn:aws:s3:::$BUCKET_NAME/*" + ] + } + ] +} +EOF + +# Attach the policy to the role +aws iam put-role-policy \ + --role-name "$ROLE_NAME" \ + --policy-name S3WriteAccess \ + --policy-document file://s3-write-policy.json + +echo "Setup completed for function $FUNCTION_NAME with S3 bucket $BUCKET_NAME" +echo "S3 write permissions added to the function's execution role" + +# Clean up temporary files +rm s3-write-policy.json diff --git a/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml b/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml index fde6b625..7d3c4424 100644 --- a/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml +++ b/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml @@ -20,7 +20,7 @@ com.amazonaws aws-lambda-java-events - 3.14.0 + 3.16.0 diff --git a/samples/custom-serialization/gson/HelloWorldFunction/pom.xml b/samples/custom-serialization/gson/HelloWorldFunction/pom.xml index 56327d99..fd427182 100644 --- a/samples/custom-serialization/gson/HelloWorldFunction/pom.xml +++ b/samples/custom-serialization/gson/HelloWorldFunction/pom.xml @@ -20,7 +20,7 @@ com.amazonaws aws-lambda-java-events - 3.14.0 + 3.16.0 com.google.code.gson diff --git a/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml b/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml index abbf6968..2773ef1f 100644 --- a/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml +++ b/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml @@ -20,7 +20,7 @@ com.amazonaws aws-lambda-java-events - 3.14.0 + 3.16.0 diff --git a/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml index 234dd6eb..f6ca21bf 100644 --- a/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml +++ b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml @@ -20,7 +20,7 @@ com.amazonaws aws-lambda-java-events - 3.14.0 + 3.16.0 com.google.code.gson diff --git a/samples/kinesis-firehose-event-handler/pom.xml b/samples/kinesis-firehose-event-handler/pom.xml index d7b04fbc..56c95924 100644 --- a/samples/kinesis-firehose-event-handler/pom.xml +++ b/samples/kinesis-firehose-event-handler/pom.xml @@ -46,7 +46,7 @@ com.amazonaws aws-lambda-java-events - 3.14.0 + 3.16.0