diff --git a/.github/workflows/build-integration-test.yml b/.github/workflows/build-integration-test.yml new file mode 100644 index 00000000..748e0e1e --- /dev/null +++ b/.github/workflows/build-integration-test.yml @@ -0,0 +1,56 @@ +# this workflow verifies that the integration test Lambda function builds successfully. +# it does NOT deploy or run the tests (that requires AWS credentials and is done in +# run-integration-test.yml). + +name: Build integration tests + +on: + push: + branches: [ main ] + paths: + - 'aws-lambda-java-log4j2/**' + - 'aws-lambda-java-core/**' + - 'lambda-integration-tests/**' + pull_request: + branches: [ '*' ] + paths: + - 'aws-lambda-java-log4j2/**' + - 'aws-lambda-java-core/**' + - 'lambda-integration-tests/**' + - '.github/workflows/build-integration-test.yml' + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up JDK + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + with: + java-version: | + 8 + 21 + distribution: corretto + cache: maven + + - name: Install core with Maven + run: | + export JAVA_HOME=$JAVA_HOME_8_X64 + mvn -B install --file aws-lambda-java-core/pom.xml + + - name: Install log4j2 with Maven + run: | + export JAVA_HOME=$JAVA_HOME_8_X64 + mvn -B install --file aws-lambda-java-log4j2/pom.xml + + # build the integration test function + # this verifies that the function compiles and packages correctly. + # the tests will run in run-integration-test.yml which deploys to AWS. + - name: Package integration test function + run: | + export JAVA_HOME=$JAVA_HOME_21_X64 + mvn -B package --file lambda-integration-tests/log4j2-test-function/pom.xml diff --git a/.github/workflows/run-integration-test.yml b/.github/workflows/run-integration-test.yml new file mode 100644 index 00000000..bd77684e --- /dev/null +++ b/.github/workflows/run-integration-test.yml @@ -0,0 +1,102 @@ +# this workflow deploys a Lambda function that uses aws-lambda-java-log4j2, +# invokes it, and verifies that logs arrive in CloudWatch. + +name: Run integration tests + +permissions: + id-token: write + contents: read + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - 'aws-lambda-java-log4j2/**' + - 'aws-lambda-java-core/**' + - 'lambda-integration-tests/**' + +jobs: + run-integration-tests: + # Only run on the main repo, not forks + if: ${{ github.repository_owner == 'aws' }} + runs-on: ubuntu-latest + concurrency: + group: integration-test + cancel-in-progress: false + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up JDK + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + with: + java-version: | + 8 + 21 + distribution: corretto + cache: maven + + - name: Install SAM CLI + uses: aws-actions/setup-sam@f84ec7d548307efafe33230528756de3c5841a17 # v2 + with: + use-installer: true + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + role-session-name: ${{ secrets.ROLE_SESSION_NAME }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Install core with Maven + run: | + export JAVA_HOME=$JAVA_HOME_8_X64 + mvn -B install --file aws-lambda-java-core/pom.xml + + - name: Install log4j2 with Maven + run: | + export JAVA_HOME=$JAVA_HOME_8_X64 + mvn -B install --file aws-lambda-java-log4j2/pom.xml + + - name: Build SAM stack + run: | + export JAVA_HOME=$JAVA_HOME_21_X64 + cd lambda-integration-tests && sam build + + - name: Validate SAM stack + run: cd lambda-integration-tests && sam validate --lint + + - name: Deploy stack + id: deploy_stack + env: + AWS_REGION: ${{ secrets.AWS_REGION }} + run: | + cd lambda-integration-tests + stackName="aws-lambda-java-log4j2-integ-test-$GITHUB_RUN_ID" + echo "STACK_NAME=$stackName" >> "$GITHUB_OUTPUT" + echo "Stack name = $stackName" + sam deploy \ + --stack-name "${stackName}" \ + --parameter-overrides "ParameterKey=LambdaRole,ParameterValue=${{ secrets.AWS_LAMBDA_ROLE }}" \ + --no-confirm-changeset \ + --no-progressbar \ + --resolve-s3 \ + --capabilities CAPABILITY_IAM \ + 2>&1 | tee /tmp/sam-deploy.log | tail -n 20 + LOG4J2_TEST_FUNCTION=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | select(.OutputKey=="Log4j2TestFunction") | .OutputValue') + echo "LOG4J2_TEST_FUNCTION=$LOG4J2_TEST_FUNCTION" >> "$GITHUB_OUTPUT" + echo "Function name: $LOG4J2_TEST_FUNCTION" + + - name: Run integration test + env: + LOG4J2_TEST_FUNCTION: ${{ steps.deploy_stack.outputs.LOG4J2_TEST_FUNCTION }} + AWS_REGION: ${{ secrets.AWS_REGION }} + run: ./lambda-integration-tests/run-tests.sh + + - name: Cleanup + if: always() && steps.deploy_stack.outputs.STACK_NAME + env: + AWS_REGION: ${{ secrets.AWS_REGION }} + STACK_NAME: ${{ steps.deploy_stack.outputs.STACK_NAME }} + run: | + sam delete --stack-name "${STACK_NAME}" --no-prompts --region "${AWS_REGION}" diff --git a/lambda-integration-tests/log4j2-test-function/pom.xml b/lambda-integration-tests/log4j2-test-function/pom.xml new file mode 100644 index 00000000..b036d1de --- /dev/null +++ b/lambda-integration-tests/log4j2-test-function/pom.xml @@ -0,0 +1,77 @@ + + 4.0.0 + + com.amazonaws + log4j2-integration-test-function + 1.0.0 + jar + + Log4j2 Integration Test Function + + Lambda function used to verify that aws-lambda-java-log4j2 correctly emits logs to CloudWatch. + + + + 21 + 21 + UTF-8 + 2.25.4 + + + + + com.amazonaws + aws-lambda-java-core + 1.4.0 + + + com.amazonaws + aws-lambda-java-log4j2 + 1.6.4 + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + package + + shade + + + + + + + + + + + + com.github.edwgiz + maven-shade-plugin.log4j2-cachefile-transformer + 2.8.1 + + + + + + diff --git a/lambda-integration-tests/log4j2-test-function/src/main/java/integ/Log4j2TestHandler.java b/lambda-integration-tests/log4j2-test-function/src/main/java/integ/Log4j2TestHandler.java new file mode 100644 index 00000000..d81a3fa2 --- /dev/null +++ b/lambda-integration-tests/log4j2-test-function/src/main/java/integ/Log4j2TestHandler.java @@ -0,0 +1,30 @@ +package integ; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Map; + +/** + * integration test handler that logs a marker string using Log4j2 with the LambdaAppender. + * the test verifies that the marker appears in CloudWatch Logs, confirming end-to-end + * log delivery through the aws-lambda-java-log4j2 library. + */ +public class Log4j2TestHandler implements RequestHandler, String> { + + private static final Logger logger = LogManager.getLogger(Log4j2TestHandler.class); + + @Override + public String handleRequest(Map event, Context context) { + String marker = event.getOrDefault("marker", "NO_MARKER_PROVIDED"); + + logger.info("INTEG_TEST_MARKER: {}", marker); + logger.debug("Debug level message with marker: {}", marker); + logger.warn("Warning level message with marker: {}", marker); + logger.error("Error level message with marker: {}", marker); + + return "OK:" + marker; + } +} diff --git a/lambda-integration-tests/log4j2-test-function/src/main/resources/log4j2.xml b/lambda-integration-tests/log4j2-test-function/src/main/resources/log4j2.xml new file mode 100644 index 00000000..1cbc36bd --- /dev/null +++ b/lambda-integration-tests/log4j2-test-function/src/main/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + %d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m%n + + + + + + + + + + diff --git a/lambda-integration-tests/run-tests.sh b/lambda-integration-tests/run-tests.sh new file mode 100755 index 00000000..844cc565 --- /dev/null +++ b/lambda-integration-tests/run-tests.sh @@ -0,0 +1,103 @@ +# integration test script for aws-lambda-java-log4j2. +# invokes the deployed lambda function and verifies logs appear in CloudWatch. + +set -euo pipefail + +FUNCTION_NAME="${LOG4J2_TEST_FUNCTION:?LOG4J2_TEST_FUNCTION env var is required}" +REGION="${AWS_REGION:?AWS_REGION env var is required}" +MARKER="integ-test-$(date +%s)-${RANDOM}" + +echo "=== Log4j2 Integration Test ===" +echo "Function: ${FUNCTION_NAME}" +echo "Region: ${REGION}" +echo "Marker: ${MARKER}" +echo "" + +# invoke the lambda function +echo ">>> Invoking Lambda function..." +INVOKE_OUTPUT=$(aws lambda invoke \ + --function-name "${FUNCTION_NAME}" \ + --region "${REGION}" \ + --payload "{\"marker\": \"${MARKER}\"}" \ + --cli-binary-format raw-in-base64-out \ + --output json \ + /tmp/integ-test-response.json) || { + echo "FAIL: aws lambda invoke command failed with exit code $?" + echo "Output: ${INVOKE_OUTPUT:-}" + exit 1 +} + +echo "Invoke output: ${INVOKE_OUTPUT}" +RESPONSE=$(cat /tmp/integ-test-response.json) +echo "Response payload: ${RESPONSE}" + +# check for lambda execution errors +FUNCTION_ERROR=$(echo "${INVOKE_OUTPUT}" | jq -r '.FunctionError // empty') +if [ -n "${FUNCTION_ERROR}" ]; then + echo "FAIL: Lambda function returned an execution error (FunctionError: ${FUNCTION_ERROR})" + echo "Error response: ${RESPONSE}" + exit 1 +fi + +# verify the function executed successfully +if echo "${RESPONSE}" | grep -q "OK:${MARKER}"; then + echo ">>> Function invocation successful." +else + echo "FAIL: Unexpected response from Lambda function." + echo "Expected response containing: OK:${MARKER}" + echo "Got: ${RESPONSE}" + exit 1 +fi + +# query CloudWatch logs for the marker +LOG_GROUP="/aws/lambda/${FUNCTION_NAME}" +echo "" +echo ">>> Querying CloudWatch Logs group: ${LOG_GROUP}" + +MAX_ATTEMPTS=5 +WAIT_SECONDS=10 +FOUND=false + +for attempt in $(seq 1 $MAX_ATTEMPTS); do + echo ">>> Attempt ${attempt}/${MAX_ATTEMPTS}: waiting ${WAIT_SECONDS}s for log propagation..." + sleep "${WAIT_SECONDS}" + + LOGS_OUTPUT=$(aws logs filter-log-events \ + --log-group-name "${LOG_GROUP}" \ + --region "${REGION}" \ + --filter-pattern "\"INTEG_TEST_MARKER\" \"${MARKER}\"" \ + --start-time $(($(date +%s) * 1000 - 120000)) \ + --output json 2>&1) + + if echo "${LOGS_OUTPUT}" | grep -q "INTEG_TEST_MARKER: ${MARKER}"; then + FOUND=true + break + fi + + echo " Marker not found yet." + WAIT_SECONDS=$((WAIT_SECONDS * 2)) +done + +# verify the marker was found +if [ "${FOUND}" = true ]; then + echo "" + echo "=== PASS: Log4j2 integration test succeeded ===" + echo "The marker '${MARKER}' was found in CloudWatch Logs (attempt ${attempt})." + echo "This confirms that the LambdaAppender plugin was discovered by Log4j2" + echo "and logs are being delivered to CloudWatch correctly." +else + echo "" + echo "=== FAIL: Log4j2 integration test failed ===" + echo "The marker '${MARKER}' was NOT found in CloudWatch Logs after ${MAX_ATTEMPTS} attempts." + echo "This indicates that the LambdaAppender was not discovered by Log4j2," + echo "likely due to a missing Log4j2Plugins.dat in the packaged JAR." + echo "" + echo "Dumping all recent log events for debugging:" + aws logs filter-log-events \ + --log-group-name "${LOG_GROUP}" \ + --region "${REGION}" \ + --start-time $(($(date +%s) * 1000 - 120000)) \ + --limit 50 \ + --output text 2>&1 || true + exit 1 +fi diff --git a/lambda-integration-tests/samconfig.toml b/lambda-integration-tests/samconfig.toml new file mode 100644 index 00000000..5e659786 --- /dev/null +++ b/lambda-integration-tests/samconfig.toml @@ -0,0 +1,24 @@ +version = 0.1 + +[default] +[default.build.parameters] +cached = true +parallel = true +build_in_source = true + +[default.validate.parameters] +lint = true + +[default.deploy.parameters] +capabilities = "CAPABILITY_IAM" +confirm_changeset = true +resolve_s3 = true + +[default.sync.parameters] +watch = true + +[default.local_start_api.parameters] +warm_containers = "EAGER" + +[default.local_start_lambda.parameters] +warm_containers = "EAGER" diff --git a/lambda-integration-tests/template.yaml b/lambda-integration-tests/template.yaml new file mode 100644 index 00000000..101e586e --- /dev/null +++ b/lambda-integration-tests/template.yaml @@ -0,0 +1,34 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: aws-lambda-java-log4j2 integration tests + +Parameters: + LambdaRole: + Type: String + +Globals: + Function: + Timeout: 30 + MemorySize: 512 + +Resources: + Log4j2TestFunction: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: java21 + Properties: + CodeUri: log4j2-test-function/ + Handler: integ.Log4j2TestHandler::handleRequest + Runtime: java21 + Role: !Ref LambdaRole + Environment: + Variables: + AWS_LAMBDA_LOG_FORMAT: TEXT + +Outputs: + Log4j2TestFunction: + Description: "Log4j2 integration test function name" + Value: !Ref Log4j2TestFunction + Log4j2TestFunctionArn: + Description: "Log4j2 integration test function ARN" + Value: !GetAtt Log4j2TestFunction.Arn