diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index abd8faa56..8810605b9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,46 +1,81 @@ ---- name: Bug report -about: Create a report to help us improve -title: '' -labels: bug, triage -assignees: '' +description: Report a reproducible bug to help us improve +title: "Bug: TITLE" +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Thank you for submitting a bug report. Please add as much information as possible to help us reproduce, and remove any potential sensitive data. ---- + Please become familiar with [our definition of bug](https://docs.powertools.aws.dev/lambda/java/processes/maintainers/#is-that-a-bug). + - type: textarea + id: expected_behaviour + attributes: + label: Expected Behaviour + description: Please share details on the behaviour you expected + validations: + required: true + - type: textarea + id: current_behaviour + attributes: + label: Current Behaviour + description: Please share details on the current issue + validations: + required: true + - type: textarea + id: code_snippet + attributes: + label: Code snippet + description: Please share a code snippet to help us reproduce the issue + render: java + validations: + required: true + - type: textarea + id: solution + attributes: + label: Possible Solution + description: If known, please suggest a potential resolution + validations: + required: false + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: Please share how we might be able to reproduce this issue + validations: + required: true + - type: input + id: version + attributes: + label: Powertools for AWS Lambda (Java) version + placeholder: "latest, 1.19.0" + value: latest + validations: + required: true + - type: dropdown + id: runtime + attributes: + label: AWS Lambda function runtime + options: + - "Java 8" + - "Java 11" + - "Java 17" + - "Java 21" + - "provided.al2023" + validations: + required: true + - type: textarea + id: logs + attributes: + label: Debugging logs + description: If available, please share [debugging logs](https://docs.powertools.aws.dev/lambda/lambda/#debug-mode) + render: java + validations: + required: false + - type: markdown + attributes: + value: | + --- - - - -**What were you trying to accomplish?** - -## Expected Behavior - - - -## Current Behavior - - - -## Possible Solution - - - -## Steps to Reproduce (for bugs) - - -1. -2. -3. -4. - -## Environment - -* **Powertools for AWS Lambda (Java) version used**: -* **Packaging format (Layers, Maven/Gradle)**: -* **AWS Lambda function runtime:** -* **Debugging logs** - -> [How to enable debug mode](https://docs.powertools.aws.dev/lambda-java/#debug-mode)** - -```text -# paste logs here -``` + **Disclaimer**: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/maintenance.yml b/.github/ISSUE_TEMPLATE/maintenance.yml index 843f5103c..1a84ed7ef 100644 --- a/.github/ISSUE_TEMPLATE/maintenance.yml +++ b/.github/ISSUE_TEMPLATE/maintenance.yml @@ -57,11 +57,11 @@ body: options: - label: This request meets [Powertools for AWS Lambda (Java) Tenets](https://docs.powertools.aws.dev/lambda-java/#tenets) required: true - - label: Should this be considered in other Powertools for AWS Lambda (Java) languages? i.e. [Python](https://github.com/aws-powertools/powertools-lambda-python/), [TypeScript](https://github.com/aws-powertools/powertools-lambda-typescript/) + - label: Should this be considered in other Powertools for AWS Lambda languages? i.e. [Python](https://github.com/aws-powertools/powertools-lambda-python/), [TypeScript](https://github.com/aws-powertools/powertools-lambda-typescript/) required: false - type: markdown attributes: value: | --- - **Disclaimer**: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful. + **Disclaimer**: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/rfc.md b/.github/ISSUE_TEMPLATE/rfc.md index 84fce71df..ae2337402 100644 --- a/.github/ISSUE_TEMPLATE/rfc.md +++ b/.github/ISSUE_TEMPLATE/rfc.md @@ -1,52 +1,107 @@ ---- -name: RFC -about: Feature design and proposals -title: 'RFC: ' -labels: RFC, triage -assignees: '' - ---- - -## Key information - -* RFC PR: (leave this empty) -* Related issue(s), if known: -* Area: (i.e. Tracer, Metrics, Logger, etc.) -* Meet [tenets](https://docs.powertools.aws.dev/lambda-java/#tenets): (Yes/no) - -## Summary -[summary]: #summary - -> One paragraph explanation of the feature. - -## Motivation -[motivation]: #motivation - -> Why are we doing this? What use cases does it support? What is the expected outcome? - -## Proposal -[proposal]: #proposal - -> This is the bulk of the RFC. - -> Explain the design in enough detail for somebody familiar with Powertools for AWS Lambda (Java) to understand it, and for somebody familiar with the implementation to implement it. - -> This should get into specifics and corner-cases, and include examples of how the feature is used. Any new terminology should be defined here. - -## Drawbacks -[drawbacks]: #drawbacks - -> Why should we *not* do this? - -> Do we need additional dependencies? Impact performance/package size? - -## Rationale and alternatives -[rationale-and-alternatives]: #rationale-and-alternatives - -* **What other designs have been considered? Why not them?** -* **What is the impact of not doing this?** - -## Unresolved questions -[unresolved-questions]: #unresolved-questions - -> Optional, stash area for topics that need further development e.g. TBD +name: Request for Comments (RFC) +description: Feature design and detailed proposals +title: "RFC: TITLE" +labels: ["RFC", "triage"] +body: + - type: markdown + attributes: + value: | + Thank you for submitting a RFC. Please add as many details as possible to help further enrich this design. + - type: input + id: relation + attributes: + label: Is this related to an existing feature request or issue? + description: Please share a link, if applicable + - type: dropdown + id: area + attributes: + label: Which Powertools for AWS Lambda (Java) utility does this relate to? + options: + - Tracer + - Logger + - Metrics + - Middleware factory + - Parameters + - Batch processing + - Typing + - Validation + - Event Source Data Classes + - Parser + - Idempotency + - Feature flags + - JMESPath functions + - Other + validations: + required: true + - type: textarea + id: summary + attributes: + label: Summary + description: Please provide an overview in one or two paragraphs + validations: + required: true + - type: textarea + id: problem + attributes: + label: Use case + description: Please share the use case and motivation behind this proposal + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + description: Please explain the design in detail, so anyone familiar with the project could implement it + placeholder: What the user experience looks like before and after this design? + validations: + required: true + - type: textarea + id: scope + attributes: + label: Out of scope + description: Please explain what should be considered out of scope in your proposal + validations: + required: true + - type: textarea + id: challenges + attributes: + label: Potential challenges + description: Nothing is perfect. Please share what common challenges, edge cases, unresolved areas, and suggestions on how to mitigate them + validations: + required: true + - type: textarea + id: integrations + attributes: + label: Dependencies and Integrations + description: If applicable, please share whether this feature has additional dependencies, and how it might integrate with other utilities available + validations: + required: false + - type: textarea + id: alternatives + attributes: + label: Alternative solutions + description: Please describe what alternative solutions to this use case, if any + render: markdown + validations: + required: false + - type: checkboxes + id: acknowledgment + attributes: + label: Acknowledgment + options: + - label: This feature request meets [Powertools for AWS Lambda (Java) Tenets](https://docs.powertools.aws.dev/lambda/Java/#tenets) + required: true + - label: Should this be considered in other Powertools for AWS Lambda languages? i.e. [Python](https://github.com/aws-powertools/powertools-lambda-python/), [TypeScript](https://github.com/aws-powertools/powertools-lambda-typescript/), and [.NET](https://github.com/aws-powertools/powertools-lambda-dotnet/) + required: false + - type: markdown + attributes: + value: | + --- + + **Disclaimer**: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful. + + Metadata information for admin purposes, please leave them empty. + + * RFC PR: + * Approved by: '' + * Reviewed by: '' \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/share_your_work.yml b/.github/ISSUE_TEMPLATE/share_your_work.yml index 228ee8281..01dae4fdf 100644 --- a/.github/ISSUE_TEMPLATE/share_your_work.yml +++ b/.github/ISSUE_TEMPLATE/share_your_work.yml @@ -53,4 +53,4 @@ body: label: Acknowledgment options: - label: I understand this content may be removed from Powertools for AWS Lambda (Java) documentation if it doesn't conform with the [Code of Conduct](https://aws.github.io/code-of-conduct) - required: true + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/support_powertools.yml b/.github/ISSUE_TEMPLATE/support_powertools.yml index 2b66d830d..8623c2b73 100644 --- a/.github/ISSUE_TEMPLATE/support_powertools.yml +++ b/.github/ISSUE_TEMPLATE/support_powertools.yml @@ -1,7 +1,7 @@ name: Support Powertools for AWS Lambda (Java) (become a reference) description: Add your organization's name or logo to the Powertools for AWS Lambda (Java) documentation title: "[Support Powertools for AWS Lambda (Java)]: " -labels: ["customer_reference"] +labels: ["customer-reference"] body: - type: markdown attributes: @@ -48,9 +48,9 @@ body: - type: checkboxes id: other_languages attributes: - label: Also using other Powertools for AWS Lambda (Java) languages? + label: Also using other Powertools for AWS Lambda languages? options: - - label: Python + - label: Java required: false - label: TypeScript required: false @@ -59,6 +59,6 @@ body: - type: markdown attributes: value: | - *By raising a Support Powertools for AWS Lambda (Java) issue, you are granting AWS permission to use your company's name (and/or logo) for the limited purpose described here. You are also confirming that you have authority to grant such permission.* + *By raising a Support Powertools for AWS Lambda (Python) issue, you are granting AWS permission to use your company's name (and/or logo) for the limited purpose described here. You are also confirming that you have authority to grant such permission.* *You can opt-out at any time by commenting or reopening this issue.* \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/tech_debt.yml b/.github/ISSUE_TEMPLATE/tech_debt.yml new file mode 100644 index 000000000..56cd4b8c7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/tech_debt.yml @@ -0,0 +1,60 @@ +name: Technical debt +description: Suggest an activity to help address technical debt. +title: "Tech debt: TITLE" +labels: ["tech-debt", "triage"] +body: + - type: markdown + attributes: + value: Thank you for taking the time to help us proactively improve delivery velocity, safely. + - type: textarea + id: importance + attributes: + label: Why is this needed? + description: Please help us understand the value so we can prioritize it accordingly + validations: + required: true + - type: dropdown + id: area + attributes: + label: Which area does this relate to? + multiple: true + options: + - Tests + - Static typing + - Tracer + - Logger + - Metrics + - Middleware factory + - Parameters + - Batch processing + - Validation + - Event Source Data Classes + - Parser + - Idempotency + - Feature flags + - JMESPath functions + - Streaming + - Automation + - Other + - type: textarea + id: suggestion + attributes: + label: Suggestion + description: If available, please share what a good solution would look like + validations: + required: false + - type: checkboxes + id: acknowledgment + attributes: + label: Acknowledgment + options: + - label: This request meets [Powertools for AWS Lambda (Python) Tenets](https://docs.powertools.aws.dev/lambda/python/latest/#tenets) + required: true + - label: Should this be considered in other Powertools for AWS Lambda languages? i.e. [Python](https://github.com/aws-powertools/powertools-lambda-python/), [TypeScript](https://github.com/aws-powertools/powertools-lambda-typescript/), and [.NET](https://github.com/aws-powertools/powertools-lambda-dotnet/) + required: false + - type: markdown + attributes: + value: | + --- + + **Disclaimer**: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful. \ No newline at end of file diff --git a/.github/actions/gradle/action.yml b/.github/actions/gradle/action.yml new file mode 100644 index 000000000..e69de29bb diff --git a/.github/actions/restore/action.yml b/.github/actions/restore/action.yml new file mode 100644 index 000000000..e69de29bb diff --git a/.github/actions/seal/action.yml b/.github/actions/seal/action.yml new file mode 100644 index 000000000..079496c8c --- /dev/null +++ b/.github/actions/seal/action.yml @@ -0,0 +1,78 @@ +name: Seal and hash source code +description: | + Seals and creates a SHA256SUM of an artifact for storage + + Process: + 1. Create a unique name based on environment details + 2. Compress work directory or specified path + 3. Hash compressed file + 4. Upload archive using `actions/upload-artifact` + + Usage: + ```yml + - id: seal + name: Seal + uses: .github/actions/seal + with: + prefix: foo + ``` + +inputs: + prefix: + description: Prefix to use when exporting artifact + required: true +outputs: + hash: + description: SHA256SUM hash of compressed files + value: ${{ steps.hash.outputs.hash }} + artifact_name: + description: Artifact name + value: ${{ steps.artifact_name.outputs.artifact_name }} + +runs: + using: composite + steps: + - id: adjust_path + name: Adjust path + shell: bash + run: echo "${{ github.action_path }}" >> $GITHUB_PATH + + - id: artifact_name + name: Export final artifact name + env: + GITHUB_RUN_ID: ${{ github.run_id }} + ARTIFACT_PREFIX: ${{ inputs.prefix }} + shell: bash + run: | + echo "artifact_name=${ARTIFACT_PREFIX}-${GITHUB_RUN_ID}" >> "$GITHUB_OUTPUT" + + - id: compress + name: Create tarball for entire source + env: + ARTIFACT_NAME: ${{ steps.artifact_name.outputs.artifact_name }} + shell: bash + run: | + tar --exclude-vcs -cvf "${ARTIFACT_NAME}".tar * + + - id: hash + name: Hash + env: + ARTIFACT_NAME: ${{ steps.artifact_name.outputs.artifact_name }} + shell: bash + run: | + echo "hash=$(openssl dgst -sha256 -binary "${{ ARTIFACT_NAME }}".tar | openssl enc -base64)" >> "$GITHUB_OUTPUT" + + - name: Upload artifacts + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + with: + if-no-files-found: error + name: ${{ steps.artifact_name.outputs.artifact_name }} + path: ${{ steps.artifact_name.outputs.artifact_name }}.tar + retention-days: 1 + + - name: Remove archive + env: + ARTIFACT_NAME: ${{ steps.artifact_name.outputs.artifact_name }} + shell: bash + run: | + rm -f "${ARTIFACT_NAME}.tar" \ No newline at end of file diff --git a/.github/actions/version/action.yml b/.github/actions/version/action.yml new file mode 100644 index 000000000..f0f0516ee --- /dev/null +++ b/.github/actions/version/action.yml @@ -0,0 +1,53 @@ +name: Version Java Project +description: | + Versions the maven project using an input + + Process: + 1. Grab current version from project.version variable from maven + 2. Set new version using maven-versions-plugin + + Usage: + ```yml + - id: version + name: version + uses: .github/actions/version + with: + new_version: 1.20.0 + snapshot: 'false' + ``` + +inputs: + new_version: + description: New package version, expressed as SemVer (1.x.y) + required: true + snapshot: + description: New version is a SNAPSHOT release + required: true + default: 'false' + +outputs: + old_version: + description: Current version of project + value: ${{ steps.current_version.outputs.current_version}} + +runs: + using: composite + steps: + - id: current_version + name: Get current version + shell: bash + run: | + echo "current_version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_OUTPUT + + - id: replace_version + name: Replace current version + shell: bash + run: | + mvn versions:set -DnewVersion=${{ inputs.new_version }} -DprocessAllModules=true -DallowSnapshots=true + + - id: asset_version + name: Replace version for assets + if: ${{ inputs.snapshot == 'false' }} + shell: bash + run: | + grep "${{ steps.current_version.outputs.current_version }}" -r . --include build.gradle --include build.gradle.kts --include mkdocs.yml --include README.md -l | xargs sed -i 's#${{ steps.current_version.outputs.current_version }}#${{ inputs.new_version }}#' \ No newline at end of file diff --git a/.github/branch_protection_settings/main.json b/.github/branch_protection_settings/main.json new file mode 100644 index 000000000..d283b3d5f --- /dev/null +++ b/.github/branch_protection_settings/main.json @@ -0,0 +1,53 @@ +{ + "url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/main/protection", + "required_status_checks": { + "url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/main/protection/required_status_checks", + "strict": true, + "contexts": [ + "SonarCloud" + ], + "contexts_url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/main/protection/required_status_checks/contexts", + "checks": [ + { + "context": "SonarCloud", + "app_id": 57789 + } + ] + }, + "required_pull_request_reviews": { + "url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/main/protection/required_pull_request_reviews", + "dismiss_stale_reviews": false, + "require_code_owner_reviews": false, + "require_last_push_approval": false, + "required_approving_review_count": 0 + }, + "required_signatures": { + "url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/main/protection/required_signatures", + "enabled": false + }, + "enforce_admins": { + "url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/main/protection/enforce_admins", + "enabled": true + }, + "required_linear_history": { + "enabled": false + }, + "allow_force_pushes": { + "enabled": false + }, + "allow_deletions": { + "enabled": false + }, + "block_creations": { + "enabled": false + }, + "required_conversation_resolution": { + "enabled": false + }, + "lock_branch": { + "enabled": false + }, + "allow_fork_syncing": { + "enabled": false + } +} diff --git a/.github/branch_protection_settings/v2.json b/.github/branch_protection_settings/v2.json new file mode 100644 index 000000000..fb9fdebcd --- /dev/null +++ b/.github/branch_protection_settings/v2.json @@ -0,0 +1,63 @@ +{ + "url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection", + "required_status_checks": { + "url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection/required_status_checks", + "strict": true, + "contexts": [], + "contexts_url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection/required_status_checks/contexts", + "checks": [] + }, + "restrictions": { + "url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection/restrictions", + "users_url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection/restrictions/users", + "teams_url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection/restrictions/teams", + "apps_url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection/restrictions/apps", + "users": [], + "teams": [], + "apps": [] + }, + "required_pull_request_reviews": { + "url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection/required_pull_request_reviews", + "dismiss_stale_reviews": true, + "require_code_owner_reviews": false, + "require_last_push_approval": true, + "required_approving_review_count": 1, + "dismissal_restrictions": { + "url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection/dismissal_restrictions", + "users_url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection/dismissal_restrictions/users", + "teams_url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection/dismissal_restrictions/teams", + "users": [], + "teams": [], + "apps": [] + } + }, + "required_signatures": { + "url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection/required_signatures", + "enabled": false + }, + "enforce_admins": { + "url": "https://api.github.com/repos/aws-powertools/powertools-lambda-java/branches/v2/protection/enforce_admins", + "enabled": false + }, + "required_linear_history": { + "enabled": true + }, + "allow_force_pushes": { + "enabled": false + }, + "allow_deletions": { + "enabled": false + }, + "block_creations": { + "enabled": true + }, + "required_conversation_resolution": { + "enabled": true + }, + "lock_branch": { + "enabled": false + }, + "allow_fork_syncing": { + "enabled": false + } +} diff --git a/.github/dependency-review-config.yml b/.github/dependency-review-config.yml new file mode 100644 index 000000000..6d737ee55 --- /dev/null +++ b/.github/dependency-review-config.yml @@ -0,0 +1,30 @@ +allow-licenses: + - 'Apache-1.1' + - 'Apache-2.0' + - 'ISC' + - 'MIT' + - 'MIT-0' + - 'MIT-CMU' + - 'MIT-enna' + - 'MIT-feh' + - 'MIT-Festival' + - 'MIT-Modern-Variant' + - 'MIT-open-group' + - 'MIT-testregex' + - 'MIT-Wu' + - 'BSD-1-Clause' + - 'BSD-2-Clause' + - 'BSD-2-Clause-Views' + - 'BSD-3-Clause' + - 'BSD-3-Clause-Attribution' + - 'BSD-3-Clause-Clear' + - 'BSD-3-Clause-flex' + - 'BSD-3-Clause-HP' + - 'BSD-3-Clause-LBNL' + - 'BSD-3-Clause-Modification' + - 'BSD-3-Clause-No-Military-License' + - 'BSD-3-Clause-No-Nuclear-License' + - 'BSD-3-Clause-No-Nuclear-License-2014' + - 'BSD-3-Clause-No-Nuclear-Warranty' + - 'BSD-3-Clause-Open-MPI' +comment-summary-in-pr: on-failure \ No newline at end of file diff --git a/.github/pmd-ruleset.xml b/.github/pmd-ruleset.xml new file mode 100644 index 000000000..b93fa19b8 --- /dev/null +++ b/.github/pmd-ruleset.xml @@ -0,0 +1,644 @@ + + + Rules to check Powertools for Lambda + + + + + + + + + + + + + 1 + + + 1 + + + 1 + + + 1 + + + + + + + + + + + + + 1 + + + + 1 + + + 1 + + + 1 + + + + 1 + + + + + + + 1 + + + + 1 + + + + 1 + + + 1 + + + 1 + + + 1 + + + + + + + + 1 + + + 1 + + + 1 + + + + + + + + 1 + + + + + + + 1 + + + + + 1 + + + 1 + + + + + 3 + + + + + + + + + + + + + + + + + + + + + + 1 + + + 1 + + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + + + + + + + + + 1 + + + + + + + + + + + + + + 1 + + + + + + 1 + + + + + + + + + + + + 1 + + + + 1 + + + 1 + + + 1 + + + + + + + 1 + + + + 1 + + + + + + + 1 + + + 1 + + + + 1 + + + + + + 1 + + + + + 1 + + + + + 1 + + + 1 + + + + + + + 1 + + + + + 1 + + + 1 + + + + 1 + + + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + + + + 1 + + + + + + 1 + + + + + 1 + + + + + + + 1 + + + 1 + + + 1 + + + + + + + + + + + + + 1 + + + + + + + 1 + + + 1 + + + + 1 + + + + 1 + + + + + + + 1 + + + + 1 + + + + 1 + + + 1 + + + + 1 + + + 1 + + + + 1 + + + + 1 + + + + 1 + + + 1 + + + + + + + 1 + + + + 1 + + + + + 1 + + + 1 + + + + + 1 + + + 1 + + + + + + 1 + + + 1 + + + + 1 + + + 1 + + + 1 + + + 1 + + + + 1 + + + + + + + 1 + + + + 1 + + + + + + + + 1 + + + + + + + 1 + + + + + + + + + + + + + + + + + replace o.getClass().equals(MyClass.class) with o instanceof MyClass. Make sure MyClass doesn't have descendants + 1 + + + + + + + + + + + replace MyClass.class.equals(o.getClass()) with o instanceof MyClass. Make sure MyClass doesn't have descendants + 3 + + + + + + + + + + + Calling super.visit breaks the rulechain, by starting a full visitor run from the passed node downwards. Add all needed nodes to the rulechain instead. + 1 + + + + + + + + + + + Just returning without calling super stops visiting of nested nodes like inner classes. + 3 + + + + + + + + + + + + Share the invocation matcher and not create a new one every time + 1 + + + + + + + + + + + Use slf4j: LoggerFactory.getLogger(MyClass.class) + 1 + + + + + + + + + \ No newline at end of file diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml deleted file mode 100644 index 5401eedc9..000000000 --- a/.github/workflows/auto-merge.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Auto merge if dependabot PR - -on: - workflow_run: - workflows: ["Build"] - types: [completed] - -permissions: - pull-requests: write - issues: write - repository-projects: write - contents: write - -jobs: - merge-me: - name: Merge me! - runs-on: ubuntu-latest - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' && github.actor == 'dependabot[bot]' - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - uses: ahmadnassri/action-workflow-run-wait@2aa3d9e1a12ecaaa9908e368eaf2123bb084323e # v1.4.4 - with: - timeout: 300000 - - name: 'Download artifact' - uses: actions/github-script@47f7cf65b5ced0830a325f705cad64f2f58dddf7 # v3.1.0 - with: - script: | - var artifacts = await github.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: ${{github.event.workflow_run.id }}, - }); - var matchArtifact = artifacts.data.artifacts.filter((artifact) => { - return artifact.name == "pr" - })[0]; - var download = await github.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - var fs = require('fs'); - fs.writeFileSync('${{github.workspace}}/pr.zip', Buffer.from(download.data)); - - run: unzip pr.zip - - name: Create review - uses: actions/github-script@47f7cf65b5ced0830a325f705cad64f2f58dddf7 # v3.1.0 - with: - script: | - var fs = require('fs'); - var issue_number = Number(fs.readFileSync('./NR')); - - github.pulls.createReview({ - owner: context.payload.repository.owner.login, - repo: context.payload.repository.name, - pull_number: issue_number, - event: 'APPROVE' - }) - - github.pulls.merge({ - owner: context.payload.repository.owner.login, - repo: context.payload.repository.name, - pull_number: issue_number, - merge_method: 'squash' - }) - - github-token: ${{ secrets.AUTOMERGE }} diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index a4ab6e7de..d4bf75a9d 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -1,38 +1,62 @@ -name: Build Docs +# Build Docs +# +# Description: +# Builds the docs and stores them in S3 to be served by our docs platform +# +# The workflow allows us to build to the main location (/lambda/java/) and to an alias +# (i.e. /lambda/java/preview/) if needed +# +# Triggers: +# - workflow_dispatch +# +# Inputs: +# alias – subdirectory to store the docs in for previews or in progress work on: - pull_request: - branches: - - main - - v2 - paths: - - 'docs/**' - - 'mkdocs.yml' - - 'Makefile' + workflow_dispatch: + inputs: + alias: + type: string + required: false + description: | + Alias to deploy the documentation into, this is mostly for testing pre-release + versions of the documentation, such as beta versions or snapshots. + + https://docs.powertools.aws.dev/lambda/java/ - push: - branches: - - main - paths: - - 'docs/**' - - 'mkdocs.yml' - - 'Makefile' +name: Build Docs +run-name: Build Docs - ${{ contains(github.head_ref, 'main') && 'main' || inputs.alias }} jobs: docs: runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + environment: Docs steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Set up Python - uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1 - with: - python-version: "3.8" - - name: Capture branch and tag - id: branch_name + - name: Sanity Check + if: ${{ github.head_ref != 'main' || inputs.alias == '' }} + run: + echo "::error::No buildable docs" + + - name: Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + fetch-depth: 0 + - name: Build run: | - echo "SOURCE_BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV - echo "SOURCE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - - name: Build docs website + mkdir -p dist + docker build -t squidfunk/mkdocs-material ./docs/ + docker run --rm -t -v ${PWD}:/docs squidfunk/mkdocs-material build + cp -R site/* dist/ + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 + with: + aws-region: us-east-1 + role-to-assume: ${{ secrets.AWS_DOCS_ROLE_ARN }} + - name: Deploy run: | - echo "GIT_PYTHON_REFRESH=quiet" - make build-docs-website \ No newline at end of file + aws s3 sync \ + dist \ + s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/${{ github.head_ref == 'main' && '' || format('{0}/', inputs.alias )}} \ No newline at end of file diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml new file mode 100644 index 000000000..cc5931d05 --- /dev/null +++ b/.github/workflows/check-build.yml @@ -0,0 +1,109 @@ +# Check Build +# +# Description: +# Runs the build for every java version we support +# +# Triggers: +# - pull_request: when a PR is sent to us +# - push: when code is pushed to a specified branch +# +# Notes: +# The matrix build for this workflow is unusual, we need to make it dyanmic since +# we need to change java versions we build for depending on the branch. + + +on: + workflow_dispatch: + pull_request: + paths: + - 'powertools-batch/**' + - 'powertools-core/**' + - 'powertools-cloudformation/**' + - 'powertools-common/**' + - 'powertools-e2e-tests/**' + - 'powertools-idempotency/**' + - 'powertools-large-messages/**' + - 'powertools-logging/**' + - 'powertools-metrics/**' + - 'powertools-parameters/**' + - 'powertools-serialization/**' + - 'powertools-sqs/**' + - 'powertools-tracing/**' + - 'powertools-tracing/**' + - 'powertools-validation/**' + - 'examples/**' + - 'pom.xml' + - 'examples/pom.xml' + - '.github/workflows/**' + push: + branches: + - main + - v2 + paths: # add other modules when there are under e2e tests + - 'powertools-batch/**' + - 'powertools-core/**' + - 'powertools-cloudformation/**' + - 'powertools-common/**' + - 'powertools-e2e-tests/**' + - 'powertools-idempotency/**' + - 'powertools-large-messages/**' + - 'powertools-logging/**' + - 'powertools-metrics/**' + - 'powertools-parameters/**' + - 'powertools-serialization/**' + - 'powertools-sqs/**' + - 'powertools-tracing/**' + - 'powertools-tracing/**' + - 'powertools-validation/**' + - 'pom.xml' + - 'examples/**' + - 'examples/pom.xml' + - '.github/workflows/**' + +name: Build +run-name: Build - ${{ github.event_name }} + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + build_matrix: ${{ format('{0}{1}', steps.build_matrix_v1.outputs.build_matrix, steps.build_matrix_v1.outputs.build_matrix) }} + steps: + - id: base + name: Base + run: | + echo build_version=$(test ${{ github.ref }} == "v2" && echo "v2" || echo "v1") >> $GITHUB_OUTPUT + - id: build_matrix_v1 + name: Build matrix (v1) + if: ${{ steps.base.outputs.build_version == 'v1' }} + run: | + echo build_matrix='["8", "11", "17", "21"]' >> "$GITHUB_OUTPUT" + - id: build_matrix_v2 + name: Build matrix (v2) + if: ${{ steps.base.outputs.build_version == 'v2' }} + run: | + echo build_matrix='["11", "17", "21"]'>> "$GITHUB_OUTPUT" + build: + runs-on: ubuntu-latest + strategy: + matrix: + java: + - 8 + - 11 + - 17 + - 21 + steps: + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Setup Java + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 + with: + distribution: corretto + java-version: ${{ matrix.java }} + cache: maven + - id: build-maven + name: Build (Maven) + if: ${{ matrix.java != '8' }} + run: | + mvn -B install --file pom.xml \ No newline at end of file diff --git a/.github/workflows/check-e2e.yml b/.github/workflows/check-e2e.yml new file mode 100644 index 000000000..14eab5394 --- /dev/null +++ b/.github/workflows/check-e2e.yml @@ -0,0 +1,74 @@ +# Run E2E tests for a branch +# +# Description: +# Runs E2E tests for a specified branch +# +# Triggers: +# - push +# +# Secrets: +# - E2E.AWS_IAM_ROLE + +on: + workflow_dispatch: + + push: + branches: + - main + - v2 + paths: # add other modules when there are under e2e tests + - 'powertools-batch/**' + - 'powertools-core/**' + - 'powertools-cloudformation/**' + - 'powertools-common/**' + - 'powertools-e2e-tests/**' + - 'powertools-idempotency/**' + - 'powertools-large-messages/**' + - 'powertools-logging/**' + - 'powertools-metrics/**' + - 'powertools-parameters/**' + - 'powertools-serialization/**' + - 'powertools-sqs/**' + - 'powertools-tracing/**' + - 'powertools-tracing/**' + - 'powertools-validation/**' + - 'pom.xml' + +name: E2E Tests +run-name: E2E Tests - ${{ github.event_name }} + +permissions: + contents: read + +jobs: + e2e: + name: End-to-end Tests (Java ${{ matrix.java }}) + runs-on: ubuntu-latest + permissions: + id-token: write + environment: E2E + strategy: + max-parallel: 3 + matrix: + java: + - 11 + - 17 + - 21 + + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Setup java + uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0 + with: + distribution: 'corretto' + java-version: ${{ matrix.java }} + cache: maven + - name: Setup AWS credentials + uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-east-1 + - name: Run e2e test with Maven + env: + JAVA_VERSION: ${{ matrix.java }} + run: mvn -DskipTests install --file pom.xml && mvn -Pe2e -B verify --file powertools-e2e-tests/pom.xml diff --git a/.github/workflows/check-pmd.yml b/.github/workflows/check-pmd.yml new file mode 100644 index 000000000..d97698af8 --- /dev/null +++ b/.github/workflows/check-pmd.yml @@ -0,0 +1,42 @@ +# Runs PMD for a Pull Request +# +# Description: +# Runs PMD (pmd.github.io) for a pull request and daily. +# This does not error on failure yet, our rules are too strong and would fail on every run +# +# Triggers: +# - pull_request +# - workflow_dispatch +# - cron: every day at 12:00PM + +on: + pull_request: + workflow_dispatch: + schedule: + - cron: '0 12 * * *' # Run daily at 12:00 UTC + +name: PMD +run-name: PMD - ${{ github.event_name }} + +permissions: + contents: read + +jobs: + pmd_analyse: + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + steps: + - name: Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Setup Java + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + with: + java-version: 21 + distribution: corretto + cache: maven + - uses: pmd/pmd-github-action@d9c1f3c5940cbf5923f1354e83fa858b4496ebaa # v2.0.0 + with: + rulesets: '.github/pmd-ruleset.xml' + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/spotbugs.yml b/.github/workflows/check-spotbugs.yml similarity index 55% rename from .github/workflows/spotbugs.yml rename to .github/workflows/check-spotbugs.yml index d314107fa..0749dfaa0 100644 --- a/.github/workflows/spotbugs.yml +++ b/.github/workflows/check-spotbugs.yml @@ -1,42 +1,50 @@ -name: SpotBugs - +# Check for Spotbug errors +# +# Description: +# Runs Spotbugs for a pull request. +# This does not error on failure yet, our rules are too strong and would fail on every run +# +# Triggers: +# - pull_request on: pull_request: branches: - - main - v2 paths: - - 'powertools-cloudformation/**' + - 'powertools-batch/**' - 'powertools-core/**' - - 'powertools-serialization/**' + - 'powertools-cloudformation/**' + - 'powertools-common/**' + - 'powertools-e2e-tests/**' + - 'powertools-idempotency/**' + - 'powertools-large-messages/**' - 'powertools-logging/**' + - 'powertools-metrics/**' + - 'powertools-parameters/**' + - 'powertools-serialization/**' - 'powertools-sqs/**' - 'powertools-tracing/**' + - 'powertools-tracing/**' - 'powertools-validation/**' - - 'powertools-parameters/**' - - 'powertools-idempotency/**' - - 'powertools-metrics/**' - 'powertools-test-suite/**' - 'pom.xml' - '.github/workflows/**' + +name: SpotBugs +run-name: SpotBugs + +permissions: + contents: read + jobs: codecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Setup java JDK 1.8 + - name: Setup Java uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0 with: distribution: 'corretto' - java-version: 8 - # https://github.com/jwgmeligmeyling/spotbugs-github-action/issues/6 - # https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/ - # Avoid complexity of git action with publishing report. Just build with spotbugs profile. -# - name: Build with Maven for spotbugs check to gather reports -# run: mvn -Pbuild-with-spotbugs -B install --file pom.xml -DskipTests -Dmaven.javadoc.skip=true -Dspotbugs.failOnError=false -# - uses: jwgmeligmeyling/spotbugs-github-action@master -# with: -# path: '**/spotbugsXml.xml' -# # Can be simplified post this issue is fixed https://github.com/jwgmeligmeyling/spotbugs-github-action/issues/9 + java-version: 21 - name: Build with Maven for spotbugs check to mark build as fail if voilations found run: mvn -Pbuild-with-spotbugs -B install --file pom.xml -DskipTests -Dmaven.javadoc.skip=true -Dspotbugs.failOnError=true \ No newline at end of file diff --git a/.github/workflows/dispatch_analytics.yml b/.github/workflows/dispatch_analytics.yml deleted file mode 100644 index c93cb5b36..000000000 --- a/.github/workflows/dispatch_analytics.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Dispatch analytics - -on: - workflow_dispatch: - - schedule: - - cron: '0 * * * *' - -permissions: - id-token: write - actions: read - checks: read - contents: read - deployments: read - issues: read - discussions: read - packages: read - pages: read - pull-requests: read - repository-projects: read - security-events: read - statuses: read - -jobs: - dispatch_token: - concurrency: - group: analytics - runs-on: ubuntu-latest - environment: analytics - steps: - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 - with: - aws-region: eu-central-1 - role-to-assume: ${{ secrets.AWS_ANALYTICS_ROLE_ARN }} - - - name: Invoke Lambda function - run: | - payload=$(echo -n '{"githubToken": "${{ secrets.GITHUB_TOKEN }}"}' | base64) - aws lambda invoke \ - --function-name ${{ secrets.AWS_ANALYTICS_DISPATCHER_ARN }} \ - --payload "$payload" response.json - cat response.json diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 5e37c5f45..000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Docs - -on: - release: - types: - - published - workflow_dispatch: {} - -permissions: - id-token: write - contents: write - pages: write - -jobs: - docs: - runs-on: ubuntu-latest - environment: Docs - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Set up Python - uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1 - with: - python-version: "3.8" - - name: Capture branch and tag - id: branch_name - run: | - echo "SOURCE_BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV - echo "SOURCE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - - name: Build docs website - run: | - make build-docs-website - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@e1e17a757e536f70e52b5a12b2e8d1d1c60e04ef - with: - aws-region: us-east-1 - role-to-assume: ${{ secrets.AWS_DOCS_ROLE_ARN }} - - name: Deploy Docs - run: | - aws s3 sync \ - dist \ - s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/ diff --git a/.github/workflows/post_release.js b/.github/workflows/post_release.js deleted file mode 100644 index 648236421..000000000 --- a/.github/workflows/post_release.js +++ /dev/null @@ -1,112 +0,0 @@ -const STAGED_LABEL = "status/staged-next-release"; - -/** - * Fetch issues using GitHub REST API - * - * @param {object} gh_client - Pre-authenticated REST client (Octokit) - * @param {string} org - GitHub Organization - * @param {string} repository - GitHub repository - * @param {string} state - GitHub issue state (open, closed) - * @param {string} label - Comma-separated issue labels to fetch - * @return {Object[]} issues - Array of issues matching params - * @see {@link https://octokit.github.io/rest.js/v18#usage|Octokit client} - */ -const fetchIssues = async ({ - gh_client, - org, - repository, - state = "open", - label = STAGED_LABEL, - }) => { - - try { - const { data: issues } = await gh_client.rest.issues.listForRepo({ - owner: org, - repo: repository, - state: state, - labels: label, - }); - - return issues; - - } catch (error) { - console.error(error); - throw new Error("Failed to fetch issues") - } - -}; - -/** - * Notify new release and close staged GitHub issue - * - * @param {object} gh_client - Pre-authenticated REST client (Octokit) - * @param {string} owner - GitHub Organization - * @param {string} repository - GitHub repository - * @param {string} release_version - GitHub Release version - * @see {@link https://octokit.github.io/rest.js/v18#usage|Octokit client} - */ -const notifyRelease = async ({ - gh_client, - owner, - repository, - release_version, - }) => { - const release_url = `https://github.com/${owner}/${repository}/releases/tag/v${release_version}`; - - const issues = await fetchIssues({ - gh_client: gh_client, - org: owner, - repository: repository, - }); - - issues.forEach(async (issue) => { - console.info(`Updating issue number ${issue.number}`); - - const comment = `This is now released under [${release_version}](${release_url}) version!`; - try { - await gh_client.rest.issues.createComment({ - owner: owner, - repo: repository, - body: comment, - issue_number: issue.number, - }); - } catch (error) { - console.error(error); - throw new Error(`Failed to update issue ${issue.number} about ${release_version} release`) - } - - - // Close issue and remove staged label; keep existing ones - const labels = issue.labels - .filter((label) => label.name != STAGED_LABEL) - .map((label) => label.name); - - try { - await gh_client.rest.issues.update({ - repo: repository, - owner: owner, - issue_number: issue.number, - state: "closed", - labels: labels, - }); - } catch (error) { - console.error(error); - throw new Error("Failed to close issue") - } - - console.info(`Issue number ${issue.number} closed and updated`); - }); -}; - -// context: https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts -module.exports = async ({ github, context }) => { - const { RELEASE_TAG_VERSION } = process.env; - console.log(`Running post-release script for ${RELEASE_TAG_VERSION} version`); - - await notifyRelease({ - gh_client: github, - owner: context.repo.owner, - repository: context.repo.repo, - release_version: RELEASE_TAG_VERSION, - }); -}; \ No newline at end of file diff --git a/.github/workflows/pr_artifacts_size.yml b/.github/workflows/pr_artifacts_size.yml deleted file mode 100644 index ab9ca9859..000000000 --- a/.github/workflows/pr_artifacts_size.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Artifacts Size - -on: - pull_request: - branches: - - main - - v2 - paths: - - 'powertools-batch/**' - - 'powertools-cloudformation/**' - - 'powertools-core/**' # not in v2 - - 'powertools-common/**' # v2 only - - 'powertools-e2e-tests/**' - - 'powertools-idempotency/**' - - 'powertools-large-messages/**' - - 'powertools-logging/**' - - 'powertools-metrics/**' - - 'powertools-parameters/**' - - 'powertools-serialization/**' - - 'powertools-sqs/**' # not in v2 - - 'powertools-test-suite/**' # not in v2 - - 'powertools-tracing/**' - - 'powertools-validation/**' - - 'pom.xml' - - '.github/workflows/pr_artifacts_size.yml' -jobs: - codecheck: - runs-on: ubuntu-latest - permissions: - pull-requests: write - issues: read - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Setup java JDK 11 - uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0 - with: - distribution: 'corretto' - java-version: 11 - - name: Build with Maven - run: mvn clean package --file pom.xml -DskipTests artifact:buildinfo - - name: Get artifacts size & build report - id: artifacts-size-report - run: | - echo '## :floppy_disk: Artifacts Size Report' > report.md - echo '| Module | Version | Size (KB) |' >> report.md - echo '| --- | --- | --- |' >> report.md - artifact_version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) - for artifact in $(cat target/powertools-parent-*.buildinfo | grep 'outputs.*.jar' | grep -v 'sources.jar'); do - artifact_name=$(echo "$artifact" | cut -d '=' -f2) - artifact_name=${artifact_name%-$artifact_version.jar} - artifact_size=$(grep "${artifact%%.filename*}.length" target/powertools-parent-*.buildinfo | cut -d '=' -f2) - printf "| %s | %s | %.2f |\n" "$artifact_name" "$artifact_version" "$(bc <<< "scale=2; $artifact_size/1000")" >> report.md - done - - name: Find potential existing report - uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # 2.4.0 - id: find-comment - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' - body-includes: Artifacts Size Report - - name: Write artifacts size report in comment - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # 3.0.2 - with: - comment-id: ${{ steps.find-comment.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} - body-path: 'report.md' - edit-mode: replace diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml deleted file mode 100644 index 634a4ee0f..000000000 --- a/.github/workflows/pr_build.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: Build - -on: - pull_request: - branches: - - main - paths: - - 'powertools-batch/**' - - 'powertools-cloudformation/**' - - 'powertools-core/**' - - 'powertools-e2e-tests/**' - - 'powertools-idempotency/**' - - 'powertools-large-messages/**' - - 'powertools-logging/**' - - 'powertools-metrics/**' - - 'powertools-parameters/**' - - 'powertools-serialization/**' - - 'powertools-sqs/**' - - 'powertools-test-suite/**' - - 'powertools-tracing/**' - - 'powertools-validation/**' - - 'examples/**' - - 'pom.xml' - - 'examples/pom.xml' - - '.github/workflows/**' - push: - branches: - - main - paths: - - 'powertools-batch/**' - - 'powertools-cloudformation/**' - - 'powertools-core/**' - - 'powertools-e2e-tests/**' - - 'powertools-idempotency/**' - - 'powertools-large-messages/**' - - 'powertools-logging/**' - - 'powertools-metrics/**' - - 'powertools-parameters/**' - - 'powertools-serialization/**' - - 'powertools-sqs/**' - - 'powertools-test-suite/**' - - 'powertools-tracing/**' - - 'powertools-validation/**' - - 'examples/**' - - 'pom.xml' - - 'examples/pom.xml' - - '.github/workflows/**' -jobs: - build-corretto: - runs-on: ubuntu-latest - strategy: - max-parallel: 5 - matrix: - java: [8, 11, 17, 21] - name: Java ${{ matrix.java }} - env: - JAVA: ${{ matrix.java }} - AWS_REGION: eu-west-1 - permissions: - id-token: write # needed to interact with GitHub's OIDC Token endpoint. - contents: read - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Setup java - uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0 - with: - distribution: 'corretto' - java-version: ${{ matrix.java }} - cache: 'maven' - - name: Build with Maven - run: mvn -B install --file pom.xml - - name: Build Gradle Example - Java - if: ${{ matrix.java == '8' }} # Gradle example can only be built on Java 8 - working-directory: examples/powertools-examples-core/gradle - run: ./gradlew build - - name: Build Gradle Example - Kotlin - if: ${{ matrix.java == '8' }} # Gradle example can only be built on Java 8 - working-directory: examples/powertools-examples-core/kotlin - run: ./gradlew build - - name: Upload coverage to Codecov - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 - if: ${{ matrix.java == '11' }} # publish results once - with: - files: ./powertools-cloudformation/target/site/jacoco/jacoco.xml,./powertools-core/target/site/jacoco/jacoco.xml,./powertools-idempotency/target/site/jacoco/jacoco.xml,./powertools-logging/target/site/jacoco/jacoco.xml,./powertools-metrics/target/site/jacoco/jacoco.xml,./powertools-parameters/target/site/jacoco/jacoco.xml,./powertools-serialization/target/site/jacoco/jacoco.xml,./powertools-sqs/target/site/jacoco/jacoco.xml,./powertools-tracing/target/site/jacoco/jacoco.xml,./powertools-validation/target/site/jacoco/jacoco.xml,./powertools-large-messages/target/site/jacoco/jacoco.xml,./powertools-batch/target/site/jacoco/jacoco.xml - savepr: - runs-on: ubuntu-latest - name: Save PR number if running on PR by dependabot - if: github.actor == 'dependabot[bot]' - steps: - - name: Create Directory and save issue - run: | - mkdir -p ./pr - echo ${{ github.event.number }} - echo ${{ github.event.number }} > ./pr/NR - - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - name: Upload artifact - with: - name: pr - path: pr/ diff --git a/.github/workflows/pr_build_v2.yml b/.github/workflows/pr_build_v2.yml deleted file mode 100644 index 3299dc720..000000000 --- a/.github/workflows/pr_build_v2.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Build - -on: - pull_request: - branches: - - v2 - paths: - - 'powertools-batch/**' - - 'powertools-cloudformation/**' - - 'powertools-common/**' - - 'powertools-e2e-tests/**' - - 'powertools-idempotency/**' - - 'powertools-large-messages/**' - - 'powertools-logging/**' - - 'powertools-metrics/**' - - 'powertools-parameters/**' - - 'powertools-serialization/**' - - 'powertools-tracing/**' - - 'powertools-validation/**' - - 'examples/**' - - 'pom.xml' - - 'examples/pom.xml' - - '.github/workflows/**' - push: - branches: - - v2 - paths: - - 'powertools-batch/**' - - 'powertools-cloudformation/**' - - 'powertools-common/**' - - 'powertools-e2e-tests/**' - - 'powertools-idempotency/**' - - 'powertools-large-messages/**' - - 'powertools-logging/**' - - 'powertools-metrics/**' - - 'powertools-parameters/**' - - 'powertools-serialization/**' - - 'powertools-tracing/**' - - 'powertools-validation/**' - - 'examples/**' - - 'pom.xml' - - 'examples/pom.xml' - - '.github/workflows/**' -jobs: - build-corretto: - runs-on: ubuntu-latest - strategy: - max-parallel: 5 - matrix: - java: [11, 17, 21] - name: Java ${{ matrix.java }} - env: - JAVA: ${{ matrix.java }} - AWS_REGION: eu-west-1 - permissions: - id-token: write # needed to interact with GitHub's OIDC Token endpoint. - contents: read - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Setup java - uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0 - with: - distribution: 'corretto' - java-version: ${{ matrix.java }} - cache: 'maven' - - name: Build with Maven - run: mvn -B install --file pom.xml - - name: Build Gradle Example - Java - working-directory: examples/powertools-examples-core/gradle - run: ./gradlew build - - name: Build Gradle Example - Kotlin - working-directory: examples/powertools-examples-core/kotlin - run: ./gradlew build - - name: Upload coverage to Codecov - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 - if: ${{ matrix.java == '11' }} # publish results once - with: - files: ./powertools-cloudformation/target/site/jacoco/jacoco.xml,./powertools-core/target/site/jacoco/jacoco.xml,./powertools-idempotency/target/site/jacoco/jacoco.xml,./powertools-logging/target/site/jacoco/jacoco.xml,./powertools-metrics/target/site/jacoco/jacoco.xml,./powertools-parameters/target/site/jacoco/jacoco.xml,./powertools-serialization/target/site/jacoco/jacoco.xml,./powertools-sqs/target/site/jacoco/jacoco.xml,./powertools-tracing/target/site/jacoco/jacoco.xml,./powertools-validation/target/site/jacoco/jacoco.xml,./powertools-large-messages/target/site/jacoco/jacoco.xml,./powertools-batch/target/site/jacoco/jacoco.xml - savepr: - runs-on: ubuntu-latest - name: Save PR number if running on PR by dependabot - if: github.actor == 'dependabot[bot]' - steps: - - name: Create Directory and save issue - run: | - mkdir -p ./pr - echo ${{ github.event.number }} - echo ${{ github.event.number }} > ./pr/NR - - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - name: Upload artifact - with: - name: pr - path: pr/ diff --git a/.github/workflows/pr_iac_lint.yml b/.github/workflows/pr_iac_lint.yml deleted file mode 100644 index c6e17ab1c..000000000 --- a/.github/workflows/pr_iac_lint.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Validate IaC - -on: - push: - branches: - - main - - v2 - pull_request: - branches: - - main - - v2 - paths: - - 'examples/**' -jobs: - linter: - runs-on: ubuntu-latest - strategy: - matrix: - project: ["sam", "gradle", "kotlin"] - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Setup java JDK - uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0 - with: - distribution: 'corretto' - java-version: 11 - - name: Build Project - working-directory: . - run: | - mvn install -DskipTests - - name: Run SAM validator to check syntax of IaC templates - Java - working-directory: examples/powertools-examples-core/${{ matrix.project }} - run: | - sam build - sam validate --lint - - name: Setup Terraform - uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 #v2.0.3 - - name: Run Terraform validator to check syntax of IaC templates and produce a plan of changes - working-directory: examples/powertools-examples-core/terraform - run: | - mvn install - terraform -version - terraform init -backend=false - terraform validate - - name: Setup Terraform lint - uses: terraform-linters/setup-tflint@a5a1af8c6551fb10c53f1cd4ba62359f1973746f # v3.1.1 - - name: Run Terraform lint to check for best practices, errors, deprecated syntax etc. - working-directory: examples/powertools-examples-core/terraform - run: | - tflint --version - tflint --init - tflint -f compact \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 03f04e0f4..000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Publish package to the Maven Central Repository -on: - release: - types: - - published - workflow_dispatch: {} -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Set up Maven Central Repository - uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0 - with: - distribution: 'corretto' - java-version: 8 - server-id: ossrh - server-username: MAVEN_USERNAME - server-password: MAVEN_PASSWORD - # TODO: use environments https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment - gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }} # Value of the GPG private key to import - gpg-passphrase: GPG_PASSPHRASE # env variable for GPG private key passphrase - - name: Set release notes tag - run: | - RELEASE_TAG_VERSION=${{ github.event.release.tag_name }} - echo "RELEASE_TAG_VERSION=${RELEASE_TAG_VERSION:1}" >> $GITHUB_ENV - - name: Publish package - run: mvn -Prelease clean deploy -DskipTests - env: - MAVEN_USERNAME: ${{ secrets.OSSRH_JIRA_USERNAME }} - MAVEN_PASSWORD: ${{ secrets.OSSRH_JIRA_PASSWORD }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - - name: Close issues related to this release - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 - with: - script: | - const post_release = require('.github/workflows/post_release.js') - await post_release({github, context, core}) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 72bd5c24f..f727ee25d 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -1,15 +1,32 @@ -name: Release Drafter +# Generates release notes +# +# Description: +# Generates release notes based on pull request history. This is based on the config +# stored in .github/release-drafter.yml +# +# Triggers: +# - push: main on: push: - # branches to consider in the event; optional, defaults to all - branches: - - main + branches: [ main ] + +name: Release Drafter +run-name: Release Drafter jobs: +<<<<<<< HEAD update_release_draft: runs-on: ubuntu-latest +======= + update_release: + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write +>>>>>>> 4a17172a (chore(automation): Update automation workflows (#1779)) steps: - - uses: release-drafter/release-drafter@569eb7ee3a85817ab916c8f8ff03a5bd96c9c83e # v5.23.0 + - name: Relase Drafter + uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release-prep.yml b/.github/workflows/release-prep.yml deleted file mode 100644 index f7a3c74c0..000000000 --- a/.github/workflows/release-prep.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Prepare for maven central release -on: - workflow_dispatch: - inputs: - targetRelease: - description: 'Release number to upgrade to. For example X.X.X. Follow Semantic Versioning when deciding on next version.' - required: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Get current date - id: date - run: echo "::set-output name=date::$(date +'%Y-%m-%d')" - - name: Set current release version env variable - run: | - echo "CURRENT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV - - name: Find and Replace ${{ env.CURRENT_VERSION }} with ${{ github.event.inputs.targetRelease }} in mkdocs.yml - uses: jacobtomlinson/gha-find-replace@f485fdc3f67a6d87ae6e3d11e41f648c26d7aee3 # v2.0.0 - with: - find: 'version: ${{ env.CURRENT_VERSION }}' - replace: 'version: ${{ github.event.inputs.targetRelease }}' - regex: false - include: "mkdocs.yml" - - name: Find and Replace ${{ env.CURRENT_VERSION }} with ${{ github.event.inputs.targetRelease }} in main pom.xml - uses: jacobtomlinson/gha-find-replace@f485fdc3f67a6d87ae6e3d11e41f648c26d7aee3 # v2.0.0 - with: - find: ${{ env.CURRENT_VERSION }} - replace: ${{ github.event.inputs.targetRelease }} - regex: false - include: "pom.xml" - - name: Find and Replace ${{ env.CURRENT_VERSION }} with ${{ github.event.inputs.targetRelease }} in modules pom.xml - uses: jacobtomlinson/gha-find-replace@f485fdc3f67a6d87ae6e3d11e41f648c26d7aee3 # v2.0.0 - with: - find: ${{ env.CURRENT_VERSION }} - replace: ${{ github.event.inputs.targetRelease }} - regex: false - include: "**/*pom.xml" - - name: Find and Replace ${{ env.CURRENT_VERSION }} with ${{ github.event.inputs.targetRelease }} in build.gradle - uses: jacobtomlinson/gha-find-replace@f485fdc3f67a6d87ae6e3d11e41f648c26d7aee3 # v2.0.0 - with: - find: ${{ env.CURRENT_VERSION }} - replace: ${{ github.event.inputs.targetRelease }} - regex: false - include: "**/*build.gradle" - - name: Find and Replace ${{ env.CURRENT_VERSION }} with ${{ github.event.inputs.targetRelease }} in README.md - uses: jacobtomlinson/gha-find-replace@f485fdc3f67a6d87ae6e3d11e41f648c26d7aee3 # v2.0.0 - with: - find: ${{ env.CURRENT_VERSION }} - replace: ${{ github.event.inputs.targetRelease }} - regex: false - include: "README.md" - - name: Create changelog placeholder for ${{ github.event.inputs.targetRelease }} - uses: jacobtomlinson/gha-find-replace@f485fdc3f67a6d87ae6e3d11e41f648c26d7aee3 # v2.0.0 - with: - find: '## [Unreleased]' - replace: | - ## [Unreleased] - - ## [${{ github.event.inputs.targetRelease }}] - ${{ steps.date.outputs.date }} - - - - regex: false - include: CHANGELOG.md - - name: Create Release Pull Request - uses: peter-evans/create-pull-request@18f7dc018cc2cd597073088f7c7591b9d1c02672 # v3.14.0 - with: - commit-message: chore:prep release ${{ github.event.inputs.targetRelease }} - token: ${{ secrets.RELEASE }} - signoff: false - branch: prep-release-${{ github.event.inputs.targetRelease }} - delete-branch: true - title: chore:Prep release ${{ github.event.inputs.targetRelease }} - body: | - This is automated release prep. Remember to update [CHANGELOG.md](https://github.com/aws-powertools/powertools-lambda-java/blob/prep-release-${{ github.event.inputs.targetRelease }}/CHANGELOG.md) to capture changes in this release. Please review changes carefully before merging. - - * [ ] Updated CHANGELOG.md \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..54924b558 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,291 @@ +# Release +# +# Description: +# Creates a release for the project +# +# 1. Runs a setup job to set needed variables (build_matrix & version) +# 2. Versions to the project and stores as an artifact +# 3. Run quality checks +# 4. Build +# 5. Publish to Maven Central +# 6. Create PR +# 7. Publish docs +# +# Inputs: +# - version (string): SemVer of the new release (X.Y.Z) +# - snapshot (bool): If it's a snapshot release, this skips versioning assets like docs +# - skip_checks (bool): Don't run quality checks if it's an emergency release +# - skip_publish (bool): Don't publish to maven central +# - continue_on_error (bool): Don't fail the workflow if a quality check fails +# +# Triggers: +# - workflow_dispatch +# +# Secrets: +# - RELEASE.GPG_SIGNING_KEY +# - RELEASE.OSSRH_JIRA_USERNAME +# - RELEASE.OSSRH_JIRA_PASSWORD +# - RELEASE.GPG_PASSPHRASE +# - DOCS.AWS_DOCS_ROLE_ARN +# - DOCS.AWS_DOCS_BUCKET + +on: + workflow_dispatch: + inputs: + version: + type: string + description: Semver version to release + snapshot: + type: boolean + description: Create snapshot release + default: false + skip_checks: + type: boolean + description: Skip quality checks + default: false + skip_publish: + type: boolean + description: Skip publish to Maven Central + default: false + continue_on_error: + type: boolean + description: Continue to build if there's an error in quality checks + default: false + +name: Release +run-name: Release – ${{ inputs.version }} + +permissions: + contents: read + +env: + RELEASE_COMMIT: ${{ github.sha }} + RELEASE_TAG_VERSION: ${{ inputs.version }} + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + version: ${{ format('{0}{1}', steps.version_release.outputs.version, steps.version_snapshot.outputs.version) }} + build_matrix: ${{ format('{0}{1}', steps.build_matrix_v1.outputs.build_matrix, steps.build_matrix_v2.outputs.build_matrix) }} + steps: + - id: version_snapshot + if: ${{ inputs.snapshot }} + name: Version + run: | + echo version="$(grep -q "SNAPSHOT" <<< "${{ inputs.version }}" && echo "${{ inputs.version }}" || echo "${{ inputs.version }}-SNAPSHOT")" >> "$GITHUB_OUTPUT" + - id: version_release + if: ${{ !inputs.snapshot }} + name: Version + run: | + echo version="${{ inputs.version }}" >> "$GITHUB_OUTPUT" + - id: base + name: Base + run: | + echo build_version=$(test ${{ github.ref_name }} == "v2" && echo "v2" || echo "v1") >> $GITHUB_OUTPUT + - id: build_matrix_v1 + name: Build matrix (v1) + if: ${{ steps.base.outputs.build_version == 'v1' }} + run: | + echo build_matrix='["8", "11", "17", "21"]' >> "$GITHUB_OUTPUT" + - id: build_matrix_v2 + name: Build matrix (v2) + if: ${{ steps.base.outputs.build_version == 'v2' }} + run: | + echo build_matrix='["11", "17", "21"]'>> "$GITHUB_OUTPUT" + + version_seal: + runs-on: ubuntu-latest + needs: + - setup + outputs: + source_hash: ${{ steps.upload_source.outputs.artifact-digest }} + steps: + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - id: version + name: version + uses: ./.github/actions/version + with: + new_version: ${{ needs.setup.outputs.version }} + snapshot: ${{ inputs.snapshot}} + - id: upload_source + name: Upload artifacts + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + with: + if-no-files-found: error + name: source + path: | + * + !.git/* + include-hidden-files: true + retention-days: 1 + + quality: + runs-on: ubuntu-latest + needs: + - version_seal + if: ${{ inputs.skip_checks == false }} + permissions: + contents: write + id-token: write + steps: + - id: download_source + name: Download artifacts + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.6.1 + with: + name: source + - name: Setup Java + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 + with: + distribution: corretto + java-version: 21 + cache: maven + # non-exhuastive, but gives a fair indication if the final build will succeed, tests will run when we build later + - name: Run unit tests + run: mvn -B test --file pom.xml + continue-on-error: ${{ inputs.continue_on_error }} + - name: Run Spotbugs + run: mvn -Pbuild-with-spotbugs -B install --file pom.xml -DskipTests -Dmaven.javadoc.skip=true -Dspotbugs.failOnError=true + continue-on-error: ${{ inputs.continue_on_error }} + - uses: pmd/pmd-github-action@d9c1f3c5940cbf5923f1354e83fa858b4496ebaa # v2.0.0 + with: + rulesets: '.github/pmd-ruleset.xml' + token: ${{ secrets.GITHUB_TOKEN }} + uploadSarifReport: false + + build: + runs-on: ubuntu-latest + needs: + - setup + - quality + - version_seal + if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + strategy: + matrix: + java: ${{ fromJson(needs.setup.outputs.build_matrix) }} + steps: + - id: download_source + name: Download artifacts + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.6.1 + with: + name: source + - name: Setup Java + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 + with: + distribution: corretto + java-version: ${{ matrix.java }} + cache: maven + - id: build-maven + name: Build (Maven) + run: | + mvn -B install --file pom.xml + + publish: + runs-on: ubuntu-latest + if: ${{ github.repository == 'aws-powertools/powertools-lambda-java' && inputs.skip_publish == false }} + needs: + - build + environment: Release + steps: + - id: download_source + name: Download artifacts + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.6.1 + with: + name: source + - name: Setup Java + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 + with: + distribution: corretto + java-version: 21 + cache: maven + gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }} + gpg-passphrase: GPG_PASSPHRASE + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + - name: Publish package + run: mvn -Prelease clean deploy -DskipTests + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + + create_pr: + runs-on: ubuntu-latest + if: ${{ inputs.snapshot == false && always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + needs: + - build + - publish + permissions: + pull-requests: write + contents: write + steps: + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ env.RELEASE_COMMIT }} + - id: download_source + name: Download artifacts + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.6.1 + with: + name: source + - id: setup-git + name: Git client setup and refresh tip + run: | + git config user.name "Powertools for AWS Lambda (Java) Bot" + git config user.email "151832416+aws-powertools-bot@users.noreply.github.com" + git config pull.rebase true + git config remote.origin.url >&- + - id: branch + name: Create branch + run: | + git checkout -b ci-${{ github.run_id }} + git commit -am "chore(ci): bump version to ${{ inputs.version }}" + git push origin ci-${{ github.run_id }} + - id: create_pr + name: Create PR + run: | + gh pr create \ + --title "chore(ci): bump version to ${{ inputs.version }}" \ + --body "This is an automated PR created from the following workflow: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - id: tag + name: Create release + run: | + gh release create v${{ inputs.version }} --target $(git rev-parse HEAD) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + docs: + runs-on: ubuntu-latest + if: ${{ inputs.snapshot == false }} + needs: + - create_pr + permissions: + contents: read + id-token: write + environment: Docs + steps: + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Build + run: | + mkdir -p dist + docker build -t squidfunk/mkdocs-material ./docs/ + docker run --rm -t -v ${PWD}:/docs squidfunk/mkdocs-material build + cp -R site/* dist/ + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 + with: + aws-region: us-east-1 + role-to-assume: ${{ secrets.AWS_DOCS_ROLE_ARN }} + - name: Deploy + run: | + aws s3 sync \ + dist \ + s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-java/${{ inputs.version }}/ diff --git a/.github/workflows/run-e2e-tests-v2.yml b/.github/workflows/run-e2e-tests-v2.yml deleted file mode 100644 index 255c89cfe..000000000 --- a/.github/workflows/run-e2e-tests-v2.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Run end-to-end tests - -on: - workflow_dispatch: - - push: - branches: - - v2 - paths: # add other modules when there are under e2e tests - - 'powertools-e2e-tests/**' - - 'powertools-batch/**' - - 'powertools-core/**' - - 'powertools-common/**' - - 'powertools-idempotency/**' - - 'powertools-large-message/**' - - 'powertools-logging/**' - - 'powertools-metrics/**' - - 'powertools-parameters/**' - - 'powertools-serialization/**' - - 'powertools-tracing/**' - - 'pom.xml' - - '.github/workflows/**' - - pull_request: - branches: - - v2 - paths: - - 'powertools-e2e-tests/**' - -jobs: - e2e: - runs-on: ubuntu-latest - strategy: - max-parallel: 4 - matrix: - java: [ 11, 17, 21 ] - name: End-to-end tests java${{ matrix.java }} - env: - AWS_DEFAULT_REGION: eu-west-1 - JAVA_VERSION: ${{ matrix.java }} - permissions: - id-token: write # needed to interact with GitHub's OIDC Token endpoint. - contents: read - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Setup java - uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0 - with: - distribution: 'corretto' - java-version: ${{ matrix.java }} - cache: maven - - name: Setup AWS credentials - uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 - with: - role-to-assume: ${{ secrets.AWS_ROLE_ARN_TO_ASSUME }} - aws-region: ${{ env.AWS_DEFAULT_REGION }} - - name: Run e2e test with Maven - run: mvn -DskipTests install --file pom.xml && mvn -Pe2e -B verify --file powertools-e2e-tests/pom.xml \ No newline at end of file diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml deleted file mode 100644 index 77cdea890..000000000 --- a/.github/workflows/run-e2e-tests.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Run end-to-end tests - -on: - workflow_dispatch: - - push: - branches: - - main - paths: # add other modules when there are under e2e tests - - 'powertools-e2e-tests/**' - - 'powertools-batch/**' - - 'powertools-core/**' - - 'powertools-common/**' - - 'powertools-idempotency/**' - - 'powertools-large-message/**' - - 'powertools-logging/**' - - 'powertools-metrics/**' - - 'powertools-parameters/**' - - 'powertools-serialization/**' - - 'powertools-tracing/**' - - 'pom.xml' - - '.github/workflows/**' - - pull_request: - branches: - - main - paths: - - 'powertools-e2e-tests/**' - -jobs: - e2e: - runs-on: ubuntu-latest - strategy: - max-parallel: 4 - matrix: - java: [ 8, 11, 17, 21 ] - name: End-to-end tests java${{ matrix.java }} - env: - AWS_DEFAULT_REGION: eu-west-1 - JAVA_VERSION: ${{ matrix.java }} - permissions: - id-token: write # needed to interact with GitHub's OIDC Token endpoint. - contents: read - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Setup java - uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0 - with: - distribution: 'corretto' - java-version: ${{ matrix.java }} - cache: maven - - name: Setup AWS credentials - uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 - with: - role-to-assume: ${{ secrets.AWS_ROLE_ARN_TO_ASSUME }} - aws-region: ${{ env.AWS_DEFAULT_REGION }} - - name: Run e2e test with Maven - run: mvn -DskipTests install --file pom.xml && mvn -Pe2e -B verify --file powertools-e2e-tests/pom.xml \ No newline at end of file diff --git a/.github/workflows/secure_workflows.yml b/.github/workflows/secure_workflows.yml deleted file mode 100644 index 1430e91d6..000000000 --- a/.github/workflows/secure_workflows.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Lockdown untrusted workflows - -# PROCESS -# -# 1. Scans for any external GitHub Action being used without version pinning (@ vs @v3) -# 2. Scans for insecure practices for inline bash scripts (shellcheck) -# 3. Fail CI and prevent PRs to be merged if any malpractice is found - -# USAGE -# -# Always triggered on new PR, PR changes and PR merge. - - -on: - push: - paths: - - ".github/workflows/**" - pull_request: - paths: - - ".github/workflows/**" - -jobs: - enforce_pinned_workflows: - name: Harden Security - runs-on: ubuntu-latest - permissions: - contents: read # checkout code and subsequently GitHub action workflows - steps: - - name: Checkout code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Ensure 3rd party workflows have SHA pinned - uses: zgosalvez/github-actions-ensure-sha-pinned-actions@555a30da2656b4a7cf47b107800bef097723363e # v2.1.3 diff --git a/.github/workflows/security-branch-protections.yml b/.github/workflows/security-branch-protections.yml new file mode 100644 index 000000000..dc7c06316 --- /dev/null +++ b/.github/workflows/security-branch-protections.yml @@ -0,0 +1,72 @@ +# Branch Protections +# +# Description: +# This workflow compares current security branch protections against those stored, +# if there's any changes, it'll fail the job and alert using a Slack webhook +# +# Triggers: +# - pull_request +# - branch_protection_rule +# - cron: daily at 16:40 +# +# Secrets: +# - SECURITY.BRANCH_PROTECTION_TOKEN +# - SECURITY.SLACK_WEBHOOK_URL +# +# Notes: +# Modified copy of: https://github.com/github/docs/blob/main/.github/workflows/alert-changed-branch-protections.yml + +on: + branch_protection_rule: + schedule: + - cron: '20 16 * * *' # Run daily at 16:20 UTC + pull_request: + paths: + - .github/workflows/security-branch-protections.yml + - .github/branch_protection_settings/*.json + +name: Alert Changed Branch Protections +run-name: Alert Changed Branch Protections + +permissions: + contents: read + +jobs: + check-branch-protections: + runs-on: ubuntu-latest + permissions: + contents: write + environment: Security + if: ${{ github.repository == 'aws-powertools/powertools-lambda-java' }} + strategy: + matrix: + # List of branches we want to monitor for protection changes + branch: + - main + - v2 + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Fetch branch protections + id: fetch + env: + GH_TOKEN: ${{ secrets.BRANCH_PROTECTION_TOKEN }} + run: | + # Fetch branch protections and store them in a file + gh api /repos/${{ github.repository }}/branches/${{ matrix.branch }}/protection | jq \ + > .github/branch_protection_settings/${{ matrix.branch }}.json + - name: Compare branch protections + id: compare + run: | + git diff --quiet .github/branch_protection_settings/${{ matrix.branch }}.json \ + || echo "diff_failed=true" >> $GITHUB_ENV + - name: Send webhook + if: ${{ env.diff_failed == 'true' }} + run: | + curl -X POST -d '{"message": "Branch protections have changed for ${{ github.repository }} on ${{ matrix.branch }}. Please review the changes or revert the changes in GitHub. https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' \ + ${{ secrets.SLACK_WEBHOOK_URL }} + - name: Fail workflow + if: ${{ env.diff_failed == 'true' }} + run: | + git diff .github/branch_protection_settings/${{ matrix.branch }}.json + echo "::error::Branch protections have been changed" \ No newline at end of file diff --git a/.github/workflows/security-dependabot.yml b/.github/workflows/security-dependabot.yml new file mode 100644 index 000000000..095219045 --- /dev/null +++ b/.github/workflows/security-dependabot.yml @@ -0,0 +1,42 @@ +# Auto merges dependabot PRs +# +# Description: +# Auto-merges dependabot PRs if all checks pass +# We verify all commits in the PR to ensure no one else has committed to the PR +# +# Triggers: +# - pull_request + +on: + pull_request: + branches: [ dependabot/* ] + +name: Dependabot updates +run-name: Dependabot + +permissions: + contents: read + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'aws-powertools/powertools-lambda-java' }} + permissions: + pull-requests: read + steps: + - id: dependabot-metadata + name: Fetch Dependabot metadata + uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 # v2.3.0 + - name: Fail workflow + if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-major' }} + run: | + echo "::error::Major version upgrades are not wanted" + - name: Approve PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr review "${{ github.event.pull_request.html_url }}" --approve --body '🤖 Approved by another robot.' + - name: Enable auto-merge on PR + run: gh pr merge --auto --squash "${{ github.event.pull_request.html_url }}" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/security-dependencies-check.yml b/.github/workflows/security-dependencies-check.yml new file mode 100644 index 000000000..9c588d9be --- /dev/null +++ b/.github/workflows/security-dependencies-check.yml @@ -0,0 +1,39 @@ +# Dependency checks +# +# Description: +# Verifies that dependencies are compatible with our project +# by checking licenses and their security posture +# +# Triggers: +# - pull_request +# - push +# - workflow_dispatch +# - cron: daily at 12:00PM + +on: + pull_request: + workflow_dispatch: + push: + branches: [ main ] + schedule: + - cron: '0 12 * * *' # Run daily at 12:00 UTC + +name: Verify Dependencies +run-name: Verify Dependencies – ${{ github.event_name }} + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Verify Contents + uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 + with: + config-file: './.github/dependency-review-config.yml' \ No newline at end of file diff --git a/.github/workflows/security-osv.yml b/.github/workflows/security-osv.yml new file mode 100644 index 000000000..b332faae3 --- /dev/null +++ b/.github/workflows/security-osv.yml @@ -0,0 +1,37 @@ +# Runs OSV scan +# +# Description: +# Checks dependencies already in the project for known issues +# +# Triggers: +# - pull_request +# - workflow_dispatch +# - cron +# - push + +on: + pull_request: + branches: + - main + - v2 + workflow_dispatch: {} + schedule: + - cron: "30 12 * * 1" + push: + branches: + - main + - v2 + +name: OpenSource Vulnerability Scanner +run-name: OpenSource Vulnerability Scanner + +permissions: + contents: read + +jobs: + scan-pr: + permissions: + actions: read + contents: read + security-events: write + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@764c91816374ff2d8fc2095dab36eecd42d61638 # v1.9.2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6615ac729..0972d7449 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,6 @@ native-libs/ classes/ out/ - ###################### # Eclipse ###################### @@ -99,6 +98,7 @@ Desktop.ini docs/node_modules docs/.cache +.cache docs/public /example/.aws-sam/ /example/HelloWorldFunction/.aws-sam/ @@ -110,4 +110,4 @@ example/HelloWorldFunction/build .gradle build/ .terraform* -terraform.tfstate* \ No newline at end of file +terraform.tfstate* diff --git a/.mvn/README b/.mvn/README new file mode 100644 index 000000000..a851f5e55 --- /dev/null +++ b/.mvn/README @@ -0,0 +1,2 @@ +This is here purely so that we can get the root directory using maven.multiModuleProjectDirectory + diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9f664fe..20a04c488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,82 @@ This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) fo ## [Unreleased] +## [1.20.1] - 2025-04-08 + +* docs: fix 2 typos (#1739) by @ntestor +* docs: Correct XML formatting for Maven configuration in Large Messages utility docs (#1796) by @jreijn +* fix: Load version.properties file as resource stream to fix loading when packaged as jar (#1813) by @phipag + +## [1.20.0] - 2025-03-25 + +* feat(cfn-custom-resource): Add optional 'reason' field for detailed failure reporting (#1758) by @moizsh + +## [1.19.0] - 2025-03-07 + +* chore(deps): Update deps for jackson ([#1793](https://github.com/aws-powertools/powertools-lambda-java/pull/1793)) by [@sthulb](https://github.com/sthulb) +* build(deps): bump log4j.version from 2.22.1 to 2.24.3 ([#1777](https://github.com/aws-powertools/powertools-lambda-java/pull/1777)) by [@dependabot](https://github.com/dependabot) +* chore(deps): update JSII to 1.108 ([#1791](https://github.com/aws-powertools/powertools-lambda-java/pull/1791)) by [@sthulb](https://github.com/sthulb) +* build(deps): bump jinja2 from 3.1.5 to 3.1.6 in /docs ([#1789](https://github.com/aws-powertools/powertools-lambda-java/pull/1789)) by [@dependabot](https://github.com/dependabot) +* chore: Update netty version ([#1768](https://github.com/aws-powertools/powertools-lambda-java/pull/1768)) by [@sthulb](https://github.com/sthulb) +* chore: Set versions of transitive dependencies ([#1767](https://github.com/aws-powertools/powertools-lambda-java/pull/1767)) by [@sthulb](https://github.com/sthulb) +* chore: update Jackson in examples ([#1766](https://github.com/aws-powertools/powertools-lambda-java/pull/1766)) by [@sthulb](https://github.com/sthulb) +* build(deps): bump org.apache.maven.plugins:maven-jar-plugin from 3.4.1 to 3.4.2 ([#1731](https://github.com/aws-powertools/powertools-lambda-java/pull/1731)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.xray.recorder.version from 2.15.3 to 2.18.1 ([#1726](https://github.com/aws-powertools/powertools-lambda-java/pull/1726)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.26.29 to 2.27.12 ([#1724](https://github.com/aws-powertools/powertools-lambda-java/pull/1724)) by [@dependabot](https://github.com/dependabot) +* fix: Allow empty responses as well as null response in AppConfig ([#1673](https://github.com/aws-powertools/powertools-lambda-java/pull/1673)) by [@chrisclayson](https://github.com/chrisclayson) +* build(deps): bump aws.sdk.version from 2.27.2 to 2.27.7 ([#1715](https://github.com/aws-powertools/powertools-lambda-java/pull/1715)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.26.29 to 2.27.2 ([#1714](https://github.com/aws-powertools/powertools-lambda-java/pull/1714)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.25.26 to 2.26.29 ([#1713](https://github.com/aws-powertools/powertools-lambda-java/pull/1713)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.26.25 to 2.26.29 ([#1712](https://github.com/aws-powertools/powertools-lambda-java/pull/1712)) by [@dependabot](https://github.com/dependabot) +* chore: deprecate java1.8 al1 ([#1706](https://github.com/aws-powertools/powertools-lambda-java/pull/1706)) by [@jeromevdl](https://github.com/jeromevdl) +* chore: java 1.8 AL1 is deprecated, fix E2E tests ([#1692](https://github.com/aws-powertools/powertools-lambda-java/pull/1692)) by [@jeromevdl](https://github.com/jeromevdl) +* build(deps): bump aws.sdk.version from 2.26.21 to 2.26.25 ([#1703](https://github.com/aws-powertools/powertools-lambda-java/pull/1703)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.26.3 to 2.26.21 ([#1697](https://github.com/aws-powertools/powertools-lambda-java/pull/1697)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump jackson.version from 2.17.0 to 2.17.2 ([#1696](https://github.com/aws-powertools/powertools-lambda-java/pull/1696)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.apache.commons:commons-lang3 from 3.13.0 to 3.14.0 ([#1694](https://github.com/aws-powertools/powertools-lambda-java/pull/1694)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump commons-io:commons-io from 2.15.1 to 2.16.1 ([#1691](https://github.com/aws-powertools/powertools-lambda-java/pull/1691)) by [@dependabot](https://github.com/dependabot) +* docs: improve tracing doc for sdk instrumentation ([#1687](https://github.com/aws-powertools/powertools-lambda-java/pull/1687)) by [@jeromevdl](https://github.com/jeromevdl) +* docs: fix tracing links for xray ([#1686](https://github.com/aws-powertools/powertools-lambda-java/pull/1686)) by [@jeromevdl](https://github.com/jeromevdl) +* build(deps): bump org.apache.maven.plugins:maven-failsafe-plugin from 3.2.5 to 3.3.0 ([#1679](https://github.com/aws-powertools/powertools-lambda-java/pull/1679)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.25.69 to 2.26.3 ([#1658](https://github.com/aws-powertools/powertools-lambda-java/pull/1658)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump com.github.spotbugs:spotbugs-maven-plugin from 4.7.3.6 to 4.8.5.0 ([#1657](https://github.com/aws-powertools/powertools-lambda-java/pull/1657)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.apache.maven.plugins:maven-checkstyle-plugin from 3.3.0 to 3.4.0 ([#1653](https://github.com/aws-powertools/powertools-lambda-java/pull/1653)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.25.50 to 2.25.69 ([#1652](https://github.com/aws-powertools/powertools-lambda-java/pull/1652)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.apache.maven.plugins:maven-source-plugin from 3.3.0 to 3.3.1 ([#1646](https://github.com/aws-powertools/powertools-lambda-java/pull/1646)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.assertj:assertj-core from 3.25.3 to 3.26.0 ([#1644](https://github.com/aws-powertools/powertools-lambda-java/pull/1644)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.xray.recorder.version from 2.15.1 to 2.15.3 ([#1643](https://github.com/aws-powertools/powertools-lambda-java/pull/1643)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.25.35 to 2.25.50 ([#1642](https://github.com/aws-powertools/powertools-lambda-java/pull/1642)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump com.amazonaws:aws-lambda-java-events from 3.11.2 to 3.11.4 ([#1597](https://github.com/aws-powertools/powertools-lambda-java/pull/1597)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.24.10 to 2.25.6 ([#1603](https://github.com/aws-powertools/powertools-lambda-java/pull/1603)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.apache.maven.plugins:maven-surefire-plugin from 3.1.2 to 3.2.5 ([#1596](https://github.com/aws-powertools/powertools-lambda-java/pull/1596)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.codehaus.mojo:exec-maven-plugin from 3.1.0 to 3.2.0 ([#1585](https://github.com/aws-powertools/powertools-lambda-java/pull/1585)) by [@dependabot](https://github.com/dependabot) +* build(deps-dev): bump software.amazon.awscdk:aws-cdk-lib from 2.100.0 to 2.130.0 ([#1586](https://github.com/aws-powertools/powertools-lambda-java/pull/1586)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump io.burt:jmespath-jackson from 0.5.1 to 0.6.0 ([#1587](https://github.com/aws-powertools/powertools-lambda-java/pull/1587)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.21.0 to 2.24.10 ([#1581](https://github.com/aws-powertools/powertools-lambda-java/pull/1581)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump commons-io:commons-io from 2.13.0 to 2.15.1 ([#1584](https://github.com/aws-powertools/powertools-lambda-java/pull/1584)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.xray.recorder.version from 2.14.0 to 2.15.1 ([#1583](https://github.com/aws-powertools/powertools-lambda-java/pull/1583)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.apache.maven.plugins:maven-shade-plugin from 3.5.0 to 3.5.2 ([#1582](https://github.com/aws-powertools/powertools-lambda-java/pull/1582)) by [@dependabot](https://github.com/dependabot) +* build(deps-dev): bump org.yaml:snakeyaml from 2.1 to 2.2 ([#1400](https://github.com/aws-powertools/powertools-lambda-java/pull/1400)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump log4j.version from 2.20.0 to 2.22.1 ([#1547](https://github.com/aws-powertools/powertools-lambda-java/pull/1547)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.apache.maven.plugins:maven-artifact-plugin from 3.4.1 to 3.5.0 ([#1485](https://github.com/aws-powertools/powertools-lambda-java/pull/1485)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump com.amazonaws:aws-lambda-java-serialization from 1.1.2 to 1.1.5 ([#1573](https://github.com/aws-powertools/powertools-lambda-java/pull/1573)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.jacoco:jacoco-maven-plugin from 0.8.10 to 0.8.11 ([#1509](https://github.com/aws-powertools/powertools-lambda-java/pull/1509)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aspectj to 1.9.21 for jdk21 ([#1536](https://github.com/aws-powertools/powertools-lambda-java/pull/1536)) by [@jeromevdl](https://github.com/jeromevdl) +* docs: HelloWorldStreamFunction in examples fails with sam ([#1532](https://github.com/aws-powertools/powertools-lambda-java/pull/1532)) by [@jasoniharris](https://github.com/jasoniharris) +* chore: Testing java21 aspectj pre-release ([#1519](https://github.com/aws-powertools/powertools-lambda-java/pull/1519)) by [@scottgerring](https://github.com/scottgerring) +* fix: LargeMessageIdempotentE2ET Flaky ([#1518](https://github.com/aws-powertools/powertools-lambda-java/pull/1518)) by [@scottgerring](https://github.com/scottgerring) +* build(deps): bump software.amazon.payloadoffloading:payloadoffloading-common from 2.1.3 to 2.2.0 ([#1639](https://github.com/aws-powertools/powertools-lambda-java/pull/1639)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.apache.maven.plugins:maven-jar-plugin from 3.3.0 to 3.4.1 ([#1638](https://github.com/aws-powertools/powertools-lambda-java/pull/1638)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump jackson.version from 2.15.3 to 2.17.0 ([#1637](https://github.com/aws-powertools/powertools-lambda-java/pull/1637)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.25.31 to 2.25.35 ([#1629](https://github.com/aws-powertools/powertools-lambda-java/pull/1629)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.25.16 to 2.25.31 ([#1625](https://github.com/aws-powertools/powertools-lambda-java/pull/1625)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.21.1 to 2.25.26 ([#1622](https://github.com/aws-powertools/powertools-lambda-java/pull/1622)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.apache.maven.plugins:maven-failsafe-plugin from 3.1.2 to 3.2.5 ([#1619](https://github.com/aws-powertools/powertools-lambda-java/pull/1619)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump com.fasterxml.jackson.datatype:jackson-datatype-joda from 2.15.2 to 2.17.0 ([#1616](https://github.com/aws-powertools/powertools-lambda-java/pull/1616)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump aws.sdk.version from 2.25.6 to 2.25.16 ([#1613](https://github.com/aws-powertools/powertools-lambda-java/pull/1613)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.apache.maven.plugins:maven-gpg-plugin from 3.1.0 to 3.2.1 ([#1610](https://github.com/aws-powertools/powertools-lambda-java/pull/1610)) by [@dependabot](https://github.com/dependabot) +* build(deps): bump org.assertj:assertj-core from 3.24.2 to 3.25.3 ([#1609](https://github.com/aws-powertools/powertools-lambda-java/pull/1609)) by [@dependabot](https://github.com/dependabot) + ## [1.18.0] - 2023-11-16 ### Added diff --git a/GraalVM.md b/GraalVM.md new file mode 100644 index 000000000..56c72d96f --- /dev/null +++ b/GraalVM.md @@ -0,0 +1,60 @@ +# GraalVM Compatibility for AWS Lambda Powertools Java + +## Table of Contents +- [Overview](#overview) +- [Prerequisites](#prerequisites) +- [General Implementation Steps](#general-implementation-steps) +- [Known Issues and Solutions](#known-issues-and-solutions) +- [Reference Implementation](#reference-implementation) + +## Overview +This documentation provides guidance for adding GraalVM support for AWS Lambda Powertools Java modules and using the modules in Lambda functions. + +## Prerequisites +- GraalVM 21+ installation +- Maven 3.x + +## General Implementation Steps +GraalVM native image compilation requires complete knowledge of an application's dynamic features at build time. The GraalVM reachability metadata (GRM) JSON files are essential because Java applications often use features that are determined at runtime, such as reflection, dynamic proxy classes, resource loading, and JNI (Java Native Interface). The metadata files tell GraalVM which classes need reflection access, which resources need to be included in the native image, and which proxy classes need to be generated. + +In order to generate the metadata reachability files for Powertools for Lambda, follow these general steps. + +1. **Add Maven Profiles** + - Add profile for generating GraalVM reachability metadata files. You can find an example of this in profile `generate-graalvm-files` of this [pom.xml](powertools-common/pom.xml). + - Add another profile for running the tests in the native image. You can find and example of this in profile `graalvm-native` of this [pom.xml](powertools-common/pom.xml). + +2. **Generate Reachability Metadata** + - Set the `JAVA_HOME` environment variable to use GraalVM + - Run tests with `-Pgenerate-graalvm-files` profile. +```shell +mvn -Pgenerate-graalvm-files clean test +``` + +3. **Validate Native Image Tests** + - Set the `JAVA_HOME` environment variable to use GraalVM + - Run tests with `-Pgraalvm-native` profile. This will build a GraalVM native image and run the JUnit tests. +```shell +mvn -Pgraalvm-native clean test +``` + +4. **Clean Up Metadata** + - GraalVM metadata reachability files generated in Step 2 contains references to the test scoped dependencies as well. + - Remove the references in generated metadata files for the following (and any other references to test scoped resources and classes): + - JUnit + - Mockito + - ByteBuddy + +## Known Issues and Solutions +1. **Mockito Compatibility** + - Powertools uses Mockito 5.x which uses “inline mock maker” as the default. This mock maker does not play well with GraalVM. Mockito [recommends](https://github.com/mockito/mockito/releases/tag/v5.0.0) using subclass mock maker with GraalVM. Therefore `generate-graalvm-files` profile uses subclass mock maker instead of inline mock maker. + - Subclass mock maker does not support testing static methods. Tests have therefore been modified to use [JUnit Pioneer](https://junit-pioneer.org/docs/environment-variables/) to inject the environment variables in the scope of the test's execution. + +2. **Log4j Compatibility** + - Version 2.22.1 fails with this error +``` +java.lang.InternalError: com.oracle.svm.core.jdk.UnsupportedFeatureError: Defining hidden classes at runtime is not supported. +``` + - This has been [fixed](https://github.com/apache/logging-log4j2/discussions/2364#discussioncomment-8950077) in Log4j 2.24.x. PT has been updated to use this version of Log4j + +## Reference Implementation +Working example is available in the [examples](examples/powertools-examples-core-utilities/sam-graalvm). diff --git a/README.md b/README.md index dbe8eb2fe..625d3d009 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ -# Powertools for AWS Lambda (Java) +# Powertools for AWS Lambda (Java) V2 -![aws provider](https://img.shields.io/badge/provider-AWS-orange?logo=amazon-aws&color=ff9900) ![Build status](https://github.com/aws-powertools/powertools-lambda-java/actions/workflows/build.yml/badge.svg) ![Maven Central](https://img.shields.io/maven-central/v/software.amazon.lambda/powertools-parent) [![codecov.io](https://codecov.io/github/aws-powertools/powertools-lambda-java/branch/main/graphs/badge.svg)](https://app.codecov.io/gh/aws-powertools/powertools-lambda-java) +**This is pre-release code for Powertools for AWS Lambda (Java) V2! Please check out the `main` branch for the stable release** + +![aws provider](https://img.shields.io/badge/provider-AWS-orange?logo=amazon-aws&color=ff9900) [![V2 Build Status](https://github.com/aws-powertools/powertools-lambda-java/actions/workflows/pr_build.yml/badge.svg?branch=v2)](https://github.com/aws-powertools/powertools-lambda-java/actions/workflows/pr_build.yml) **MAVEN DEPLOY NOT DONE** [![codecov.io](https://codecov.io/github/aws-powertools/powertools-lambda-java/branch/v2/graphs/badge.svg)](https://app.codecov.io/gh/aws-powertools/powertools-lambda-java/tree/v2) Powertools for AWS Lambda (Java) is a developer toolkit to implement Serverless best practices and increase developer velocity. > Also available in [Python](https://github.com/aws-powertools/powertools-lambda-python), [TypeScript](https://github.com/aws-powertools/powertools-lambda-typescript), and [.NET](https://github.com/aws-powertools/powertools-lambda-dotnet). -**[📜Documentation](https://docs.powertools.aws.dev/lambda-java/)** | **[Feature request](https://github.com/aws-powertools/powertools-lambda-java/issues/new?assignees=&labels=feature-request%2C+triage&template=feature_request.md&title=)** | **[🐛Bug Report](https://github.com/aws-powertools/powertools-lambda-java/issues/new?assignees=&labels=bug%2C+triage&template=bug_report.md&title=)** | **[Detailed blog post](https://aws.amazon.com/blogs/opensource/simplifying-serverless-best-practices-with-aws-lambda-powertools-java/)** +**[📜Documentation](https://docs.powertools.aws.dev/lambda-java/preview)** | **[Feature request](https://github.com/aws-powertools/powertools-lambda-java/issues/new?assignees=&labels=feature-request%2C+triage&template=feature_request.md&title=)** | **[🐛Bug Report](https://github.com/aws-powertools/powertools-lambda-java/issues/new?assignees=&labels=bug%2C+triage&template=bug_report.md&title=)** | **[Detailed blog post](https://aws.amazon.com/blogs/opensource/simplifying-serverless-best-practices-with-aws-lambda-powertools-java/)** ## Installation @@ -20,26 +22,26 @@ Powertools for AWS Lambda (Java) is available in Maven Central. You can use your software.amazon.lambda powertools-tracing - 1.18.0 + 2.0.0-RC1 software.amazon.lambda powertools-logging - 1.18.0 + 2.0.0-RC1 software.amazon.lambda powertools-metrics - 1.18.0 + 2.0.0-RC1 ... ``` -Next, configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lambda-powertools-java aspects into your project. A different configuration is needed for projects on Java 8. +Next, configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lambda-powertools-java aspects into your project.
- Maven - Java 11 and newer + Maven ```xml @@ -48,11 +50,11 @@ Next, configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lam dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 - 1.8 - 1.8 - 1.8 + 11 + 11 + 11 software.amazon.lambda @@ -68,6 +70,14 @@ Next, configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lam + + + org.aspectj + aspectjtools + + 1.9.22 + + @@ -83,51 +93,7 @@ Next, configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lam
-Maven - Java 8 - -```xml - - - ... - - org.codehaus.mojo - aspectj-maven-plugin - 1.14.0 - - 1.8 - 1.8 - 1.8 - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-metrics - - - - - - - compile - - - - - ... - - -``` -
- -
-Gradle - Java 11+ +Gradle ```groovy @@ -150,7 +116,7 @@ Next, configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lam aspect 'software.amazon.lambda:powertools-logging:{{ powertools.version }}' aspect 'software.amazon.lambda:powertools-tracing:{{ powertools.version }}' aspect 'software.amazon.lambda:powertools-metrics:{{ powertools.version }}' - implementation "org.aspectj:aspectjrt:1.9.8.RC3" + implementation "org.aspectj:aspectjrt:1.9.22" } sourceCompatibility = 11 @@ -158,38 +124,9 @@ Next, configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lam ```
-
-Gradle - Java 8 - -```groovy - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' - } - - // the freefair aspect plugins targets gradle 7.6.1 - // https://docs.freefair.io/gradle-plugins/6.6.3/reference/ - wrapper { - gradleVersion = "7.6.1" - } - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-logging:{{ powertools.version }}' - aspect 'software.amazon.lambda:powertools-tracing:{{ powertools.version }}' - aspect 'software.amazon.lambda:powertools-metrics:{{ powertools.version }}' - } - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 -``` -
### Java Compatibility -Powertools for AWS Lambda (Java) supports all Java version from 8 up to 21 as well as the +Powertools for AWS Lambda (Java) supports all Java version from 11 up to 21 as well as the [corresponding Lambda runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html). For the modules that provide annotations, Powertools for AWS Lambda (Java) leverages the **aspectj** library. You may need to add the good version of `aspectjrt` to your dependencies based on the JDK used for building your function: @@ -205,11 +142,10 @@ You may need to add the good version of `aspectjrt` to your dependencies based o
JDK - aspectj dependency matrix -| JDK version | aspectj version | -|-------------|-----------------| -| `1.8` | `1.9.7` | -| `11-17` | `1.9.20.1` | -| `21` | `1.9.21` | +| JDK version | aspectj version | +|-------------|------------------------| +| `11-17` | `1.9.20.1` (or higher) | +| `21` | `1.9.21` (or higher) | More info [here](https://github.com/aws-powertools/powertools-lambda-java/pull/1519/files#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R191). diff --git a/checkstyle.xml b/checkstyle.xml index 34ef98ef2..680d8808c 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -51,7 +51,7 @@ - + diff --git a/docs/Dockerfile b/docs/Dockerfile index 1524933ab..8b5e5e275 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,2 +1,4 @@ -FROM squidfunk/mkdocs-material -RUN pip install mkdocs-git-revision-date-plugin mkdocs-macros-plugin \ No newline at end of file +FROM squidfunk/mkdocs-material@sha256:23b69789b1dd836c53ea25b32f62ef8e1a23366037acd07c90959a219fd1f285 + +COPY requirements.txt /tmp/ +RUN pip install --require-hashes -r /tmp/requirements.txt diff --git a/docs/FAQs.md b/docs/FAQs.md index 99ef40905..75f699c91 100644 --- a/docs/FAQs.md +++ b/docs/FAQs.md @@ -3,16 +3,15 @@ title: FAQs description: Frequently Asked Questions --- - ## How can I use Powertools for AWS Lambda (Java) with Lombok? -Poweretools uses `aspectj-maven-plugin` to compile-time weave (CTW) aspects into the project. In case you want to use `Lombok` or other compile-time preprocessor for your project, it is required to change `aspectj-maven-plugin` configuration to enable in-place weaving feature. Otherwise the plugin will ignore changes introduced by `Lombok` and will use `.java` files as a source. +Many utilities in this library use `aspectj-maven-plugin` to compile-time weave (CTW) aspects into the project. In case you want to use `Lombok` or other compile-time preprocessor for your project, it is required to change `aspectj-maven-plugin` configuration to enable in-place weaving feature. Otherwise the plugin will ignore changes introduced by `Lombok` and will use `.java` files as a source. To enable in-place weaving feature you need to use following `aspectj-maven-plugin` configuration: ```xml hl_lines="2-6" - true + true ${project.build.directory}/classes @@ -29,14 +28,14 @@ To enable in-place weaving feature you need to use following `aspectj-maven-plug ## How can I use Powertools for AWS Lambda (Java) with Kotlin projects? -Poweretools uses `aspectj-maven-plugin` to compile-time weave (CTW) aspects into the project. When using it with Kotlin projects, it is required to `forceAjcCompile`. -No explicit configuration should be required for gradle projects. +Many utilities use `aspectj-maven-plugin` to compile-time weave (CTW) aspects into the project. When using it with Kotlin projects, it is required to `forceAjcCompile`. +No explicit configuration should be required for gradle projects. To enable `forceAjcCompile` you need to use following `aspectj-maven-plugin` configuration: ```xml hl_lines="2" - true + true ... @@ -47,3 +46,212 @@ To enable `forceAjcCompile` you need to use following `aspectj-maven-plugin` con ``` +## How can I use Powertools for AWS Lambda (Java) with the AWS CRT HTTP Client? + +Utilities relying on AWS SDK clients use the `url-connection-client` as the default HTTP client. The `url-connection-client` is a lightweight HTTP client, which keeps the impact on Lambda cold starts to a minimum. +With the [announcement](https://aws.amazon.com/blogs/developer/announcing-availability-of-the-aws-crt-http-client-in-the-aws-sdk-for-java-2-x/) of the `aws-crt-client` a new HTTP client has been released, which offers faster SDK startup time and smaller memory footprint. + +Unfortunately, replacing the `url-connection-client` dependency with the `aws-crt-client` will not immediately improve the lambda cold start performance and memory footprint, +as the default version of the dependency contains native system libraries for all supported runtimes and architectures (Linux, MacOS, Windows, AMD64, ARM64, etc). This makes the CRT client portable, without the user having to consider _where_ their code will run, but comes at the cost of JAR size. + +### Configuring dependencies + +Using the `aws-crt-client` in your project requires the exclusion of the `url-connection-client` transitive dependency from the `powertools-*` dependency. + +```xml + + software.amazon.lambda + powertools-parameters + 2.0.0 + + + software.amazon.awssdk + url-connection-client + + + +``` + +Next, add the `aws-crt-client` and exclude the "generic" `aws-crt` dependency (contains all runtime libraries). +Instead, set a specific classifier of the `aws-crt` to use the one for your target runtime: either `linux-x86_64` for a Lambda configured for x86 or `linux-aarch_64` for Lambda using arm64. + +!!! note "You will need to add a separate maven profile to build and debug locally when your development environment does not share the target architecture you are using in Lambda." +By specifying the specific target runtime, we prevent other target runtimes from being included in the jar file, resulting in a smaller Lambda package and improved cold start times. + +```xml + + + software.amazon.awssdk + aws-crt-client + 2.23.21 + + + software.amazon.awssdk.crt + aws-crt + + + + + + software.amazon.awssdk.crt + aws-crt + 0.29.9 + linux-x86_64 + + +``` + +### Explicitly set the AWS CRT HTTP Client + +After configuring the dependencies, it's required to explicitly specify the AWS SDK HTTP client. +Depending on the utility you are using, there is a different way to configure the SDK client. + +The following example shows how to use the Parameters module while leveraging the AWS CRT Client. + +```java hl_lines="16 23-24" +import static software.amazon.lambda.powertools.parameters.transform.Transformer.base64; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import software.amazon.awssdk.services.ssm.SsmClient; +import software.amazon.awssdk.http.crt.AwsCrtHttpClient; +import software.amazon.lambda.powertools.parameters.ssm.SSMProvider; + +public class RequestHandlerWithParams implements RequestHandler { + + // Get an instance of the SSMProvider with a custom HTTP client (aws crt). + SSMProvider ssmProvider = SSMProvider + .builder() + .withClient( + SsmClient.builder() + .httpClient(AwsCrtHttpClient.builder().build()) + .build() + ) + .build(); + + public String handleRequest(String input, Context context) { + // Retrieve a single param + String value = ssmProvider + .get("/my/secret"); + // We might instead want to retrieve multiple parameters at once, returning a Map of key/value pairs + // .getMultiple("/my/secret/path"); + + // Return the result + return value; + } +} +``` + +The `aws-crt-client` was considered for adoption as the default HTTP client in Powertools for AWS Lambda (Java) as mentioned in [Move SDK http client to CRT](https://github.com/aws-powertools/powertools-lambda-java/issues/1092), +but due to the impact on the developer experience it was decided to stick with the `url-connection-client`. + +## How can I use Powertools for AWS Lambda (Java) with GraalVM? + +Core utilities, i.e. [logging](./core/logging.md), [metrics](./core/metrics.md) and [tracing](./core/tracing.md), include the [GraalVM Reachability Metadata (GRM)](https://www.graalvm.org/latest/reference-manual/native-image/metadata/) in the `META-INF` directories of the respective JARs. You can find a working example of Serverless Application Model (SAM) based application in the [examples](../examples/powertools-examples-core-utilities/sam-graalvm/README.md) directory. + +Below, you find typical steps you need to follow in a Maven based Java project: + +### Set the environment to use GraalVM + +```shell +export JAVA_HOME= +``` + +### Use log4j `>2.24.0` + +Log4j version `2.24.0` adds [support for GraalVM](https://github.com/apache/logging-log4j2/issues/1539#issuecomment-2106766878). Depending on your project's dependency hierarchy, older version of log4j might be included in the final dependency graph. Make sure version `>2.24.0` of these dependencies are used by your Maven project: + +```xml + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-slf4j2-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-layout-template-json + ${log4j.version} + + + +``` + +### Add the AWS Lambda Java Runtime Interface Client dependency + +The Runtime Interface Client allows your function to receive invocation events from Lambda, send the response back to Lambda, and report errors to the Lambda service. Add the below dependency to your Maven project: + +```xml + + com.amazonaws + aws-lambda-java-runtime-interface-client + 2.1.1 + +``` + +Also include the AWS Lambda GRM files by copying the `com.amazonaws` [directory](../examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/) in your project's `META-INF/native-image` directory + +### Build the native image + +Use the `native-maven-plugin` to build the native image. You can do this by adding the plugin to your `pom.xml` and creating a build profile called `native-image` that can build the native image of your Lambda function: + +```xml + + + native-image + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.1 + true + + + build-native + + build + + package + + + + your-project-name + com.amazonaws.services.lambda.runtime.api.client.AWSLambda + + + --enable-url-protocols=http + --add-opens java.base/java.util=ALL-UNNAMED + + + + + + + +``` + +Create a Docker image using a `Dockerfile` like [this](../examples/powertools-examples-core-utilities/sam-graalvm/Dockerfile) to create an x86 based build image. + +```shell +docker build --platform linux/amd64 . -t your-org/your-app-graalvm-builder +``` + +Create the native image of you Lambda function using the Docker command below. + +```shell +docker run --platform linux/amd64 -it -v `pwd`:`pwd` -w `pwd` -v ~/.m2:/root/.m2 your-org/your-app-graalvm-builder mvn clean -Pnative-image package + +``` + +The native image is created in the `target/` directory. diff --git a/docs/core/logging.md b/docs/core/logging.md index 70781b1b2..2d9e57dda 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -5,24 +5,35 @@ description: Core utility Logging provides an opinionated logger with output structured as JSON. -**Key features** +## Key features -* Capture key fields from Lambda context, cold start and structures logging output as JSON -* Log Lambda event when instructed, disabled by default, can be enabled explicitly via annotation param -* Append additional keys to structured log at any point in time +* Leverages standard logging libraries: [_SLF4J_](https://www.slf4j.org/){target="_blank"} as the API, and [_log4j2_](https://logging.apache.org/log4j/2.x/){target="_blank"} or [_logback_](https://logback.qos.ch/){target="_blank"} for the implementation +* Captures key fields from Lambda context, cold start and structures logging output as JSON +* Optionally logs Lambda request +* Optionally logs Lambda response +* Optionally supports log sampling by including a configurable percentage of DEBUG logs in logging output +* Allows additional keys to be appended to the structured log at any point in time +* GraalVM support -## Install -Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes. +## Getting started -=== "Maven Java 11+" +???+ tip + You can find complete examples in the [project repository](https://github.com/aws-powertools/powertools-lambda-java/tree/v2/examples/powertools-examples-core-utilities){target="_blank"}. - ```xml hl_lines="3-7 16 18 24-27" +### Installation +Depending on preference, you must choose to use either _log4j2_ or _logback_ as your log provider. In both cases you need to configure _aspectj_ +to weave the code and make sure the annotation is processed. + +#### Maven +=== "log4j2" + + ```xml hl_lines="3-7 24-27" ... software.amazon.lambda - powertools-logging + powertools-logging-log4j {{ powertools.version }} ... @@ -35,7 +46,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 11 11 @@ -47,6 +58,14 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl + + + org.aspectj + aspectjtools + + 1.9.22 + + @@ -60,14 +79,14 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ``` -=== "Maven Java 1.8" +=== "logback" - ```xml hl_lines="3-7 16 18 24-27" + ```xml hl_lines="3-7 24-27" ... software.amazon.lambda - powertools-logging + powertools-logging-logback {{ powertools.version }} ... @@ -78,13 +97,13 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ... - org.codehaus.mojo + dev.aspectj aspectj-maven-plugin - 1.14.0 + 1.14 - 1.8 - 1.8 - 1.8 + 11 + 11 + 11 software.amazon.lambda @@ -92,6 +111,14 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl + + + org.aspectj + aspectjtools + + 1.9.22 + + @@ -105,7 +132,9 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ``` -=== "Gradle Java 11+" +#### Gradle + +=== "log4j2" ```groovy hl_lines="3 11" plugins { @@ -118,19 +147,19 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl } dependencies { - aspect 'software.amazon.lambda:powertools-logging:{{ powertools.version }}' + aspect 'software.amazon.lambda:powertools-logging-log4j:{{ powertools.version }}' } sourceCompatibility = 11 targetCompatibility = 11 ``` -=== "Gradle Java 1.8" +=== "logback" ```groovy hl_lines="3 11" plugins { id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' + id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0' } repositories { @@ -138,27 +167,61 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl } dependencies { - aspect 'software.amazon.lambda:powertools-logging:{{ powertools.version }}' + aspect 'software.amazon.lambda:powertools-logging-logback:{{ powertools.version }}' } - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + sourceCompatibility = 11 + targetCompatibility = 11 ``` -## Initialization +### Configuration -Powertools for AWS Lambda (Java) extends the functionality of Log4J. Below is an example `#!xml log4j2.xml` file, with the `JsonTemplateLayout` using `#!json LambdaJsonLayout.json` configured. +#### Main environment variables -!!! info "LambdaJsonLayout is now deprecated" +The logging module requires two settings: - Configuring utiltiy using `` plugin is deprecated now. While utility still supports the old configuration, we strongly recommend upgrading the - `log4j2.xml` configuration to `JsonTemplateLayout` instead. [JsonTemplateLayout](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html) is recommended way of doing structured logging. - - Please follow [this guide](#upgrade-to-jsontemplatelayout-from-deprecated-lambdajsonlayout-configuration-in-log4j2xml) for upgrade steps. +| Environment variable | Setting | Description | +|---------------------------|-------------------|-------------------------------------------------------------------------------------------------------------| +| `POWERTOOLS_LOG_LEVEL` | **Logging level** | Sets how verbose Logger should be. If not set, will use the [Logging configuration](#logging-configuration) | +| `POWERTOOLS_SERVICE_NAME` | **Service** | Sets service key that will be included in all log statements (Default is `service_undefined`) | + +Here is an example using AWS Serverless Application Model (SAM): + +=== "template.yaml" +``` yaml hl_lines="10 11" +Resources: + PaymentFunction: + Type: AWS::Serverless::Function + Properties: + MemorySize: 512 + Timeout: 20 + Runtime: java17 + Environment: + Variables: + POWERTOOLS_LOG_LEVEL: WARN + POWERTOOLS_SERVICE_NAME: payment +``` + +There are some other environment variables which can be set to modify Logging's settings at a global scope: + +| Environment variable | Type | Description | +|---------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------| +| `POWERTOOLS_LOGGER_SAMPLE_RATE` | float | Configure the sampling rate at which `DEBUG` logs should be included. See [sampling rate](#sampling-debug-logs) | +| `POWERTOOLS_LOG_EVENT` | boolean | Specify if the incoming Lambda event should be logged. See [Logging event](#logging-incoming-event) | +| `POWERTOOLS_LOG_RESPONSE` | boolean | Specify if the Lambda response should be logged. See [logging response](#logging-handler-response) | +| `POWERTOOLS_LOG_ERROR` | boolean | Specify if a Lambda uncaught exception should be logged. See [logging exception](#logging-handler-uncaught-exception ) | + +#### Logging configuration + +Powertools for AWS Lambda (Java) simply extends the functionality of the underlying library you choose (_log4j2_ or _logback_). +You can leverage the standard configuration files (_log4j2.xml_ or _logback.xml_): === "log4j2.xml" + With log4j2, we leverage the [`JsonTemplateLayout`](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html){target="_blank"} + to provide structured logging. A default template is provided in powertools ([_LambdaJsonLayout.json_](https://github.com/aws-powertools/powertools-lambda-java/blob/4444b4bce8eb1cc19880d1c1ef07188d97de9126/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json){target="_blank"}): + ```xml hl_lines="5" @@ -168,7 +231,7 @@ Powertools for AWS Lambda (Java) extends the functionality of Log4J. Below is an - + @@ -178,152 +241,161 @@ Powertools for AWS Lambda (Java) extends the functionality of Log4J. Below is an ``` -You can also override log level by setting **`POWERTOOLS_LOG_LEVEL`** env var. Here is an example using AWS Serverless Application Model (SAM) +=== "logback.xml" -=== "template.yaml" - ``` yaml hl_lines="9 10" - Resources: - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - ... - Runtime: java8 - Environment: - Variables: - POWERTOOLS_LOG_LEVEL: DEBUG - POWERTOOLS_SERVICE_NAME: example + With logback, we leverage a custom [Encoder](https://logback.qos.ch/manual/encoders.html){target="_blank"} + to provide structured logging: + + ```xml hl_lines="4 5" + + + + + + + + + + + + + ``` -You can also explicitly set a service name via **`POWERTOOLS_SERVICE_NAME`** env var. This sets **service** key that will be present across all log statements. +## Log level +Log level is generally configured in the `log4j2.xml` or `logback.xml`. But this level is static and needs a redeployment of the function to be changed. +Powertools for AWS Lambda permits to change this level dynamically thanks to an environment variable `POWERTOOLS_LOG_LEVEL`. -## Standard structured keys +We support the following log levels (SLF4J levels): `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. +If the level is set to `CRITICAL` (supported in log4j but not logback), we revert it back to `ERROR`. +If the level is set to any other value, we set it to the default value (`INFO`). -Your logs will always include the following keys to your structured logging: +### AWS Lambda Advanced Logging Controls (ALC) -Key | Type | Example | Description -------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------- -**timestamp** | String | "2020-05-24 18:17:33,774" | Timestamp of actual log statement -**level** | String | "INFO" | Logging level -**coldStart** | Boolean | true| ColdStart value. -**service** | String | "payment" | Service name defined. "service_undefined" will be used if unknown -**samplingRate** | int | 0.1 | Debug logging sampling rate in percentage e.g. 10% in this case -**message** | String | "Collecting payment" | Log statement value. Unserializable JSON values will be casted to string -**functionName**| String | "example-powertools-HelloWorldFunction-1P1Z6B39FLU73" -**functionVersion**| String | "12" -**functionMemorySize**| String | "128" -**functionArn**| String | "arn:aws:lambda:eu-west-1:012345678910:function:example-powertools-HelloWorldFunction-1P1Z6B39FLU73" -**xray_trace_id**| String | "1-5759e988-bd862e3fe1be46a994272793" | X-Ray Trace ID when Lambda function has enabled Tracing -**function_request_id**| String | "899856cb-83d1-40d7-8611-9e78f15f32f4"" | AWS Request ID from lambda context +!!!question "When is it useful?" + When you want to set a logging policy to drop informational or verbose logs for one or all AWS Lambda functions, regardless of runtime and logger used. -## Capturing context Lambda info + +With [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced){target="_blank"}, you can enforce a minimum log level that Lambda will accept from your application code. -When debugging in non-production environments, you can instruct Logger to log the incoming event with `@Logger(logEvent = true)` or via `POWERTOOLS_LOGGER_LOG_EVENT=true` environment variable. +When enabled, you should keep your own log level and ALC log level in sync to avoid data loss. -!!! warning - Log event is disabled by default to prevent sensitive info being logged. +Here's a sequence diagram to demonstrate how ALC will drop both `INFO` and `DEBUG` logs emitted from `Logger`, when ALC log level is stricter than `Logger`. + +```mermaid +sequenceDiagram + participant Lambda service + participant Lambda function + participant Application Logger -=== "App.java" + Note over Lambda service: AWS_LAMBDA_LOG_LEVEL="WARN" + Note over Application Logger: POWERTOOLS_LOG_LEVEL="DEBUG" + + Lambda service->>Lambda function: Invoke (event) + Lambda function->>Lambda function: Calls handler + Lambda function->>Application Logger: logger.error("Something happened") + Lambda function-->>Application Logger: logger.debug("Something happened") + Lambda function-->>Application Logger: logger.info("Something happened") + Lambda service--xLambda service: DROP INFO and DEBUG logs + Lambda service->>CloudWatch Logs: Ingest error logs +``` - ```java hl_lines="14" - import org.apache.logging.log4j.LogManager; - import org.apache.logging.log4j.Logger; - import software.amazon.lambda.powertools.logging.LoggingUtils; +### Priority of log level settings in Powertools for AWS Lambda + +We prioritise log level settings in this order: + +1. `AWS_LAMBDA_LOG_LEVEL` environment variable +2. `POWERTOOLS_LOG_LEVEL` environment variable +3. level defined in the `log4j2.xml` or `logback.xml` files + +If you set `POWERTOOLS_LOG_LEVEL` lower than ALC, we will emit a warning informing you that your messages will be discarded by Lambda. + +> **NOTE** +> +> With ALC enabled, we are unable to increase the minimum log level below the `AWS_LAMBDA_LOG_LEVEL` environment variable value, see [AWS Lambda service documentation](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-log-level){target="_blank"} for more details. + +## Basic Usage + +To use Lambda Powertools for AWS Lambda Logging, use the `@Logging` annotation in your code and the standard _SLF4J_ logger: + +=== "PaymentFunction.java" + + ```java hl_lines="8 10 12 14" + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; - ... - - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { - - Logger log = LogManager.getLogger(App.class); - - @Logging - public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... - } - } - ``` + // ... other imports -=== "AppLogEvent.java" - - ```java hl_lines="8" - /** - * Handler for requests to Lambda function. - */ - public class AppLogEvent implements RequestHandler { + public class PaymentFunction implements RequestHandler { - Logger log = LogManager.getLogger(AppLogEvent.class); + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentFunction.class); - @Logging(logEvent = true) + @Logging public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... + LOGGER.info("Collecting payment"); + // ... + LOGGER.debug("order={}, amount={}", order.getId(), order.getAmount()); + // ... } } ``` -### Customising fields in logs +## Standard structured keys -- Utility by default emits `timestamp` field in the logs in format `yyyy-MM-dd'T'HH:mm:ss.SSSZz` and in system default timezone. -If you need to customize format and timezone, you can do so by configuring `log4j2.component.properties` and configuring properties as shown in example below: +Your logs will always include the following keys in your structured logging: -=== "log4j2.component.properties" +| Key | Type | Example | Description | +|-------------------|--------|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| +| **timestamp** | String | "2023-12-01T14:49:19.293Z" | Timestamp of actual log statement, by default uses default AWS Lambda timezone (UTC) | +| **level** | String | "INFO" | Logging level (any level supported by _SLF4J_ (i.e. `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`) | +| **service** | String | "payment" | Service name defined, by default `service_undefined` | +| **sampling_rate** | float | 0.1 | Debug logging sampling rate in percentage e.g. 10% in this case (logged if not 0) | +| **message** | String | "Collecting payment" | Log statement value. Unserializable JSON values will be casted to string | +| **xray_trace_id** | String | "1-5759e988-bd862e3fe1be46a994272793" | X-Ray Trace ID when [Tracing is enabled](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html){target="_blank"} | +| **error** | Map | `{ "name": "InvalidAmountException", "message": "Amount must be superior to 0", "stack": "at..." }` | Eventual exception (e.g. when doing `logger.error("Error", new InvalidAmountException("Amount must be superior to 0"));`) | - ```properties hl_lines="1 2" - log4j.layout.jsonTemplate.timestampFormatPattern=yyyy-MM-dd'T'HH:mm:ss.SSSZz - log4j.layout.jsonTemplate.timeZone=Europe/Oslo - ``` +???+ note + If you emit a log message with a key that matches one of the [standard structured keys](#standard-structured-keys) or one of the [additional structured keys](#additional-structured-keys), the Logger will log a warning message and ignore the key. -- Utility also provides sample template for [Elastic Common Schema(ECS)](https://www.elastic.co/guide/en/ecs/current/ecs-reference.html) layout. -The field emitted in logs will follow specs from [ECS](https://www.elastic.co/guide/en/ecs/current/ecs-reference.html) together with field captured by utility as mentioned [above](#standard-structured-keys). +## Additional structured keys - Use `LambdaEcsLayout.json` as `eventTemplateUri` when configuring `JsonTemplateLayout`. +### Logging Lambda context information +The following keys will also be added to all your structured logs (unless [configured otherwise](#more-customization_1)): -=== "log4j2.xml" +| Key | Type | Example | Description | +|--------------------------|---------|----------------------------------------------------------------------------------------|------------------------------------| +| **cold_start** | Boolean | false | ColdStart value | +| **function_name** | String | "example-PaymentFunction-1P1Z6B39FLU73" | Name of the function | +| **function_version** | String | "12" | Version of the function | +| **function_memory_size** | String | "512" | Memory configure for the function | +| **function_arn** | String | "arn:aws:lambda:eu-west-1:012345678910:function:example-PaymentFunction-1P1Z6B39FLU73" | ARN of the function | +| **function_request_id** | String | "899856cb-83d1-40d7-8611-9e78f15f32f4"" | AWS Request ID from lambda context | - ```xml hl_lines="5" - - - - - - - - - - - - - - - - - ``` +### Logging additional keys -## Setting a Correlation ID +#### Logging a correlation ID -You can set a Correlation ID using `correlationIdPath` attribute by passing a [JSON Pointer expression](https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-03){target="_blank"}. +You can set a correlation ID using the `correlationIdPath` attribute of the `@Logging`annotation, +by passing a [JMESPath expression](https://jmespath.org/tutorial.html){target="_blank"}, +including our custom [JMESPath Functions](../utilities/serialization.md#built-in-functions). -=== "App.java" +=== "AppCorrelationIdPath.java" - ```java hl_lines="8" - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { + ```java hl_lines="5" + public class AppCorrelationIdPath implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppCorrelationIdPath.class); - @Logging(correlationIdPath = "/headers/my_request_id_header") + @Logging(correlationIdPath = "headers.my_request_id_header") public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... - log.info("Collecting payment") - ... + // ... + LOGGER.info("Collecting payment") + // ... } } ``` -=== "Example Event" +=== "Example HTTP Event" ```json hl_lines="3" { @@ -333,42 +405,37 @@ You can set a Correlation ID using `correlationIdPath` attribute by passing a [J } ``` -=== "Example CloudWatch Logs excerpt" +=== "CloudWatch Logs" - ```json hl_lines="11" + ```json hl_lines="6" { "level": "INFO", "message": "Collecting payment", - "timestamp": "2021-05-03 11:47:12,494+0200", + "timestamp": "2023-12-01T14:49:19.293Z", "service": "payment", - "coldStart": true, - "functionName": "test", - "functionMemorySize": 128, - "functionArn": "arn:aws:lambda:eu-west-1:12345678910:function:test", - "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72", "correlation_id": "correlation_id_value" } ``` -We provide [built-in JSON Pointer expression](https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-03){target="_blank"} -for known event sources, where either a request ID or X-Ray Trace ID are present. + +**Known correlation IDs** -=== "App.java" +To ease routine tasks like extracting correlation ID from popular event sources, +we provide [built-in JMESPath expressions](#built-in-correlation-id-expressions). - ```java hl_lines="10" - import software.amazon.lambda.powertools.logging.CorrelationIdPathConstants; +=== "AppCorrelationId.java" - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { + ```java hl_lines="1 7" + import software.amazon.lambda.powertools.logging.CorrelationIdPaths; + + public class AppCorrelationId implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppCorrelationId.class); - @Logging(correlationIdPath = CorrelationIdPathConstants.API_GATEWAY_REST) + @Logging(correlationIdPath = CorrelationIdPaths.API_GATEWAY_REST) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... - log.info("Collecting payment") - ... + // ... + LOGGER.info("Collecting payment") + // ... } } ``` @@ -377,316 +444,613 @@ for known event sources, where either a request ID or X-Ray Trace ID are present ```json hl_lines="3" { - "requestContext": { - "requestId": "correlation_id_value" - } + "requestContext": { + "requestId": "correlation_id_value" + } } ``` -=== "Example CloudWatch Logs excerpt" +=== "Example CloudWatch Logs" - ```json hl_lines="11" + ```json hl_lines="6" { "level": "INFO", "message": "Collecting payment", - "timestamp": "2021-05-03 11:47:12,494+0200", + "timestamp": "2023-12-01T14:49:19.293Z", "service": "payment", - "coldStart": true, - "functionName": "test", - "functionMemorySize": 128, - "functionArn": "arn:aws:lambda:eu-west-1:12345678910:function:test", - "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72", "correlation_id": "correlation_id_value" } ``` - -## Appending additional keys -!!! info "Custom keys are persisted across warm invocations" - Always set additional keys as part of your handler to ensure they have the latest value, or explicitly clear them with [`clearState=true`](#clearing-all-state). +#### Custom keys -You can append your own keys to your existing logs via `appendKey`. +**Using StructuredArguments** -=== "App.java" +To append additional keys in your logs, you can use the `StructuredArguments` class: - ```java hl_lines="11 19" - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { +=== "PaymentFunction.java" + + ```java hl_lines="1 2 11 17" + import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; + import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entries; + + public class PaymentFunction implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppLogResponse.class); - @Logging(logEvent = true) + @Logging public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... - LoggingUtils.appendKey("test", "willBeLogged"); - ... + // ... + LOGGER.info("Collecting payment", entry("orderId", order.getId())); - ... - Map customKeys = new HashMap<>(); - customKeys.put("test", "value"); - customKeys.put("test1", "value1"); - - LoggingUtils.appendKeys(customKeys); - ... + // ... + Map customKeys = new HashMap<>(); + customKeys.put("paymentId", payment.getId()); + customKeys.put("amount", payment.getAmount); + LOGGER.info("Payment successful", entries(customKeys)); } } ``` +=== "CloudWatch Logs for PaymentFunction" -### Removing additional keys + ```json hl_lines="7 16-18" + { + "level": "INFO", + "message": "Collecting payment", + "service": "payment", + "timestamp": "2023-12-01T14:49:19.293Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5", + "orderId": "41376" + } + ... + { + "level": "INFO", + "message": "Payment successful", + "service": "payment", + "timestamp": "2023-12-01T14:49:20.118Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5", + "orderId": "41376", + "paymentId": "3245", + "amount": 345.99 + } + ``` -You can remove any additional key from entry using `LoggingUtils.removeKeys()`. +`StructuredArguments` provides several options: -=== "App.java" + - `entry` to add one key and value into the log structure. Note that value can be any object type. + - `entries` to add multiple keys and values (from a Map) into the log structure. Note that values can be any object type. + - `json` to add a key and raw json (string) as value into the log structure. + - `array` to add one key and multiple values into the log structure. Note that values can be any object type. - ```java hl_lines="19 20" - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { +=== "OrderFunction.java" + + ```java hl_lines="1 2 11 17" + import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; + import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.array; + + public class OrderFunction implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppLogResponse.class); - @Logging(logEvent = true) + @Logging public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... - LoggingUtils.appendKey("test", "willBeLogged"); - ... - Map customKeys = new HashMap<>(); - customKeys.put("test1", "value"); - customKeys.put("test2", "value1"); - - LoggingUtils.appendKeys(customKeys); - ... - LoggingUtils.removeKey("test"); - LoggingUtils.removeKeys("test1", "test2"); - ... + // ... + LOGGER.info("Processing order", entry("order", order), array("products", productList)); + // ... } } ``` -### Clearing all state +=== "CloudWatch Logs for OrderFunction" + + ```json hl_lines="7 13" + { + "level": "INFO", + "message": "Processing order", + "service": "payment", + "timestamp": "2023-12-01T14:49:19.293Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5", + "order": { + "orderId": 23542, + "amount": 459.99, + "date": "2023-12-01T14:49:19.018Z", + "customerId": 328496 + }, + "products": [ + { + "productId": 764330, + "name": "product1", + "quantity": 1, + "price": 300 + }, + { + "productId": 798034, + "name": "product42", + "quantity": 1, + "price": 159.99 + } + ] + } + ``` -Logger is commonly initialized in the global scope. Due to [Lambda Execution Context reuse](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html), -this means that custom keys can be persisted across invocations. If you want all custom keys to be deleted, you can use -`clearState=true` attribute on `@Logging` annotation. +???+ tip "Use arguments without log placeholders" + As shown in the example above, you can use arguments (with `StructuredArguments`) without placeholders (`{}`) in the message. + If you add the placeholders, the arguments will be logged both as an additional field and also as a string in the log message, using the `toString()` method. + === "Function1.java" + + ```java + LOGGER.info("Processing {}", entry("order", order)); + ``` + + === "Order.java" + + ```java hl_lines="5" + public class Order { + // ... + + @Override + public String toString() { + return "Order{" + + "orderId=" + id + + ", amount=" + amount + + ", date='" + date + '\'' + + ", customerId=" + customerId + + '}'; + } + } + ``` + + === "CloudWatch Logs Function1" + + ```json hl_lines="3 7" + { + "level": "INFO", + "message": "Processing order=Order{orderId=23542, amount=459.99, date='2023-12-01T14:49:19.018Z', customerId=328496}", + "service": "payment", + "timestamp": "2023-12-01T14:49:19.293Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5", + "order": { + "orderId": 23542, + "amount": 459.99, + "date": "2023-12-01T14:49:19.018Z", + "customerId": 328496 + } + } + ``` + + You can also combine structured arguments with non structured ones. For example: + + === "Function2.java" + ```java + LOGGER.info("Processing order {}", order.getOrderId(), entry("order", order)); + ``` + + === "CloudWatch Logs Function2" + ```json + { + "level": "INFO", + "message": "Processing order 23542", + "service": "payment", + "timestamp": "2023-12-01T14:49:19.293Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5", + "order": { + "orderId": 23542, + "amount": 459.99, + "date": "2023-12-01T14:49:19.018Z", + "customerId": 328496 + } + } + ``` -=== "App.java" +???+ warning "Do not use reserved keys in `StructuredArguments`" + If the key name of your structured argument matches any of the [standard structured keys](#standard-structured-keys) or any of the [additional structured keys](#additional-structured-keys), the Logger will log a warning message and ignore the key. This is to protect you from accidentally overwriting reserved keys such as the log level or Lambda context information. + +**Using MDC** - ```java hl_lines="8 12" - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { +Mapped Diagnostic Context (MDC) is essentially a Key-Value store. It is supported by the [SLF4J API](https://www.slf4j.org/manual.html#mdc){target="_blank"}, +[logback](https://logback.qos.ch/manual/mdc.html){target="_blank"} and log4j (known as [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html){target="_blank"}). You can use the following standard: + +`MDC.put("key", "value");` + +???+ warning "Custom keys stored in the MDC are persisted across warm invocations" + Always set additional keys as part of your handler method to ensure they have the latest value, or explicitly clear them with [`clearState=true`](#clearing-state). + +???+ warning "Do not add reserved keys to MDC" + Avoid adding any of the keys listed in [standard structured keys](#standard-structured-keys) and [additional structured keys](#additional-structured-keys) to your MDC. This may cause unindented behavior and will overwrite the context set by the Logger. Unlike with `StructuredArguments`, the Logger will **not** ignore reserved keys set via MDC. + + +### Removing additional keys + +You can remove additional keys added with the MDC using `MDC.remove("key")`. + +#### Clearing state + +Logger is commonly initialized in the global scope. Due to [Lambda Execution Context reuse](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html){target="_blank"}, +this means that custom keys, added with the MDC can be persisted across invocations. If you want all custom keys to be deleted, you can use +`clearState=true` attribute on the `@Logging` annotation. + +=== "CreditCardFunction.java" + + ```java hl_lines="5 8" + public class CreditCardFunction implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(CreditCardFunction.class); @Logging(clearState = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... - if(input.getHeaders().get("someSpecialHeader")) { - LoggingUtils.appendKey("specialKey", "value"); - } - - log.info("Collecting payment"); - ... + // ... + MDC.put("cardNumber", card.getId()); + LOGGER.info("Updating card information"); + // ... } } ``` + === "#1 Request" - ```json hl_lines="11" - { - "level": "INFO", - "message": "Collecting payment", - "timestamp": "2021-05-03 11:47:12,494+0200", - "service": "payment", - "coldStart": true, - "functionName": "test", - "functionMemorySize": 128, - "functionArn": "arn:aws:lambda:eu-west-1:12345678910:function:test", - "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72", - "specialKey": "value" - } + ```json hl_lines="7" + { + "level": "INFO", + "message": "Updating card information", + "service": "card", + "timestamp": "2023-12-01T14:49:19.293Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5", + "cardNumber": "6818 8419 9395 5322" + } ``` === "#2 Request" - ```json - { - "level": "INFO", - "message": "Collecting payment", - "timestamp": "2021-05-03 11:47:12,494+0200", - "service": "payment", - "coldStart": true, - "functionName": "test", - "functionMemorySize": 128, - "functionArn": "arn:aws:lambda:eu-west-1:12345678910:function:test", - "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72" - } + ```json hl_lines="7" + { + "level": "INFO", + "message": "Updating card information", + "service": "card", + "timestamp": "2023-12-01T14:49:20.213Z", + "xray_trace_id": "2-7a518f43-5e9d2b1f6cfd5e8b3a4e1f9c", + "cardNumber": "7201 6897 6685 3285" + } ``` -## Override default object mapper +`clearState` is based on `MDC.clear()`. State clearing is automatically done at the end of the execution of the handler if set to `true`. -You can optionally choose to override default object mapper which is used to serialize lambda function events. You might -want to supply custom object mapper in order to control how serialisation is done, for example, when you want to log only -specific fields from received event due to security. -=== "App.java" +## Logging incoming event - ```java hl_lines="9 10" - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { +When debugging in non-production environments, you can instruct the `@Logging` annotation to log the incoming event with `logEvent` param or via `POWERTOOLS_LOGGER_LOG_EVENT` env var. + +???+ warning + This is disabled by default to prevent sensitive info being logged + +=== "AppLogEvent.java" + + ```java hl_lines="5" + public class AppLogEvent implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppLogEvent.class); + + @Logging(logEvent = true) + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + // ... + } + } + ``` + +???+ note + If you use this on a RequestStreamHandler, the SDK must duplicate input streams in order to log them. + +## Logging handler response - static { - ObjectMapper objectMapper = new ObjectMapper(); - LoggingUtils.defaultObjectMapper(objectMapper); +When debugging in non-production environments, you can instruct the `@Logging` annotation to log the response with `logResponse` param or via `POWERTOOLS_LOGGER_LOG_RESPONSE` env var. + +???+ warning + This is disabled by default to prevent sensitive info being logged + +=== "AppLogResponse.java" + + ```java hl_lines="5" + public class AppLogResponse implements RequestHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(AppLogResponse.class); + + @Logging(logResponse = true) + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + // ... } + } + ``` + +???+ note + If you use this on a RequestStreamHandler, Powertools must duplicate output streams in order to log them. + +## Logging handler uncaught exception +By default, AWS Lambda logs any uncaught exception that might happen in the handler. However, this log is not structured +and does not contain any additional context. You can instruct the `@Logging` annotation to log this kind of exception +with `logError` param or via `POWERTOOLS_LOGGER_LOG_ERROR` env var. + +???+ warning + This is disabled by default to prevent double logging + +=== "AppLogResponse.java" + + ```java hl_lines="5" + public class AppLogError implements RequestHandler { - @Logging(logEvent = true) + private static final Logger LOGGER = LoggerFactory.getLogger(AppLogError.class); + + @Logging(logError = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... + // ... } } ``` +# Advanced + ## Sampling debug logs -You can dynamically set a percentage of your logs to **DEBUG** level via env var `POWERTOOLS_LOGGER_SAMPLE_RATE` or -via `samplingRate` attribute on annotation. +You can dynamically set a percentage of your logs to`DEBUG` level to be included in the logger output, regardless of configured log leve, using the`POWERTOOLS_LOGGER_SAMPLE_RATE` environment variable or +via `samplingRate` attribute on the `@Logging` annotation. !!! info Configuration on environment variable is given precedence over sampling rate configuration on annotation, provided it's in valid value range. === "Sampling via annotation attribute" - ```java hl_lines="8" - /** - * Handler for requests to Lambda function. - */ + ```java hl_lines="5" public class App implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(App.class); @Logging(samplingRate = 0.5) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... + // will eventually be logged based on the sampling rate + LOGGER.debug("Handle payment"); } } ``` === "Sampling via environment variable" - ```yaml hl_lines="9" + ```yaml hl_lines="8" Resources: - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - ... - Runtime: java8 - Environment: - Variables: - POWERTOOLS_LOGGER_SAMPLE_RATE: 0.5 + PaymentFunction: + Type: AWS::Serverless::Function + Properties: + ... + Environment: + Variables: + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.5 + ``` -## AWS Lambda Advanced Logging Controls (ALC) +## Built-in Correlation ID expressions -!!!question "When is it useful?" - When you want to set a logging policy to drop informational or verbose logs for one or all AWS Lambda functions, regardless of runtime and logger used. +You can use any of the following built-in JMESPath expressions as part of `@Logging(correlationIdPath = ...)`: - -With [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced){target="_blank"}, you can enforce a minimum log level that Lambda will accept from your application code. +???+ note "Note: Any object key named with `-` must be escaped" + For example, **`request.headers."x-amzn-trace-id"`**. -When enabled, you should keep `Logger` and ALC log level in sync to avoid data loss. +| Name | Expression | Description | +|-------------------------------|-------------------------------------|---------------------------------| +| **API_GATEWAY_REST** | `"requestContext.requestId"` | API Gateway REST API request ID | +| **API_GATEWAY_HTTP** | `"requestContext.requestId"` | API Gateway HTTP API request ID | +| **APPSYNC_RESOLVER** | `request.headers."x-amzn-trace-id"` | AppSync X-Ray Trace ID | +| **APPLICATION_LOAD_BALANCER** | `headers."x-amzn-trace-id"` | ALB X-Ray Trace ID | +| **EVENT_BRIDGE** | `"id"` | EventBridge Event ID | -Here's a sequence diagram to demonstrate how ALC will drop both `INFO` and `DEBUG` logs emitted from `Logger`, when ALC log level is stricter than `Logger`. - -```mermaid -sequenceDiagram - participant Lambda service - participant Lambda function - participant Application Logger +## Customising fields in logs - Note over Lambda service: AWS_LAMBDA_LOG_LEVEL="WARN" - Note over Application Logger: POWERTOOLS_LOG_LEVEL="DEBUG" +Powertools for AWS Lambda comes with default json structure ([standard fields](#standard-structured-keys) & [lambda context fields](#logging-lambda-context-information)). - Lambda service->>Lambda function: Invoke (event) - Lambda function->>Lambda function: Calls handler - Lambda function->>Application Logger: logger.error("Something happened") - Lambda function-->>Application Logger: logger.debug("Something happened") - Lambda function-->>Application Logger: logger.info("Something happened") - Lambda service--xLambda service: DROP INFO and DEBUG logs - Lambda service->>CloudWatch Logs: Ingest error logs +You can go further and customize which fields you want to keep in your logs or not. The configuration varies according to the underlying logging library. + +### Log4j2 configuration +Log4j2 configuration is done in _log4j2.xml_ and leverages `JsonTemplateLayout`: + +```xml + + + ``` -### Priority of log level settings in Powertools for AWS Lambda +The `JsonTemplateLayout` is automatically configured with the provided template: -We prioritise log level settings in this order: +??? example "LambdaJsonLayout.json" + ```json + { + "level": { + "$resolver": "level", + "field": "name" + }, + "message": { + "$resolver": "powertools", + "field": "message" + }, + "error": { + "message": { + "$resolver": "exception", + "field": "message" + }, + "name": { + "$resolver": "exception", + "field": "className" + }, + "stack": { + "$resolver": "exception", + "field": "stackTrace", + "stackTrace": { + "stringified": true + } + } + }, + "cold_start": { + "$resolver": "powertools", + "field": "cold_start" + }, + "function_arn": { + "$resolver": "powertools", + "field": "function_arn" + }, + "function_memory_size": { + "$resolver": "powertools", + "field": "function_memory_size" + }, + "function_name": { + "$resolver": "powertools", + "field": "function_name" + }, + "function_request_id": { + "$resolver": "powertools", + "field": "function_request_id" + }, + "function_version": { + "$resolver": "powertools", + "field": "function_version" + }, + "sampling_rate": { + "$resolver": "powertools", + "field": "sampling_rate" + }, + "service": { + "$resolver": "powertools", + "field": "service" + }, + "timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + }, + "xray_trace_id": { + "$resolver": "powertools", + "field": "xray_trace_id" + }, + "": { + "$resolver": "powertools" + } + } + ``` -1. `AWS_LAMBDA_LOG_LEVEL` environment variable -2. `POWERTOOLS_LOG_LEVEL` environment variable +You can create your own template and leverage the [PowertoolsResolver](https://github.com/aws-powertools/powertools-lambda-java/tree/v2/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java){target="_blank"} +and any other resolver to log the desired fields with the desired format. Some examples of customization are given below: -If you set `Logger` level lower than ALC, we will emit a warning informing you that your messages will be discarded by Lambda. +#### Customising date format -> **NOTE** -> -> With ALC enabled, we are unable to increase the minimum log level below the `AWS_LAMBDA_LOG_LEVEL` environment variable value, see [AWS Lambda service documentation](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-log-level){target="_blank"} for more details. +Utility by default emits `timestamp` field in the logs in format `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` and in system default timezone. +If you need to customize format and timezone, you can update your template.json or by configuring `log4j2.component.properties` as shown in examples below: -### Timestamp format +=== "my-custom-template.json" -When the Advanced Logging Controls feature is enabled, Powertools for AWS Lambda must comply with the timestamp format required by AWS Lambda, which is [RFC3339](https://www.rfc-editor.org/rfc/rfc3339). -In this case the format will be `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`. + ```json + { + "timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd HH:mm:ss", + "timeZone": "Europe/Paris", + } + }, + } + ``` -## Upgrade to JsonTemplateLayout from deprecated LambdaJsonLayout configuration in log4j2.xml +=== "log4j2.component.properties" -Prior to version [1.10.0](https://github.com/aws-powertools/powertools-lambda-java/releases/tag/v1.10.0), only supported way of configuring `log4j2.xml` was via ``. This plugin is -deprecated now and will be removed in future version. Switching to `JsonTemplateLayout` is straight forward. + ```properties hl_lines="1 2" + log4j.layout.jsonTemplate.timestampFormatPattern=yyyy-MM-dd'T'HH:mm:ss.SSSZz + log4j.layout.jsonTemplate.timeZone=Europe/Oslo + ``` -Below examples shows deprecated and new configuration of `log4j2.xml`. +See [`TimestampResolver` documentation](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html#event-template-resolver-timestamp){target="_blank"} for more details. -=== "Deprecated configuration of log4j2.xml" +???+ warning "Lambda Advanced Logging Controls date format" + When using the Lambda ALC, you must have a date format compatible with the [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) - ```xml hl_lines="5" - - - - - - - - - - - - - - - - - ``` +#### More customization +You can also customize how [exceptions are logged](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html#event-template-resolver-exception){target="_blank"}, and much more. +See the [JSON Layout template documentation](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html){target="_blank"} for more details. -=== "New configuration of log4j2.xml" +### Logback configuration +Logback configuration is done in _logback.xml_ and the `LambdaJsonEncoder`: + +```xml + + + + +``` + +The `LambdaJsonEncoder` can be customized in different ways: + +#### Customising date format +Utility by default emits `timestamp` field in the logs in format `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` and in system default timezone. +If you need to customize format and timezone, you can change use the following: + +```xml + + yyyy-MM-dd HH:mm:ss + Europe/Paris + +``` + +#### More customization + +- You can use a standard `ThrowableHandlingConverter` to customize the exception format (default is no converter). Example: + +```xml + + + 30 + 2048 + 20 + sun\.reflect\..*\.invoke.* + net\.sf\.cglib\.proxy\.MethodProxy\.invoke + + true + true + + +``` + +- You can choose to add information about threads (default is `false`): + +```xml + + true + +``` + +- You can even choose to remove Powertools information from the logs like function name, arn: + +```xml + + false + +``` + +## Elastic Common Schema (ECS) Support + +Utility also supports [Elastic Common Schema(ECS)](https://www.elastic.co/guide/en/ecs/current/ecs-reference.html){target="_blank"} format. +The field emitted in logs will follow specs from [ECS](https://www.elastic.co/guide/en/ecs/current/ecs-reference.html){target="_blank"} together with field captured by utility as mentioned [above](#standard-structured-keys). + +### Log4j2 configuration + +Use `LambdaEcsLayout.json` as `eventTemplateUri` when configuring `JsonTemplateLayout`. + +=== "log4j2.xml" ```xml hl_lines="5" - + - - - @@ -694,3 +1058,20 @@ Below examples shows deprecated and new configuration of `log4j2.xml`. ``` +### Logback configuration + +Use the `LambdaEcsEncoder` rather than the `LambdaJsonEncoder` when configuring the appender: + +=== "logback.xml" + + ```xml hl_lines="3" + + + + + + + + + + ``` diff --git a/docs/core/metrics.md b/docs/core/metrics.md index e06ab6d10..35d08b140 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -3,23 +3,29 @@ title: Metrics description: Core utility --- -Metrics creates custom metrics asynchronously by logging metrics to standard output following Amazon CloudWatch Embedded Metric Format (EMF). +Metrics creates custom metrics asynchronously by logging metrics to standard output following [Amazon CloudWatch Embedded Metric Format (EMF)](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html). -These metrics can be visualized through [Amazon CloudWatch Console](https://console.aws.amazon.com/cloudwatch/). +These metrics can be visualized through [Amazon CloudWatch Console](https://aws.amazon.com/cloudwatch/). -**Key features** +## Key features -* Aggregate up to 100 metrics using a single CloudWatch EMF object (large JSON blob). -* Validate against common metric definitions mistakes (metric unit, values, max dimensions, max metrics, etc). -* Metrics are created asynchronously by the CloudWatch service, no custom stacks needed. -* Context manager to create a one off metric with a different dimension. +- Aggregate up to 100 metrics using a single [CloudWatch EMF](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html){target="\_blank"} object (large JSON blob) +- Validating your metrics against common metric definitions mistakes (for example, metric unit, values, max dimensions, max metrics) +- Metrics are created asynchronously by the CloudWatch service. You do not need any custom stacks, and there is no impact to Lambda function latency +- Support for creating one off metrics with different dimensions +- GraalVM support ## Terminologies -If you're new to Amazon CloudWatch, there are two terminologies you must be aware of before using this utility: +If you're new to Amazon CloudWatch, there are some terminologies you must be aware of before using this utility: -* **Namespace**. It's the highest level container that will group multiple metrics from multiple services for a given application, for example `ServerlessEcommerce`. -* **Dimensions**. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example `ColdStart` metric by Payment `service`. +- **Namespace**. It's the highest level container that will group multiple metrics from multiple services for a given application, for example `ServerlessAirline`. +- **Dimensions**. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example `ColdStart` metric by `service`. +- **Metric**. It's the name of the metric, for example: `SuccessfulBooking` or `UpdatedBooking`. +- **Unit**. It's a value representing the unit of measure for the corresponding metric, for example: `Count` or `Seconds`. +- **Resolution**. It's a value representing the storage resolution for the corresponding metric. Metrics can be either `Standard` or `High` resolution. Read more about CloudWatch Periods [here](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition). + +Visit the AWS documentation for a complete explanation for [Amazon CloudWatch concepts](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html).
@@ -28,9 +34,7 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar ## Install - Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes. - -=== "Maven Java 11+" +=== "Maven" ```xml hl_lines="3-7 16 18 24-27" @@ -50,7 +54,7 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 11 11 @@ -62,6 +66,14 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar + + + org.aspectj + aspectjtools + + 1.9.22 + + @@ -75,102 +87,51 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar ``` -=== "Maven Java 1.8" - - ```xml hl_lines="3-7 16 18 24-27" - - ... - - software.amazon.lambda - powertools-metrics - {{ powertools.version }} - - ... - - ... - - - - ... - - org.codehaus.mojo - aspectj-maven-plugin - 1.14.0 - - 1.8 - 1.8 - 1.8 - - - software.amazon.lambda - powertools-metrics - - - - - - - compile - - - - - ... - - - ``` - -=== "Gradle Java 11+" +=== "Gradle" ```groovy hl_lines="3 11" plugins { id 'java' id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0' } - + repositories { mavenCentral() } - + dependencies { aspect 'software.amazon.lambda:powertools-metrics:{{ powertools.version }}' } - + sourceCompatibility = 11 targetCompatibility = 11 ``` -=== "Gradle Java 1.8" - - ```groovy hl_lines="3 11" - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' - } - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-metrics:{{ powertools.version }}' - } - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - ``` - ## Getting started -Metric has two global settings that will be used across all metrics emitted: +Metrics has three global settings that will be used across all metrics emitted. Use your application or main service as the metric namespace to easily group all metrics: -Setting | Description | Environment variable | Constructor parameter -------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- -**Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` -**Service** | Optionally, sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `service` +| Setting | Description | Environment variable | Decorator parameter | +| -------------------- | ------------------------------------------------------------------------------- | ---------------------------------- | ------------------- | +| **Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` | +| **Service** | Optionally, sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `service` | +| **Function name** | Function name used as dimension for the cold start metric | `POWERTOOLS_METRICS_FUNCTION_NAME` | `functionName` | +| **Disable Metrics** | Optionally, disables all metrics flushing | `POWERTOOLS_METRICS_DISABLED` | N/A | !!! tip "Use your application or main service as the metric namespace to easily group all metrics" +!!! info "`POWERTOOLS_METRICS_DISABLED` will not disable default metrics created by AWS services." + +### Order of Precedence of `Metrics` configuration + +The `Metrics` Singleton can be configured by three different interfaces. The following order of precedence applies: + +1. `@FlushMetrics` annotation +2. `MetricsBuilder` using Builder pattern (see [Advanced section](#usage-without-metrics-annotation)) +3. Environment variables (recommended) + +For most use-cases, we recommend using Environment variables and only overwrite settings in code where needed using either the `@FlushMetrics` annotation or `MetricsBuilder` if the annotation cannot be used. + === "template.yaml" ```yaml hl_lines="9 10" @@ -188,72 +149,159 @@ Setting | Description | Environment variable | Constructor parameter === "MetricsEnabledHandler.java" - ```java hl_lines="8" - import software.amazon.lambda.powertools.metrics.Metrics; - + ```java hl_lines="9" + import software.amazon.lambda.powertools.metrics.FlushMetrics; + import software.amazon.lambda.powertools.metrics.MetricsFactory; + public class MetricsEnabledHandler implements RequestHandler { - - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); - + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + @Override - @Metrics(namespace = "ExampleApplication", service = "booking") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - ... + // ... } } ``` -You can initialize Metrics anywhere in your code as many times as you need - It'll keep track of your aggregate metrics in memory. +`Metrics` is implemented as a Singleton to keep track of your aggregate metrics in memory and make them accessible anywhere in your code. To guarantee that metrics are flushed properly the `@FlushMetrics` annotation must be added on the lambda handler. + +!!!info "You can use the Metrics utility without the `@FlushMetrics` annotation and flush manually. Read more in the [advanced section below](#usage-without-metrics-annotation)." ## Creating metrics -You can create metrics using `putMetric`, and manually create dimensions for all your aggregate metrics using `putDimensions`. +You can create metrics using `addMetric`, and manually create dimensions for all your aggregate metrics using `addDimension`. Anywhere in your code, you can access the current `Metrics` Singleton using the `MetricsFactory`. === "MetricsEnabledHandler.java" - ```java hl_lines="11 12" + ```java hl_lines="13" + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; - import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; + import software.amazon.lambda.powertools.metrics.MetricsFactory; + import software.amazon.lambda.powertools.metrics.model.MetricUnit; public class MetricsEnabledHandler implements RequestHandler { - - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); - + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + @Override - @Metrics(namespace = "ExampleApplication", service = "booking") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - metricsLogger.putDimensions(DimensionSet.of("environment", "prod")); - metricsLogger.putMetric("SuccessfulBooking", 1, Unit.COUNT); - ... + metrics.addDimension("environment", "prod"); + metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); + // ... } } ``` -!!! tip "The `Unit` enum facilitate finding a supported metric unit by CloudWatch." +!!! tip "The `MetricUnit` enum facilitates finding a supported metric unit by CloudWatch." + + +!!! note "Metrics dimensions" + CloudWatch EMF supports a max of 9 dimensions per metric. The Metrics utility will validate this limit when adding dimensions. + -!!! note "Metrics overflow" - CloudWatch EMF supports a max of 100 metrics. Metrics utility will flush all metrics when adding the 100th metric while subsequent metrics will be aggregated into a new EMF object, for your convenience. +### Adding high-resolution metrics + +You can create [high-resolution metrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/publishingMetrics.html#high-resolution-metrics) +passing a `#!java MetricResolution.HIGH` to the `addMetric` method. If nothing is passed `#!java MetricResolution.STANDARD` will be used. + +=== "HigResMetricsHandler.java" + + ```java hl_lines="3 13" + import software.amazon.lambda.powertools.metrics.FlushMetrics; + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.model.MetricResolution; + + public class MetricsEnabledHandler implements RequestHandler { + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + + @Override + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") + public Object handleRequest(Object input, Context context) { + // ... + metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT, MetricResolution.HIGH); + } + } + ``` + + +!!! info "When is it useful?" + High-resolution metrics are data with a granularity of one second and are very useful in several situations such as telemetry, time series, real-time incident management, and others. + + +### Adding dimensions + +You can add dimensions to your metrics using the `addDimension` method. You can either pass key-value pairs or you can create higher cardinality dimensions using `DimensionSet`. + +=== "KeyValueDimensionHandler.java" + + ```java hl_lines="3 13" + import software.amazon.lambda.powertools.metrics.FlushMetrics; + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.model.MetricResolution; + + public class MetricsEnabledHandler implements RequestHandler { + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + + @Override + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") + public Object handleRequest(Object input, Context context) { + metrics.addDimension("Dimension", "Value"); + metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); + } + } + ``` + +=== "HighCardinalityDimensionHandler.java" + + ```java hl_lines="4 13-14" + import software.amazon.lambda.powertools.metrics.FlushMetrics; + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.model.MetricResolution; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + + public class MetricsEnabledHandler implements RequestHandler { + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + + @Override + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") + public Object handleRequest(Object input, Context context) { + // You can add up to 30 dimensions in a single DimensionSet + metrics.addDimension(DimensionSet.of("Dimension1", "Value1", "Dimension2", "Value2")); + metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); + } + } + ``` ### Flushing metrics -The `@Metrics` annotation **validates**, **serializes**, and **flushes** all your metrics. During metrics validation, -if no metrics are provided no exception will be raised. If metrics are provided, and any of the following criteria are -not met, `ValidationException` exception will be raised. +The `@FlushMetrics` annotation **validates**, **serializes**, and **flushes** all your metrics. During metrics validation, +if no metrics are provided no exception will be raised. If metrics are provided, and any of the following criteria are +not met, `IllegalStateException` or `IllegalArgumentException` exceptions will be raised. + !!! tip "Metric validation" - * Maximum of 9 dimensions + - Maximum of 30 dimensions (`Service` default dimension counts as a regular dimension) + - Dimension keys and values cannot be null or empty + - Metric values must be valid numbers + -If you want to ensure that at least one metric is emitted, you can pass `raiseOnEmptyMetrics = true` to the **@Metrics** annotation: +If you want to ensure that at least one metric is emitted, you can pass `raiseOnEmptyMetrics = true` to the `@FlushMetrics` annotation: === "MetricsRaiseOnEmpty.java" ```java hl_lines="6" - import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; public class MetricsRaiseOnEmpty implements RequestHandler { @Override - @Metrics(raiseOnEmptyMetrics = true) + @FlushMetrics(raiseOnEmptyMetrics = true) public Object handleRequest(Object input, Context context) { ... } @@ -262,17 +310,17 @@ If you want to ensure that at least one metric is emitted, you can pass `raiseOn ## Capturing cold start metric -You can capture cold start metrics automatically with `@Metrics` via the `captureColdStart` variable. +You can capture cold start metrics automatically with `@FlushMetrics` via the `captureColdStart` variable. === "MetricsColdStart.java" ```java hl_lines="6" - import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; public class MetricsColdStart implements RequestHandler { @Override - @Metrics(captureColdStart = true) + @FlushMetrics(captureColdStart = true) public Object handleRequest(Object input, Context context) { ... } @@ -281,33 +329,77 @@ You can capture cold start metrics automatically with `@Metrics` via the `captur If it's a cold start invocation, this feature will: -* Create a separate EMF blob solely containing a metric named `ColdStart` -* Add `FunctionName` and `Service` dimensions +- Create a separate EMF blob solely containing a metric named `ColdStart` +- Add `FunctionName` and `Service` dimensions This has the advantage of keeping cold start metric separate from your application metrics. +You can also specify a custom function name to be used in the cold start metric: + +=== "MetricsColdStartCustomFunction.java" + + ```java hl_lines="6" + import software.amazon.lambda.powertools.metrics.FlushMetrics; + + public class MetricsColdStartCustomFunction implements RequestHandler { + + @Override + @FlushMetrics(captureColdStart = true, functionName = "CustomFunction") + public Object handleRequest(Object input, Context context) { + ... + } + } + ``` + + +!!!tip "You can overwrite the default `Service` and `FunctionName` dimensions of the cold start metric" + Set `#!java @FlushMetrics(captureColdStart = false)` and use the `captureColdStartMetric` method manually: + + ```java hl_lines="6 8" + public class MetricsColdStartCustomFunction implements RequestHandler { + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + + @Override + @FlushMetrics(captureColdStart = false) + public Object handleRequest(Object input, Context context) { + metrics.captureColdStartMetric(context, DimensionSet.of("CustomDimension", "CustomValue")); + ... + } + } + ``` + + ## Advanced -## Adding metadata +### Adding metadata -You can use `putMetadata` for advanced use cases, where you want to metadata as part of the serialized metrics object. +You can use `addMetadata` for advanced use cases, where you want to add metadata as part of the serialized metrics object. + !!! info - **This will not be available during metrics visualization, use `dimensions` for this purpose.** + This will not be available during metrics visualization, use Dimensions for this purpose. + +!!! info + Adding metadata with a key that is the same as an existing metric will be ignored + === "App.java" - ```java hl_lines="8 9" + ```java hl_lines="13" + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; - import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; + import software.amazon.lambda.powertools.metrics.MetricsFactory; public class App implements RequestHandler { + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + @Override - @Metrics(namespace = "ServerlessAirline", service = "payment") + @FlushMetrics(namespace = "ServerlessAirline", service = "booking-service") public Object handleRequest(Object input, Context context) { - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); - metricsLogger().putMetadata("booking_id", "1234567890"); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + metrics.addMetadata("booking_id", "1234567890"); // Needs to be added BEFORE flushing ... } } @@ -315,79 +407,225 @@ You can use `putMetadata` for advanced use cases, where you want to metadata as This will be available in CloudWatch Logs to ease operations on high cardinal data. -## Overriding default dimension set +### Setting default dimensions + +By default, all metrics emitted via module captures `Service` as one of the default dimensions. This is either specified via `POWERTOOLS_SERVICE_NAME` environment variable or via `service` attribute on `Metrics` annotation. -By default, all metrics emitted via module captures `Service` as one of the default dimension. This is either specified via -`POWERTOOLS_SERVICE_NAME` environment variable or via `service` attribute on `Metrics` annotation. If you wish to override the default -Dimension, it can be done via `#!java MetricsUtils.defaultDimensions()`. +If you wish to set custom default dimensions, it can be done via `#!java metrics.setDefaultDimensions()`. You can also use the `MetricsBuilder` instead of the `MetricsFactory` to configure **and** retrieve the `Metrics` Singleton at the same time (see `MetricsBuilder.java` tab). === "App.java" - ```java hl_lines="8 9 10" + ```java hl_lines="13" + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; - import static software.amazon.lambda.powertools.metrics.MetricsUtils; - + import software.amazon.lambda.powertools.metrics.MetricsFactory; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + public class App implements RequestHandler { - - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); - - static { - MetricsUtils.defaultDimensions(DimensionSet.of("CustomDimension", "booking")); + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + + @Override + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") + public Object handleRequest(Object input, Context context) { + metrics.setDefaultDimensions(DimensionSet.of("CustomDimension", "booking", "Environment", "prod")); + ... } - + } + ``` + +=== "MetricsBuilder.java" + + ```java hl_lines="8-10" + import software.amazon.lambda.powertools.metrics.FlushMetrics; + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.MetricsFactory; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + + public class App implements RequestHandler { + + private static final Metrics metrics = MetricsBuilder.builder() + .withDefaultDimensions(DimensionSet.of("CustomDimension", "booking", "Environment", "prod")) + .build(); + @Override - @Metrics(namespace = "ExampleApplication", service = "booking") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); ... - MetricsUtils.withSingleMetric("Metric2", 1, Unit.COUNT, log -> {}); } } ``` -## Creating a metric with a different dimension + +!!!note + Overwriting the default dimensions will also overwrite the default `Service` dimension. If you wish to keep `Service` in your default dimensions, you need to add it manually. + -CloudWatch EMF uses the same dimensions across all your metrics. Use `withSingleMetric` if you have a metric that should have different dimensions. +### Creating a single metric with different configuration -!!! info - Generally, this would be an edge case since you [pay for unique metric](https://aws.amazon.com/cloudwatch/pricing/). Keep the following formula in mind: - **unique metric = (metric_name + dimension_name + dimension_value)** +You can create a single metric with its own namespace and dimensions using `flushSingleMetric`: === "App.java" - ```java hl_lines="7 8 9" - import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; + ```java hl_lines="12-18" + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.MetricsFactory; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + import software.amazon.lambda.powertools.metrics.model.MetricUnit; public class App implements RequestHandler { + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Override + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - }); + metrics.flushSingleMetric( + "CustomMetric", + 1, + MetricUnit.COUNT, + "CustomNamespace", + DimensionSet.of("CustomDimension", "value") // Dimensions are optional + ); } } ``` -## Creating metrics with different configurations + +!!! info + Generally, this would be an edge case since you [pay for unique metric](https://aws.amazon.com/cloudwatch/pricing). Keep the following formula in mind: + + **unique metric = (metric_name + dimension_name + dimension_value)** + + +### Usage without `@FlushMetrics` annotation + +The `Metrics` Singleton provides all configuration options via `MetricsBuilder` in addition to the `@FlushMetrics` annotation. This can be useful if work in an environment or framework that does not leverage the vanilla Lambda `handleRequest` method. -Use `withMetricsLogger` if you have one or more metrics that should have different configurations e.g. dimensions or namespace. +!!!info "The environment variables for Service and Namespace configuration still apply but can be overwritten with `MetricsBuilder` if needed." + +The following example shows how to configure a custom `Metrics` Singleton using the Builder pattern. Note that it is necessary to manually flush metrics now. === "App.java" - ```java hl_lines="7 8 9 10 11 12 13" - import static software.amazon.lambda.powertools.metrics.MetricsUtils.withMetricsLogger; + ```java hl_lines="7-12 19 23" + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.MetricsBuilder; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + import software.amazon.lambda.powertools.metrics.model.MetricUnit; public class App implements RequestHandler { + // Create and configure a Metrics singleton without annotation + private static final Metrics customMetrics = MetricsBuilder.builder() + .withNamespace("ServerlessAirline") + .withRaiseOnEmptyMetrics(true) + .withService("payment") + .build(); @Override public Object handleRequest(Object input, Context context) { - withMetricsLogger(logger -> { - // override default dimensions - logger.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - // add metrics - logger.putMetric("CustomMetrics1", 1, Unit.COUNT); - logger.putMetric("CustomMetrics2", 5, Unit.COUNT); - }); + // You can manually capture the cold start metric + // Lambda context is an optional argument if not available in your environment + // Dimensions are also optional. + customMetrics.captureColdStartMetric(context, DimensionSet.of("FunctionName", "MyFunction", "Service", "payment")); + + // Add metrics to the custom metrics singleton + customMetrics.addMetric("CustomMetric", 1, MetricUnit.COUNT); + customMetrics.flush(); } } ``` + +## Testing your code + +### Suppressing metrics output + +If you would like to suppress metrics output during your unit tests, you can use the `POWERTOOLS_DISABLE_METRICS` environment variable. For example, using Maven you can set in your build plugins: + +```xml + + org.apache.maven.plugins + maven-surefire-plugin + + + true + + + +``` + +### Asserting EMF output + +When unit testing your code, you can run assertions against the output generated by the `Metrics` Singleton. For the `EmfMetricsLogger`, you can assert the generated JSON blob following the [CloudWatch EMF specification](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html) against your expected output. + +Consider the following example where we redirect the standard output to a custom `PrintStream`. We use the Jackson library to parse the EMF output into a `JsonNode` and run assertions against that. + +```java hl_lines="23 28 33 50-55" +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.metrics.testutils.TestContext; + +class MetricsTestExample { + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @AfterEach + void tearDown() { + System.setOut(standardOut); + } + + @Test + void shouldCaptureMetricsFromAnnotatedHandler() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("test-metric")).isTrue(); + assertThat(rootNode.get("test-metric").asDouble()).isEqualTo(100.0); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("CustomNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("CustomService"); + } + + static class HandlerWithMetricsAnnotation implements RequestHandler, String> { + @Override + @FlushMetrics(namespace = "CustomNamespace", service = "CustomService") + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } +} +``` diff --git a/docs/core/tracing.md b/docs/core/tracing.md index 17e81b867..883f8db86 100644 --- a/docs/core/tracing.md +++ b/docs/core/tracing.md @@ -3,7 +3,7 @@ title: Tracing description: Core utility --- -Powertools tracing is an opinionated thin wrapper for [AWS X-Ray Java SDK](https://github.com/aws/aws-xray-sdk-java/) +The Tracing utility is an opinionated thin wrapper for [AWS X-Ray Java SDK](https://github.com/aws/aws-xray-sdk-java/) a provides functionality to reduce the overhead of performing common tracing tasks. ![Tracing showcase](../media/tracing_utility_showcase.png) @@ -14,12 +14,11 @@ a provides functionality to reduce the overhead of performing common tracing tas * Helper methods to improve the developer experience of creating new X-Ray subsegments. * Better developer experience when developing with multiple threads. * Auto patch supported modules by AWS X-Ray + * GraalVM support ## Install -Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes. - -=== "Maven Java 11+" +=== "Maven" ```xml hl_lines="3-7 16 18 24-27" @@ -39,7 +38,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 11 11 @@ -51,6 +50,14 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl + + + org.aspectj + aspectjtools + + 1.9.22 + + @@ -64,52 +71,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ``` -=== "Maven Java 1.8" - - ```xml hl_lines="3-7 16 18 24-27" - - ... - - software.amazon.lambda - powertools-tracing - {{ powertools.version }} - - ... - - ... - - - - ... - - org.codehaus.mojo - aspectj-maven-plugin - 1.14.0 - - 1.8 - 1.8 - 1.8 - - - software.amazon.lambda - powertools-tracing - - - - - - - compile - - - - - ... - - - ``` - -=== "Gradle Java 11+" +=== "Gradle" ```groovy hl_lines="3 11" plugins { @@ -129,27 +91,6 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl targetCompatibility = 11 ``` -=== "Gradle Java 1.8" - - ```groovy hl_lines="3 11" - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' - } - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-tracing:{{ powertools.version }}' - } - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - ``` - - ## Initialization Before your use this utility, your AWS Lambda function [must have permissions](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html#services-xray-permissions) to send traces to AWS X-Ray. @@ -376,8 +317,24 @@ under a subsegment, or you are doing multithreaded programming. Refer examples b ## Instrumenting SDK clients and HTTP calls -User should make sure to instrument the SDK clients explicitly based on the function dependency. Refer details on -[how to instrument SDK client with Xray](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java-awssdkclients.html) and [outgoing http calls](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java-httpclients.html). +Powertools for Lambda (Java) cannot intercept SDK clients instantiation to add X-Ray instrumentation. You should make sure to instrument the SDK clients explicitly. Refer details on +[how to instrument SDK client with Xray](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java.html#xray-sdk-java-awssdkclients) +and [outgoing http calls](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java.html#xray-sdk-java-httpclients). For example: + +=== "LambdaHandler.java" + + ```java hl_lines="1 2 7" + import com.amazonaws.xray.AWSXRay; + import com.amazonaws.xray.handlers.TracingHandler; + + public class LambdaHandler { + private AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard() + .withRegion(Regions.fromName(System.getenv("AWS_REGION"))) + .withRequestHandlers(new TracingHandler(AWSXRay.getGlobalRecorder())) + .build(); + // ... + } + ``` ## Testing your code diff --git a/docs/index.md b/docs/index.md index 06c9beb6d..43d1ea03b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,13 @@ title: Homepage description: Powertools for AWS Lambda (Java) --- -![aws provider](https://img.shields.io/badge/provider-AWS-orange?logo=amazon-aws&color=ff9900) ![Build status](https://github.com/aws-powertools/powertools-lambda-java/actions/workflows/build.yml/badge.svg) ![Maven Central](https://img.shields.io/maven-central/v/software.amazon.lambda/powertools-parent) +![aws provider](https://img.shields.io/badge/provider-AWS-orange?logo=amazon-aws&color=ff9900) ![Build status](https://github.com/aws-powertools/powertools-lambda-java/actions/workflows/publish-v2-snapshot.yml/badge.svg) ![Maven Central](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Faws.oss.sonatype.org%2Fcontent%2Frepositories%2Fsnapshots%2Fsoftware%2Famazon%2Flambda%2Fpowertools-parent%2Fmaven-metadata.xml +) + +???+ warning + You are browsing the documentation for Powertools for AWS Lambda (Java) - v2. This is a snapshot release and not stable! + Check out our stable [v1](https://docs.powertools.aws.dev/lambda/java/) documentation if this is not what you wanted. + The v2 maven snapshot repository can be found [here](https://aws.oss.sonatype.org/content/repositories/snapshots/software/amazon/lambda/) . Powertools for AWS Lambda (Java) is a suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. @@ -80,9 +86,7 @@ Powertools for AWS Lambda (Java) dependencies are available in Maven Central. Yo * [Maven](https://maven.apache.org/) * [Gradle](https://gradle.org) -Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes. - -=== "Maven Java 11+" +=== "Maven" ```xml @@ -102,6 +106,12 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl powertools-metrics {{ powertools.version }} + + + org.aspectj + aspectjrt + 1.9.22 + ... ... @@ -112,7 +122,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 11 11 @@ -132,6 +142,14 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl + + + org.aspectj + aspectjtools + + 1.9.22 + + @@ -145,70 +163,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ``` -=== "Maven Java 1.8" - - ```xml - - ... - - software.amazon.lambda - powertools-tracing - {{ powertools.version }} - - - software.amazon.lambda - powertools-logging - {{ powertools.version }} - - - software.amazon.lambda - powertools-metrics - {{ powertools.version }} - - ... - - ... - - - - ... - - org.codehaus.mojo - aspectj-maven-plugin - 1.14.0 - - 1.8 - 1.8 - 1.8 - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-metrics - - - - - - - compile - - - - - ... - - - ``` - -=== "Gradle Java 11+" +=== "Gradle" ```groovy @@ -237,36 +192,6 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl targetCompatibility = 11 ``` -=== "Gradle Java 1.8" - - ```groovy - - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' - } - - // the freefair aspect plugins targets gradle 7.6.1 - // https://docs.freefair.io/gradle-plugins/6.6.3/reference/ - wrapper { - gradleVersion = "7.6.1" - } - - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-logging:{{ powertools.version }}' - aspect 'software.amazon.lambda:powertools-tracing:{{ powertools.version }}' - aspect 'software.amazon.lambda:powertools-metrics:{{ powertools.version }}' - } - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - ``` - ???+ tip "Why a different configuration?" Powertools for AWS Lambda (Java) is using [AspectJ](https://eclipse.dev/aspectj/doc/released/progguide/starting.html) internally to handle annotations. Recently, in order to support Java 17 we had to move to `dev.aspectj:aspectj-maven-plugin` because @@ -275,7 +200,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl while `dev.aspectj:aspectj-maven-plugin` is based on AspectJ 1.9.8, compiled for Java 11+. ### Java Compatibility -Powertools for AWS Lambda (Java) supports all Java version from 8 up to 21 as well as the +Powertools for AWS Lambda (Java) supports all Java version from 11 up to 21 as well as the [corresponding Lambda runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html). For the following modules, Powertools for AWS Lambda (Java) leverages the **aspectj** library to provide annotations: @@ -300,24 +225,24 @@ You may need to add the good version of `aspectjrt` to your dependencies based o Use the following [dependency matrix](https://github.com/eclipse-aspectj/aspectj/blob/master/docs/dist/doc/JavaVersionCompatibility.md) between this library and the JDK: -| JDK version | aspectj version | -|-------------|-----------------| -| `1.8` | `1.9.7` | -| `11-17` | `1.9.20.1` | -| `21` | `1.9.21` | +| JDK version | aspectj version | +|-------------|------------------------| +| `11-17` | `1.9.20.1` (or higher) | +| `21` | `1.9.21` (or higher) | ## Environment variables !!! info **Explicit parameters take precedence over environment variables.** -| Environment variable | Description | Utility | -| ------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -| **POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics dimension and structured logging | All | -| **POWERTOOLS_METRICS_NAMESPACE** | Sets namespace used for metrics | [Metrics](./core/metrics) | -| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logging](./core/logging) | -| **POWERTOOLS_LOG_LEVEL** | Sets logging level | [Logging](./core/logging) | -| **POWERTOOLS_LOGGER_LOG_EVENT** | Enables/Disables whether to log the incoming event when using the aspect | [Logging](./core/logging) | -| **POWERTOOLS_TRACER_CAPTURE_RESPONSE** | Enables/Disables tracing mode to capture method response | [Tracing](./core/tracing) | -| **POWERTOOLS_TRACER_CAPTURE_ERROR** | Enables/Disables tracing mode to capture method error | [Tracing](./core/tracing) | - +| Environment variable | Description | Utility | +| -------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------- | +| **POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics dimension and structured logging | All | +| **POWERTOOLS_METRICS_NAMESPACE** | Sets namespace used for metrics | [Metrics](./core/metrics) | +| **POWERTOOLS_METRICS_FUNCTION_NAME** | Function name used as dimension for the cold start metric | [Metrics](./core/metrics) | +| **POWERTOOLS_METRICS_DISABLED** | Disables all flushing of metrics | [Metrics](./core/metrics) | +| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logging](./core/logging) | +| **POWERTOOLS_LOG_LEVEL** | Sets logging level | [Logging](./core/logging) | +| **POWERTOOLS_LOGGER_LOG_EVENT** | Enables/Disables whether to log the incoming event when using the aspect | [Logging](./core/logging) | +| **POWERTOOLS_TRACER_CAPTURE_RESPONSE** | Enables/Disables tracing mode to capture method response | [Tracing](./core/tracing) | +| **POWERTOOLS_TRACER_CAPTURE_ERROR** | Enables/Disables tracing mode to capture method error | [Tracing](./core/tracing) | diff --git a/docs/processes/maintainers.md b/docs/processes/maintainers.md index 6b3d9e126..8f7f6a8fd 100644 --- a/docs/processes/maintainers.md +++ b/docs/processes/maintainers.md @@ -14,21 +14,23 @@ This is document explains who the maintainers are, their responsibilities, and h ## Current Maintainers -| Maintainer | GitHub ID | Affiliation | -|-----------------------|---------------------------------------------------------------------------------| ----------- | -| Jerome Van Der Linden | [jeromevdl](https://github.com/jeromevdl){target="_blank"} | Amazon | -| Michele Ricciardi | [mriccia](https://github.com/mriccia){target="_blank"} | Amazon | -| Scott Gerring | [scottgerring](https://github.com/scottgerring){target="_blank"} | Amazon | +| Maintainer | GitHub ID | Affiliation | +| --------------- | -------------------------------------------------------------------- | ----------- | +| Philipp Page | [phipag](https://github.com/phipag){target="\_blank" rel="nofollow"} | Amazon | +| Simon Thulbourn | [sthulb](https://github.com/sthulb){target="\_blank" rel="nofollow"} | Amazon | ## Emeritus Previous active maintainers who contributed to this project. -| Maintainer | GitHub ID | Affiliation | -|----------------|-----------------------------------------------------------------------------------------|---------------| -| Mark Sailes | [msailes](https://github.com/msailes){target="_blank"} | Amazon | -| Pankaj Agrawal | [pankajagrawal16](https://github.com/pankajagrawal16){target="_blank"} | Former Amazon | -| Steve Houel | [stevehouel](https://github.com/stevehouel) | Amazon | +| Maintainer | GitHub ID | Affiliation | +| --------------------- | -------------------------------------------------------------------------------------- | ------------- | +| Jerome Van Der Linden | [jeromevdl](https://github.com/jeromevdl){target="\_blank" rel="nofollow"} | Amazon | +| Michele Ricciardi | [mriccia](https://github.com/mriccia){target="\_blank" rel="nofollow"} | Amazon | +| Scott Gerring | [scottgerring](https://github.com/scottgerring){target="\_blank" rel="nofollow"} | DataDog | +| Mark Sailes | [msailes](https://github.com/msailes){target="\_blank" rel="nofollow"} | Former Amazon | +| Pankaj Agrawal | [pankajagrawal16](https://github.com/pankajagrawal16){target="\_blank" rel="nofollow"} | Former Amazon | +| Steve Houel | [stevehouel](https://github.com/stevehouel){target="\_blank" rel="nofollow"} | Amazon | ## Labels diff --git a/docs/processes/versioning.md b/docs/processes/versioning.md new file mode 100644 index 000000000..8b12e0fa9 --- /dev/null +++ b/docs/processes/versioning.md @@ -0,0 +1,60 @@ +--- +title: Versioning and maintenance policy +description: Versioning and maintenance policy for Powertools for AWS Lambda (Python) +--- + +### Overview + +This document outlines the maintenance policy for Powertools for AWS Lambda and their underlying dependencies. AWS regularly provides Powertools for AWS Lambda with updates that may contain new features, enhancements, bug fixes, security patches, or documentation updates. Updates may also address changes with dependencies, language runtimes, and operating systems. Powertools for AWS Lambda is published to package managers (e.g. PyPi, NPM, Maven, NuGet), and are available as source code on GitHub. + +We recommend users to stay up-to-date with Powertools for AWS Lambda releases to keep up with the latest features, security updates, and underlying dependencies. Continued use of an unsupported Powertools for AWS Lambda version is not recommended and is done at the user’s discretion. + +!!! info "For brevity, we will interchangeably refer to Powertools for AWS Lambda as "SDK" _(Software Development Toolkit)_." + +### Versioning + +Powertools for AWS Lambda release versions are in the form of X.Y.Z where X represents the major version. Increasing the major version of an SDK indicates that this SDK underwent significant and substantial changes to support new idioms and patterns in the language. Major versions are introduced when public interfaces _(e.g. classes, methods, types, etc.)_, behaviors, or semantics have changed. Applications need to be updated in order for them to work with the newest SDK version. It is important to update major versions carefully and in accordance with the upgrade guidelines provided by AWS. + +### SDK major version lifecycle + +The lifecycle for major Powertools for AWS Lambda versions consists of 5 phases, which are outlined below. + +- **Developer Preview** (Phase 0) - During this phase, SDKs are not supported, should not be used in production environments, and are meant for early access and feedback purposes only. It is possible for future releases to introduce breaking changes. Once AWS identifies a release to be a stable product, it may mark it as a Release Candidate. Release Candidates are ready for GA release unless significant bugs emerge, and will receive full AWS support. +- **General Availability (GA)** (Phase 1) - During this phase, SDKs are fully supported. AWS will provide regular SDK releases that include support for new features, enhancements, as well as bug and security fixes. AWS will support the GA version of an SDK for _at least 24 months_, unless otherwise specified. +- **Maintenance Announcement** (Phase 2) - AWS will make a public announcement at least 6 months before an SDK enters maintenance mode. During this period, the SDK will continue to be fully supported. Typically, maintenance mode is announced at the same time as the next major version is transitioned to GA. +- **Maintenance** (Phase 3) - During the maintenance mode, AWS limits SDK releases to address critical bug fixes and security issues only. An SDK will not receive API updates for new or existing services, or be updated to support new regions. Maintenance mode has a _default duration of 6 months_, unless otherwise specified. +- **End-of-Support** (Phase 4) - When an SDK reaches end-of support, it will no longer receive updates or releases. Previously published releases will continue to be available via public package managers and the code will remain on GitHub. The GitHub repository may be archived. Use of an SDK which has reached end-of-support is done at the user’s discretion. We recommend users upgrade to the new major version. + +!!! note "Please note that the timelines shown below are illustrative and not binding" + +![Maintenance policy timelines](https://docs.aws.amazon.com/images/sdkref/latest/guide/images/maint-policy.png) + +### Dependency lifecycle + +Most AWS SDKs have underlying dependencies, such as language runtimes, AWS Lambda runtime, or third party libraries and frameworks. These dependencies are typically tied to the language community or the vendor who owns that particular component. Each community or vendor publishes their own end-of-support schedule for their product. + +The following terms are used to classify underlying third party dependencies: + +- [**AWS Lambda Runtime**](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html): Examples include `java17`, `nodejs20.x`, `python3.13`, etc. +- **Language Runtime**: Examples include Java 17, Python 3.13, NodeJS 20, .NET Core, etc. +- **Third party Library**: Examples include Jackson Project, AWS X-Ray SDK, AWS Encryption SDK, etc. + +Powertools for AWS Lambda follows the [AWS Lambda Runtime deprecation policy cycle](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html#runtime-support-policy), when it comes to Language Runtime. This means we will stop supporting their respective deprecated Language Runtime _(e.g., `java8`)_ without increasing the major SDK version. + +!!! note "AWS reserves the right to stop support for an underlying dependency without increasing the major SDK version" + +### Communication methods + +Maintenance announcements are communicated in several ways: + +- A pinned GitHub Request For Comments (RFC) issue indicating the campaign for the next major version. The RFC will outline the path to end-of-support, specify campaign timelines, and upgrade guidance. +- AWS SDK documentation, such as API reference documentation, user guides, SDK product marketing pages, and GitHub readme(s) are updated to indicate the campaign timeline and provide guidance on upgrading affected applications. +- Deprecation warnings are added to the SDKs, outlining the path to end-of-support and linking to the upgrade guide. + +To see the list of available major versions of Powertools for AWS Lambda and where they are in their maintenance lifecycle, see the [version support matrix](#version-support-matrix). + +### Version support matrix + +| SDK | Major version | Current Phase | General Availability Date | Notes | +| -------------------------------- | ------------- | -------------------- | ------------------------- | ------------------------------------------------------------------------------------------------- | +| Powertools for AWS Lambda (Java) | 1.x | General Availability | 11/04/2020 | See [Release notes](https://github.com/aws-powertools/powertools-lambda-java/releases/tag/v1.0.0) | diff --git a/docs/requirements.in b/docs/requirements.in new file mode 100644 index 000000000..e8acc7112 --- /dev/null +++ b/docs/requirements.in @@ -0,0 +1,3 @@ +mkdocs-git-revision-date-plugin==0.3.2 +mkdocs-macros-plugin==1.3.7 +mkdocs-llmstxt==0.2.0 diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..16529cf3b --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,296 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --generate-hashes --output-file=requirements.txt requirements.in +# +beautifulsoup4==4.13.3 \ + --hash=sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b \ + --hash=sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16 + # via + # markdownify + # mkdocs-llmstxt +click==8.1.8 \ + --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ + --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a + # via mkdocs +ghp-import==2.1.0 \ + --hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \ + --hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343 + # via mkdocs +gitdb==4.0.12 \ + --hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \ + --hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf + # via gitpython +gitpython==3.1.44 \ + --hash=sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110 \ + --hash=sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269 + # via mkdocs-git-revision-date-plugin +hjson==3.1.0 \ + --hash=sha256:55af475a27cf83a7969c808399d7bccdec8fb836a07ddbd574587593b9cdcf75 \ + --hash=sha256:65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89 + # via + # mkdocs-macros-plugin + # super-collections +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via + # mkdocs + # mkdocs-git-revision-date-plugin + # mkdocs-macros-plugin +markdown==3.7 \ + --hash=sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2 \ + --hash=sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803 + # via mkdocs +markdown-it-py==3.0.0 \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb + # via mdformat +markdownify==1.1.0 \ + --hash=sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef \ + --hash=sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd + # via mkdocs-llmstxt +markupsafe==3.0.2 \ + --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \ + --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ + --hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \ + --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \ + --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \ + --hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \ + --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ + --hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \ + --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ + --hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \ + --hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \ + --hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \ + --hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \ + --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \ + --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \ + --hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \ + --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \ + --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ + --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \ + --hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \ + --hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \ + --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \ + --hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \ + --hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \ + --hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \ + --hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \ + --hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \ + --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ + --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ + --hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \ + --hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \ + --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ + --hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \ + --hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \ + --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ + --hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \ + --hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \ + --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \ + --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ + --hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \ + --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \ + --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \ + --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \ + --hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \ + --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \ + --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \ + --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \ + --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \ + --hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \ + --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \ + --hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \ + --hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \ + --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ + --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \ + --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \ + --hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \ + --hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \ + --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \ + --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \ + --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \ + --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50 + # via + # jinja2 + # mkdocs +mdformat==0.7.22 \ + --hash=sha256:61122637c9e1d9be1329054f3fa216559f0d1f722b7919b060a8c2a4ae1850e5 \ + --hash=sha256:eef84fa8f233d3162734683c2a8a6222227a229b9206872e6139658d99acb1ea + # via mkdocs-llmstxt +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +mergedeep==1.3.4 \ + --hash=sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8 \ + --hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 + # via + # mkdocs + # mkdocs-get-deps +mkdocs==1.6.1 \ + --hash=sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2 \ + --hash=sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e + # via + # mkdocs-git-revision-date-plugin + # mkdocs-macros-plugin +mkdocs-get-deps==0.2.0 \ + --hash=sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c \ + --hash=sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134 + # via mkdocs +mkdocs-git-revision-date-plugin==0.3.2 \ + --hash=sha256:2e67956cb01823dd2418e2833f3623dee8604cdf223bddd005fe36226a56f6ef + # via -r requirements.in +mkdocs-llmstxt==0.2.0 \ + --hash=sha256:104f10b8101167d6baf7761942b4743869be3d8f8a8d909f4e9e0b63307f709e \ + --hash=sha256:907de892e0c8be74002e8b4d553820c2b5bbcf03cc303b95c8bca48fb49c1a29 + # via -r requirements.in +mkdocs-macros-plugin==1.3.7 \ + --hash=sha256:02432033a5b77fb247d6ec7924e72fc4ceec264165b1644ab8d0dc159c22ce59 \ + --hash=sha256:17c7fd1a49b94defcdb502fd453d17a1e730f8836523379d21292eb2be4cb523 + # via -r requirements.in +packaging==24.2 \ + --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ + --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f + # via + # mkdocs + # mkdocs-macros-plugin +pathspec==0.12.1 \ + --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ + --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 + # via + # mkdocs + # mkdocs-macros-plugin +platformdirs==4.3.7 \ + --hash=sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94 \ + --hash=sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351 + # via mkdocs-get-deps +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via + # ghp-import + # mkdocs-macros-plugin +pyyaml==6.0.2 \ + --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ + --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ + --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ + --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ + --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ + --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ + --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ + --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ + --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ + --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ + --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ + --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ + --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ + --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ + --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ + --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ + --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ + --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ + --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ + --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ + --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ + --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ + --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ + --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ + --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ + --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ + --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ + --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ + --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ + --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ + --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ + --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ + --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ + --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ + --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ + --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 + # via + # mkdocs + # mkdocs-get-deps + # mkdocs-macros-plugin + # pyyaml-env-tag +pyyaml-env-tag==0.1 \ + --hash=sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb \ + --hash=sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069 + # via mkdocs +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via + # markdownify + # python-dateutil +smmap==5.0.2 \ + --hash=sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5 \ + --hash=sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e + # via gitdb +soupsieve==2.6 \ + --hash=sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb \ + --hash=sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9 + # via beautifulsoup4 +super-collections==0.5.3 \ + --hash=sha256:907d35b25dc4070910e8254bf2f5c928348af1cf8a1f1e8259e06c666e902cff \ + --hash=sha256:94c1ec96c0a0d5e8e7d389ed8cde6882ac246940507c5e6b86e91945c2968d46 + # via mkdocs-macros-plugin +termcolor==3.0.1 \ + --hash=sha256:a6abd5c6e1284cea2934443ba806e70e5ec8fd2449021be55c280f8a3731b611 \ + --hash=sha256:da1ed4ec8a5dc5b2e17476d859febdb3cccb612be1c36e64511a6f2485c10c69 + # via mkdocs-macros-plugin +typing-extensions==4.13.2 \ + --hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \ + --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef + # via beautifulsoup4 +watchdog==6.0.0 \ + --hash=sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a \ + --hash=sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2 \ + --hash=sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f \ + --hash=sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c \ + --hash=sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c \ + --hash=sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c \ + --hash=sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0 \ + --hash=sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13 \ + --hash=sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134 \ + --hash=sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa \ + --hash=sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e \ + --hash=sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379 \ + --hash=sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a \ + --hash=sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11 \ + --hash=sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282 \ + --hash=sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b \ + --hash=sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f \ + --hash=sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c \ + --hash=sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112 \ + --hash=sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948 \ + --hash=sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881 \ + --hash=sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860 \ + --hash=sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3 \ + --hash=sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680 \ + --hash=sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26 \ + --hash=sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26 \ + --hash=sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e \ + --hash=sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8 \ + --hash=sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c \ + --hash=sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2 + # via mkdocs diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 000000000..4ef1ef06e --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,142 @@ +--- +title: Roadmap +description: Public roadmap for Powertools for AWS Lambda (Java) +--- + +## Overview + +Our public roadmap outlines the high level direction we are working towards. We update this document when our priorities change: security and stability are our top priority. + +### Key areas + +Security and operational excellence take precedence above all else. This means bug fixing, stability, customer's support, and internal compliance may delay one or more key areas below. + +!!! info "We may choose to re-prioritize or defer items based on customer feedback, security, and operational impacts, and business value." + +#### Release Security (p0) + +Our top priority is to establish the processes and infrastructure needed for a fully automated and secure end-to-end release process of new versions to Maven Central. + +- [x] [Implement GitHub workflows](https://github.com/aws-powertools/powertools-lambda-java/issues/1231){target="\_blank"} and create infrastructure to release to Maven Central +- [x] [Implement end-to-end tests](https://github.com/aws-powertools/powertools-lambda-java/issues/1815){target="\_blank"} +- [x] Implement [OpenSSF Scorecard](https://openssf.org/projects/scorecard/){target="\_blank"} + +#### `v2` Release: Consistency and Ecosystem (p1) + +As part of a new major version `v2` release, we prioritize the Java project's consistency of core utilities (Logging, Metrics, Tracing) with the other runtimes (Python, TypeScript, .NET). Additionally, we will focus on integrating the library with popular technologies and frameworks from the Java and AWS ecosystem. Particularly, we aim at leveraging new techniques to allow customers to reduce Lambda cold-start time. The `v2` release will also drop support for Java 8 moving to Java 11 as the baseline. + +##### Core Utilities + +- [x] [Review public interfaces and reduce public API surface area](https://github.com/aws-powertools/powertools-lambda-java/issues/1283){target="\_blank"} +- [x] [Release Logging `v2` module](https://github.com/aws-powertools/powertools-lambda-java/issues/965){target="\_blank"} allowing customers to choose the logging framework and adding support for logging deeply nested objects as JSON +- [x] [Support high resolution metrics](https://github.com/aws-powertools/powertools-lambda-java/issues/1041){target="\_blank"} +- [x] [Improve modularity of metrics module](https://github.com/aws-powertools/powertools-lambda-java/issues/1848){target="\_blank"} to remove coupling with EMF library and enable future support for additional metrics providers / backends + +##### Ecosystem + +- [x] [Add GraalVM support for core utilities](https://github.com/aws-powertools/powertools-lambda-java/issues/764){target="\_blank"} +- [ ] [Implement priming using CRaC to improve AWS Snapstart support](https://github.com/aws-powertools/powertools-lambda-java/issues/1588){target="\_blank"} +- [ ] [Evaluate integration with popular Java frameworks such as Micronaut, Spring Cloud Function, or Quarkus](https://github.com/aws-powertools/powertools-lambda-java/issues/1701){target="\_blank"} + +##### Other + +- [x] [Validation module integration with HTTP requests](https://github.com/aws-powertools/powertools-lambda-java/issues/1298){target="\_blank"} +- [x] [Support validation module from within the batch module](https://github.com/aws-powertools/powertools-lambda-java/issues/1496){target="\_blank"} +- [x] [Add support for parallel processing in Batch Processing utility](https://github.com/aws-powertools/powertools-lambda-java/issues/1540){target="\_blank"} +- [ ] [Documentation: Review and improve documentation to be consistent with other runtimes](https://github.com/aws-powertools/powertools-lambda-java/issues/1352){target="\_blank"} + +#### Feature Parity (p2) + +If priorities `p0` and `p1` are addressed, we will also focus on feature parity of non-core utilities. This allows customers to achieve better standardization of their development processes across different Powertools runtimes. + +- [ ] [Re-evaluate if there is a need for adding a lightweight customer Powertools event handler](https://github.com/aws-powertools/powertools-lambda-java/issues/1103){target="\_blank"} +- [ ] [Add comprehensive GraalVM support for all utilities](){target="\_blank"} +- [ ] [Add Feature Flags module](https://github.com/aws-powertools/powertools-lambda-java/issues/1086){target="\_blank"} +- [ ] [Add S3 Streaming module](https://github.com/aws-powertools/powertools-lambda-java/issues/1085){target="\_blank"} +- [ ] Add support for Data Masking during JSON serialization + +### Missing something? + +You can help us prioritize by [upvoting existing feature requests](https://github.com/aws-powertools/powertools-lambda-java/issues?q=is%3Aissue%20state%3Aopen%20label%3Aenhancement){target="\_blank"}, +leaving a comment on what use cases it could unblock for you, and by joining our discussions on Discord. + +[![Join our Discord](https://dcbadge.vercel.app/api/server/B8zZKbbyET)](https://discord.gg/B8zZKbbyET){target="\_blank"} + +### Roadmap status definition + +
+```mermaid +graph LR + Ideas --> Backlog --> Work["Working on it"] --> Merged["Coming soon"] --> Shipped +``` +Visual representation +
+ +Within our [public board](https://github.com/orgs/aws-powertools/projects/4/){target="\_blank"}, you'll see the following values in the `Status` column: + +- **Ideas**. Incoming and existing feature requests that are not being actively considered yet. These will be reviewed + when bandwidth permits. +- **Backlog**. Accepted feature requests or enhancements that we want to work on. +- **Working on it**. Features or enhancements we're currently either researching or implementing it. +- **Coming soon**. Any feature, enhancement, or bug fixes that have been merged and are coming in the next release. +- **Shipped**. Features or enhancements that are now available in the most recent release. + +> Tasks or issues with empty `Status` will be categorized in upcoming review cycles. + +### Process + +
+```mermaid +graph LR + PFR[Feature request] --> Triage{Need RFC?} + Triage --> |Complex/major change or new utility?| RFC[Ask or write RFC] --> Approval{Approved?} + Triage --> |Minor feature or enhancement?| NoRFC[No RFC required] --> Approval + Approval --> |Yes| Backlog + Approval --> |No | Reject["Inform next steps"] + Backlog --> |Prioritized| Implementation + Backlog --> |Defer| WelcomeContributions["help-wanted label"] +``` +Visual representation +
+ +Our end-to-end mechanism follows four major steps: + +- **Feature Request**. Ideas start with a [feature request](https://github.com/aws-powertools/powertools-lambda-java/issues/new?template=feature_request.md){target="\_blank"} to outline their use case at a high level. For complex use cases, maintainers might ask for/write a + RFC. + - Maintainers review requests based on [project tenets](index.md#tenets){target="\_blank"}, customers reaction (👍), + and use cases. +- **Request-for-comments (RFC)**. Design proposals use + our [RFC template](https://github.com/aws-powertools/powertools-lambda-java/issues/new?q=is%3Aissue+state%3Aopen+label%3Aenhancement&template=rfc.md){target="\_blank"} to describe its implementation, challenges, developer experience, dependencies, and alternative solutions. + - This helps refine the initial idea with community feedback before a decision is made. +- **Decision**. After carefully reviewing and discussing them, maintainers make a final decision on whether to start + implementation, defer or reject it, and update everyone with the next steps. +- **Implementation**. For approved features, maintainers give priority to the original authors for implementation unless + it is a sensitive task that is best handled by maintainers. + +!!! info "See [Maintainers](./processes/maintainers.md){target="\_blank"} document to understand how we triage issues and pull requests, labels and governance." + +### Disclaimer + +The Powertools for AWS Lambda (Java) team values feedback and guidance from its community of users, although final +decisions on inclusion into the project will be made by AWS. + +We determine the high-level direction for our open roadmap based on customer feedback and popularity (👍🏽 and comments), +security and operational impacts, and business value. Where features don’t meet our goals and longer-term strategy, we +will communicate that clearly and openly as quickly as possible with an explanation of why the decision was made. + +### FAQs + +**Q: Why did you build this?** + +A: We know that our customers are making decisions and plans based on what we are developing, and we want to provide our +customers the insights they need to plan. + +**Q: Why are there no dates on your roadmap?** + +A: Because job zero is security and operational stability, we can't provide specific target dates for features. The +roadmap is subject to change at any time, and roadmap issues in this repository do not guarantee a feature will be +launched as proposed. + +**Q: How can I provide feedback or ask for more information?** + +A: For existing features, you can directly comment on issues. For anything else, please open an issue. diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index d135d7210..dc08ef51e 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,5 +1,5 @@ .md-grid { - max-width: 81vw + max-width: 90vw } .highlight .hll { diff --git a/docs/upgrade.md b/docs/upgrade.md new file mode 100644 index 000000000..d1388d95b --- /dev/null +++ b/docs/upgrade.md @@ -0,0 +1,455 @@ +--- +title: Upgrade guide +description: Guide to update between major Powertools for AWS Lambda (Java) versions +--- + +## End of support v1 + + + +Given our commitment to all of our customers using Powertools for AWS Lambda (Java), we will keep [Maven Central](https://central.sonatype.com/search?q=powertools){target="\_blank"} `v1` releases and a `v1` documentation archive to prevent any disruption. + +## Migrate to v2 from v1 + +!!! info "We strongly encourage you to migrate to `v2`. Refer to our [versioning policy](./processes/versioning.md) to learn more about our version support process." + +We've made minimal breaking changes to make your transition to `v2` as smooth as possible. + +### Quick summary + +The following table shows a summary of the changes made in `v2` and whether code changes are necessary. Each change that requires a code change links to a section below explaining more details. + +| Area | Change | Code change required | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | +| **Logging** | The [logging module was re-designed](#redesigned-logging-utility) from scratch to support popular Java logging paradigms and libraries like `log4j2`, `logback`, and `slf4j`. | Yes | +| **Metrics** | [Changed public interface](#updated-metrics-utility-interface) to remove direct coupling with `aws-embedded-metrics-java`. | Yes | +| **Tracing** | [Removed deprecated `captureResponse` and `captureError` options](#deprecated-capture-mode-related-tracing-annotation-parameters) on `@Tracing` annotation. | Yes | +| **Idempotency** | The [`powertools-idempotency` module was split by provider](#idempotency-utility-split-into-sub-modules-by-provider) to improve modularity and reduce the deployment package size. | Yes | +| **Idempotency** | Updated `IdempotencyConfig` interface to support addition of response hooks. | No | +| **Parameters** | The [`powertools-parameters` module was split by provider](#parameters-utility-split-into-sub-modules-by-provider) to improve modularity and reduce the deployment package size. | Yes | +| **Batch Processing** | [Removed deprecated `powertools-sqs` module](#removed-powertools-sqs-module-in-favor-of-powertools-batch) in favor of the more generic [Batch Processing](./utilities/batch.md) utility. | Yes | +| **Batch Processing** | Updated Batch Processing `BatchMessageHandler` interface to add support for parallel processing. | No | +| **Validation** | The `@Validation` utility returns 4xx error codes instead of 5xx error codes when used with API Gateway now. | No | +| **Validation** | Validating batch event sources now adds failed events as partial batch failures and does not fail the whole batch anymore. | No | +| **Custom Resources** | [Removed deprecated `Response.failed()` and `Response.success()` methods](#custom-resources-updates-the-response-class). | Yes | +| **Custom Resources** | Changed interface of `Response` class to add an optional `reason` field. | No | +| **Dependencies** | Renamed `powertools-core` to `powertools-common`. This module should not be used as direct dependency and is listed here for completeness. | No | +| **Dependencies** | [Removed `org.aspectj.aspectjrt` as project dependency](#aspectj-runtime-not-included-by-default-anymore) in favor of consumers including the version they prefer. | Yes | +| **Language support** | Removed support for Java 8. The minimum required Java version is Java 11. | N/A | + +### First Steps + +Before you start, we suggest making a copy of your current working project or create a new branch with `git`. + +1. **Upgrade** Java to at least version 11. While version 11 is supported, we recommend using the [newest available LTS version](https://downloads.corretto.aws/#/downloads){target="\_blank"} of Java. +2. **Review** the following section to confirm if you need to make changes to your code. + +## Redesigned Logging Utility + + + +The logging utility was re-designed from scratch to integrate better with Java idiomatic conventions and to remove the hard dependency on `log4j` as logging implementation. The new logging utility now supports `slfj4` as logging interface and gives you the choice among `log4j2` and `logback` as logging implementations. Consider the following steps to migrate from the v1 logging utility to the v2 logging utility: + +**1. Remove `powertools-logging` dependency and replace it with your logging backend of choice** + +In order to support different logging implementations, dedicated logging modules were created for the different logging implementations. Remove `powertools-logging` as a dependency and replace it with either `powertools-logging-log4j` or `powertools-logging-logback`. + +```diff + +- +- software.amazon.lambda +- powertools-logging +- 1.x.x +- + + ++ ++ software.amazon.lambda ++ powertools-logging-log4j ++ 2.x.x ++ +``` + + +!!! info "The AspectJ configuration still needs to depend on `powertools-logging`" + We have only replaced the logging implementation dependency. The AspectJ configuration still needs to depend on `powertools-logging` which contains the main logic. + + ```xml + + software.amazon.lambda + powertools-logging + + ``` + + +**2. Update `log4j2.xml` including new `JsonTemplateLayout`** + +This step is only required if you are using log4j2 as your logging implementation. The deprecated `#!xml ` element was removed. Replace it with the log4j2 agnostic `#!xml ` element. + +```diff + + + + +- ++ + + + + + + + + + + + +``` + +**3. Migrate all logging specific calls to SLF4J native primitives (recommended)** + +The new logging utility is designed to integrate seamlessly with Java SLF4J to allow customers adopt the Logging utility without large code refactorings. This improvement requires the migration of non-native SLF4J primitives from the v1 Logging utility. + +!!! info "While we recommend using SLF4J as a logging implementation independent facade, you can still use the log4j2 and logback interfaces directly." + +Consider the following code example which gives you hints on how to achieve the same functionality between v1 and v2 Logging: + +```diff +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.logging.Logging; +// ... other imports + +public class PaymentFunction implements RequestHandler { + // BEFORE v2: Uses org.apache.logging.log4j.LogManager +- private static final Logger LOGGER = LogManager.getLogger(PaymentFunction.class); + // AFTER v2: Use org.slf4j.LoggerFactory ++ private static final Logger LOGGER = LoggerFactory.getLogger(PaymentFunction.class); + + @Logging + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + // ... + + // BEFORE v2: Uses LoggingUtils.appendKey to append custom global keys + // LoggingUtils was removed! +- LoggingUtils.appendKey("cardNumber", card.getId()); + // AFTER v2: Uses native SLF4J Mapped Diagnostic Context (MDC) ++ MDC.put("cardNumber", card.getId()); + + // Regular logging has not changed + LOGGER.info("My log message with argument."); + + // Adding custom keys on a specific log message + // BEFORE v2: No direct way, only supported via LoggingUtils.appendKey and LoggingUtils.removeKey + // AFTER v2: Extensive support for StructuredArguments ++ LOGGER.info("Collecting payment", StructuredArguments.entry("orderId", order.getId())); + // { "message": "Collecting payment", ..., "orderId": 123} + Map customKeys = new HashMap<>(); + customKeys.put("paymentId", payment.getId()); + customKeys.put("amount", payment.getAmount); ++ LOGGER.info("Payment successful", StructuredArguments.entries(customKeys)); + // { "message": "Payment successful", ..., "paymentId": 123, "amount": 12.99} + } +} +``` + +!!! info "Make sure to learn more about the advanced structured argument serialization features in the [Logging v2 documentation](./core/logging.md/#custom-keys)." + +## Updated Metrics utility interface + + + + +The Metrics utility was redesigned to be more modular and allow for the addition of new metrics providers in the future. The same EMF-based metrics logging still applies but will be called via an updated public interface. Consider the following list to understand some of changes: + +- `#!java @Metrics` was renamed to `#!java @FlushMetrics` +- `#!java MetricsLogger.metricsLogger()` was renamed to `#!java MetricsFactory.getMetricsInstance()` +- `put*` calls such as `#!java putMetric()` where replaced with `add*` nomenclature such as `#!java addMetric()` +- All direct imports from `software.amazon.cloudwatchlogs.emf` need to be replaced with Powertools counterparts from `software.amazon.lambda.powertools.metrics` (see example below) +- The `withSingleMetric` and `withMetricsLogger` methods were removed in favor of `#!java metrics.flushSingleMetric()` +- It is no longer valid to skip declaration of a namespace. If no namespace is provided, an exception will be raised instead of using the default `aws-embedded-metrics` namespace. + +The following example shows a common Lambda handler using the Metrics utility and required refactorings. + +```diff +// Metrics is not a decorator anymore but the replacement for the `MetricsLogger` Singleton +import software.amazon.lambda.powertools.metrics.Metrics; ++ import software.amazon.lambda.powertools.metrics.FlushMetrics; +- import software.amazon.lambda.powertools.metrics.MetricsUtils; ++ import software.amazon.lambda.powertools.metrics.MetricsFactory; +- import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; +- import software.amazon.cloudwatchlogs.emf.model.DimensionSet; +- import software.amazon.cloudwatchlogs.emf.model.Unit; ++ import software.amazon.lambda.powertools.metrics.model.DimensionSet; ++ import software.amazon.lambda.powertools.metrics.model.MetricUnit; + +public class MetricsEnabledHandler implements RequestHandler { + + // This is still a Singleton +- MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); ++ Metrics metrics = MetricsFactory.getMetricsInstance(); + + @Override +- @Metrics(namespace = "ExampleApplication", service = "booking") ++ @FlushMetrics(namespace = "ExampleApplication", service = "booking") + public Object handleRequest(Object input, Context context) { +- metricsLogger.putDimensions(DimensionSet.of("environment", "prod")); ++ metrics.addDimension(DimensionSet.of("environment", "prod")); + // New method overload for adding 2D dimensions more conveniently ++ metrics.addDimension("environment", "prod"); +- metricsLogger.putMetric("SuccessfulBooking", 1, Unit.COUNT); ++ metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); + ... + } +} +``` + +Learn more about the redesigned Metrics utility in the [Metrics documentation](./core/metrics.md). + +## Deprecated capture mode related `@Tracing` annotation parameters + + + +The deprecated `captureError` and `captureResponse` arguments to the `@Tracing` annotation were removed in v2 and replaced by a new `captureMode` parameter. The parameter can be passed an Enum value of `CaptureMode`. + +You should update your code using the new `captureMode` argument: + +```diff +- @Tracing(captureError = false, captureResponse = false) ++ @Tracing(captureMode = CaptureMode.DISABLED) +public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + // ... +} +``` + +Learn more about valid `CaptureMode` values in the [Tracing documentation](./core/tracing.md). + +## Idempotency utility split into sub-modules by provider + +The Idempotency utility was split from the common `powertools-idempotency` package into individual packages for different persistence store providers. The main business logic is now in the `powertools-idempotency-core` package. + +You should now include the `powertools-idempotency-core` package as an AspectJ library and the provider package like `powertools-idempotency-dynamodb` as a regular dependency. + +```diff + +- +- software.amazon.lambda +- powertools-idempotency +- 1.x.x +- + + ++ ++ software.amazon.lambda ++ powertools-idempotency-dynamodb ++ 2.x.x ++ + ++ ++ software.amazon.lambda ++ powertools-idempotency-core ++ +``` + +## Parameters utility split into sub-modules by provider + +Parameters utilities were split from the common `powertools-parameters` package into individual packages for different parameter providers. You should now include the specific parameters dependency for your provider. If you use multiple providers, you can include multiple packages. Each parameter provider needs to be included as a dependency and an AspectJ library to use annotations. + +This new structure reduces the bundle size of your deployment package. + +```diff + + +- +- software.amazon.lambda +- powertools-parameters +- 1.x.x +- + +- +- software.amazon.lambda +- powertools-parameters +- + + ++ ++ software.amazon.lambda ++ powertools-parameters-secrets ++ 2.x.x ++ + + ++ ++ software.amazon.lambda ++ powertools-parameters-secrets ++ + +``` + +!!! info "Find the full list of supported providers in the [Parameters utility documentation](./utilities/parameters.md)." + +## Custom Resources updates the `Response` class + + + +The `Response` class supporting CloudFormation Custom Resource implementations was updated to remove deprecated methods. + +The `#!java Response.failed()` and `#!java Response.success()` methods without parameters were removed and require the physical resource ID now. You should update your code to use: + +- `#!java Response.failed(String physicalResourceId)` +- `#!java Response.success(String physicalResourceId)` + +```diff +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler; +import software.amazon.lambda.powertools.cloudformation.Response; + +public class MyCustomResourceHandler extends AbstractCustomResourceHandler { + + // ... + + @Override + protected Response update(CloudFormationCustomResourceEvent updateEvent, Context context) { ++ String physicalResourceId = updateEvent.getPhysicalResourceId(); + UpdateResult updateResult = doUpdates(physicalResourceId); + if (updateResult.isSuccessful()) { +- return Response.success(); ++ return Response.success(physicalResourceId); + } else { +- return Response.failed(); ++ return Response.failed(physicalResourceId); + } + } + + // ... +} +``` + +## Improved integration of Validation utility with other utilities + + + +The Validation utility includes two updates that change the behavior of integration with other utilities and AWS services. + +**1. Updated HTTP status code when using `@Validation` with API Gateway** + +This does not require a code change in the Lambda function using the Validation utility but might impact how your calling application treats exceptions. Prior to `v2`, a 500 HTTP status code was returned when the validation did not pass. Consistent with the [HTTP specification](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status){target="\_blank"}, a 400 status code is returned now indicating a user error instead of a server error. + +Consider the following example: + +```java +import software.amazon.lambda.powertools.validation.Validation; + +public class MyFunctionHandler implements RequestHandler { + + @Override + @Validation(inboundSchema = "classpath:/schema_in.json", outboundSchema = "classpath:/schema_out.json") + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + // ... + return something; + } +} +``` + +If the request validation fails, you can expect the following change in the HTTP response status code on the client-side: + +```sh +# BEFORE v2: 500 Internal Server Error +❯ curl -s -o /dev/null -w "%{http_code}" https://{API_ID}.execute-api.{REGION}.amazonaws.com/{STAGE}/{PATH} +500 +# AFTER v2: 400 Bad Request +❯ curl -s -o /dev/null -w "%{http_code}" https://{API_ID}.execute-api.{REGION}.amazonaws.com/{STAGE}/{PATH} +400 +``` + +**2. Integration with partial batch failures when using Batch utility** + +This does not require a code change but might affect the batch processing flow when using the Validation utility in combination with the Batch processing utility. + +Consider the following example: + +```java +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import software.amazon.lambda.powertools.batch.BatchMessageHandlerBuilder; +import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; + +public class SqsBatchHandler implements RequestHandler { + + private final BatchMessageHandler handler; + + public SqsBatchHandler() { + handler = new BatchMessageHandlerBuilder() + .withSqsBatchHandler() + .buildWithMessageHandler(this::processMessage, Product.class); + } + + @Override + @Validation(inboundSchema = "classpath:/schema_in.json", outboundSchema = "classpath:/schema_out.json") + public SQSBatchResponse handleRequest(SQSEvent sqsEvent, Context context) { + return handler.processBatch(sqsEvent, context); + } + + private void processMessage(Product p, Context c) { + // Process the product + } +} +``` + +- **Prior to `v2`** this caused the whole batch to fail. +- **After `v2`** this will add only the failed events to the batch item failure list in the response and process the remaining messages. + +!!! info "Check if your workload can tolerate this behavior and make sure it is designed for idempotency when using partial batch item failures. We offer the [Idempotency](./utilities/idempotency.md) utility to simplify integration of idempotent behavior in your workloads." + +## AspectJ runtime not included by default anymore + +The AspectJ runtime is no longer included as a transitive dependency of Powertools. For all utilities offering annotations using AspectJ compile-time weaving, you need to include the AspectJ runtime yourself now. This is also documented, with a complete example, in our [installation guide](./index.md). For Maven projects, make sure to add the following dependency in your dependencies section: + +```diff ++ ++ org.aspectj ++ aspectjrt ++ 1.9.22 ++ +``` + +## Removed `powertools-sqs` module in favor of `powertools-batch` + +The archived documentation contains a migration guide for both large message handling using `powertools-sqs` and batch processing using `powertools-sqs`. The sections below explain the high-level steps for your convenience. + +### Migrating SQS Batch processing (`@SqsBatch`) + +The [batch processing library](./utilities/batch.md) provides a way to process messages and gracefully handle partial failures for SQS, Kinesis Streams, and DynamoDB Streams batch sources. In comparison to the legacy SQS Batch library, it relies on [Lambda partial batch responses](https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-errorhandling.html#services-sqs-batchfailurereporting){target="\_blank"}, which allows the library to provide a simpler, more reliable interface for processing batches. + +In order to get started, check out the new [processing messages from SQS](./utilities/batch.md/#processing-messages-from-sqs) documentation. In most cases, you will simply be able to retain your existing batch message handler function, and wrap it with the new batch processing interface. Unlike the `powertools-sqs` module, the new `powertools-batch` module uses _partial batch responses_ to communicate to Lambda which messages have been processed and must be removed from the queue. The return value of the handler's process function must be returned to Lambda. + +The new library also no longer requires the `SQS:DeleteMessage` action on the Lambda function's role policy, as Lambda +itself now manages removal of messages from the queue. + + +!!! info "Some tuneables from `powertools-sqs` are no longer provided." + - **Non-retryable Exceptions** - there is no mechanism to indicate in a partial batch response that a particular message + should not be retried and instead moved to DLQ - a message either succeeds, or fails and is retried. A message + will be moved to the DLQ once the normal retry process has expired. + - **Suppress Exception** - The new batch processor does not throw an exception on failure of a handler. Instead, + its result must be returned by your code from your message handler to Lambda, so that Lambda can manage + the completed messages and retry behaviour. + + +### Migrating SQS Large message handling (`@SqsLargeMessage`) + +- Replace the dependency in Maven / Gradle: `powertools-sqs` ==> `powertools-large-messages` +- Replace the annotation: `@SqsLargeMessage` ==> `@LargeMessage` (the new module handles both SQS and SNS) +- Move the annotation away from the Lambda `handleRequest` method and put it on a method with `SQSEvent.SQSMessage` or `SNSEvent.SNSRecord` as first parameter. +- The annotation now handles a single message, contrary to the previous version that was handling the complete batch. This gives more control, especially when dealing with partial failures with SQS (see the batch module). +- The new module only provides an annotation: an equivalent to the `SqsUtils` class is not available anymore in this new version. diff --git a/docs/utilities/batch.md b/docs/utilities/batch.md index 6b38b438c..7693ac98f 100644 --- a/docs/utilities/batch.md +++ b/docs/utilities/batch.md @@ -30,6 +30,7 @@ stateDiagram-v2 * Reports batch item failures to reduce number of retries for a record upon errors * Simple interface to process each batch record +* Parallel processing of batches * Integrates with Java Events library and the deserialization module * Build your own batch processor by extending primitives @@ -110,16 +111,9 @@ You can use your preferred deployment framework to set the correct configuration while the `powertools-batch` module handles generating the response, which simply needs to be returned as the result of your Lambda handler. -A complete [Serverless Application Model](https://aws.amazon.com/serverless/sam/) example can be found -[here](https://github.com/aws-powertools/powertools-lambda-java/tree/main/examples/powertools-examples-batch) covering -all of the batch sources. - -For more information on configuring `ReportBatchItemFailures`, -see the details for [SQS](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting), -[Kinesis](https://docs.aws.amazon.com/lambda/latest/dg/with-kinesis.html#services-kinesis-batchfailurereporting),and -[DynamoDB Streams](https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html#services-ddb-batchfailurereporting). - +A complete [Serverless Application Model](https://aws.amazon.com/serverless/sam/) example can be found [here](https://github.com/aws-powertools/powertools-lambda-java/tree/main/examples/powertools-examples-batch) covering all the batch sources. +For more information on configuring `ReportBatchItemFailures`, see the details for [SQS](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting), [Kinesis](https://docs.aws.amazon.com/lambda/latest/dg/with-kinesis.html#services-kinesis-batchfailurereporting), and [DynamoDB Streams](https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html#services-ddb-batchfailurereporting). !!! note "You do not need any additional IAM permissions to use this utility, except for what each event source requires." @@ -150,12 +144,10 @@ see the details for [SQS](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs. public SQSBatchResponse handleRequest(SQSEvent sqsEvent, Context context) { return handler.processBatch(sqsEvent, context); } - - + private void processMessage(Product p, Context c) { // Process the product } - } ``` @@ -276,7 +268,6 @@ see the details for [SQS](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs. private void processMessage(Product p, Context c) { // process the product } - } ``` @@ -475,6 +466,51 @@ see the details for [SQS](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs. } ``` +## Parallel processing +You can choose to process batch items in parallel using the `BatchMessageHandler#processBatchInParallel()` +instead of `BatchMessageHandler#processBatch()`. Partial batch failure works the same way but items are processed +in parallel rather than sequentially. + +This feature is available for SQS, Kinesis and DynamoDB Streams but cannot be +used with SQS FIFO. In that case, an `UnsupportedOperationException` is thrown. + +!!! warning + Note that parallel processing is not always better than sequential processing, + and you should benchmark your code to determine the best approach for your use case. + +!!! info + To get more threads available (more vCPUs), you need to increase the amount of memory allocated to your Lambda function. + While it is possible to increase the number of threads using Java options or custom thread pools, + in most cases the defaults work well, and changing them is more likely to decrease performance + (see [here](https://www.baeldung.com/java-when-to-use-parallel-stream#fork-join-framework) + and [here](https://dzone.com/articles/be-aware-of-forkjoinpoolcommonpool)). + In situations where this may be useful - such as performing IO-bound work in parallel - make sure to measure before and after! + + +=== "Example with SQS" + + ```java hl_lines="13" + public class SqsBatchHandler implements RequestHandler { + + private final BatchMessageHandler handler; + + public SqsBatchHandler() { + handler = new BatchMessageHandlerBuilder() + .withSqsBatchHandler() + .buildWithMessageHandler(this::processMessage, Product.class); + } + + @Override + public SQSBatchResponse handleRequest(SQSEvent sqsEvent, Context context) { + return handler.processBatchInParallel(sqsEvent, context); + } + + private void processMessage(Product p, Context c) { + // Process the product + } + } + ``` + ## Handling Messages @@ -490,7 +526,7 @@ In general, the deserialized message handler should be used unless you need acce === "Raw Message Handler" - ```java + ```java hl_lines="4 7" public void setup() { BatchMessageHandler handler = new BatchMessageHandlerBuilder() .withSqsBatchHandler() @@ -505,7 +541,7 @@ In general, the deserialized message handler should be used unless you need acce === "Deserialized Message Handler" - ```java + ```java hl_lines="4 7" public void setup() { BatchMessageHandler handler = new BatchMessageHandlerBuilder() .withSqsBatchHandler() @@ -529,20 +565,20 @@ provide a custom failure handler. Handlers can be provided when building the batch processor and are available for all event sources. For instance for DynamoDB: -```java +```java hl_lines="3 8" BatchMessageHandler handler = new BatchMessageHandlerBuilder() - .withDynamoDbBatchHandler() - .withSuccessHandler((m) -> { - // Success handler receives the raw message - LOGGER.info("Message with sequenceNumber {} was successfully processed", - m.getDynamodb().getSequenceNumber()); - }) - .withFailureHandler((m, e) -> { - // Failure handler receives the raw message and the exception thrown. - LOGGER.info("Message with sequenceNumber {} failed to be processed: {}" - , e.getDynamodb().getSequenceNumber(), e); - }) - .buildWithMessageHander(this::processMessage); + .withDynamoDbBatchHandler() + .withSuccessHandler((m) -> { + // Success handler receives the raw message + LOGGER.info("Message with sequenceNumber {} was successfully processed", + m.getDynamodb().getSequenceNumber()); + }) + .withFailureHandler((m, e) -> { + // Failure handler receives the raw message and the exception thrown. + LOGGER.info("Message with sequenceNumber {} failed to be processed: {}" + , e.getDynamodb().getSequenceNumber(), e); + }) + .buildWithMessageHander(this::processMessage); ``` !!! info diff --git a/docs/utilities/custom_resources.md b/docs/utilities/custom_resources.md index d0c1eacf2..053e8a9d7 100644 --- a/docs/utilities/custom_resources.md +++ b/docs/utilities/custom_resources.md @@ -223,7 +223,7 @@ public class CustomSerializationHandler extends AbstractResourceHandler { While the library provides an easy-to-use interface, we recommend that you understand the lifecycle of CloudFormation custom resources before using them in production. #### Creating a custom resource -When CloudFormation issues a CREATE on a custom resource, there are 2 possible states: `CREATE_COMPLETE` and `CREATE_FAILED` +When CloudFormation issues a `CREATE` on a custom resource, there are 2 possible states: `CREATE_COMPLETE` and `CREATE_FAILED` ```mermaid stateDiagram direction LR @@ -237,7 +237,7 @@ If the resource is created successfully, the `physicalResourceId` is stored by C If the resource failed to create, CloudFormation triggers a rollback operation by default (rollback can be disabled, see [stack failure options](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stack-failure-options.html)) #### Updating a custom resource -CloudFormation issues an UPDATE operation on a custom resource only when one or more custom resource properties change. +CloudFormation issues an `UPDATE` operation on a custom resource only when one or more custom resource properties change. During the update, the custom resource may update successfully, or may fail the update. ```mermaid stateDiagram @@ -278,7 +278,8 @@ stateDiagram #### Deleting a custom resource -CloudFormation issues a DELETE on a custom resource when: +CloudFormation issues a `DELETE` on a custom resource when: + - the CloudFormation stack is being deleted - a new `physicalResourceId` was received during an update, and CloudFormation proceeds to rollback(DELETE) the custom resource with the previous `physicalResourceId`. @@ -289,4 +290,4 @@ stateDiagram [*] --> deleteState deleteState --> DELETE_COMPLETE deleteState --> DELETE_FAILED -``` \ No newline at end of file +``` diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index 5392b8d4c..3953949d1 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -26,16 +26,15 @@ times with the same parameters**. This makes idempotent operations safe to retry ## Getting started ### Installation -Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes. -=== "Maven Java 11+" +=== "Maven" ```xml hl_lines="3-7 16 18 24-27" ... software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb {{ powertools.version }} ... @@ -48,7 +47,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 11 11 @@ -56,10 +55,18 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl software.amazon.lambda - powertools-idempotency + powertools-idempotency-core + + + org.aspectj + aspectjtools + + 1.9.22 + + @@ -73,52 +80,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ``` -=== "Maven Java 1.8" - - ```xml hl_lines="3-7 16 18 24-27" - - ... - - software.amazon.lambda - powertools-idempotency - {{ powertools.version }} - - ... - - ... - - - - ... - - org.codehaus.mojo - aspectj-maven-plugin - 1.14.0 - - 1.8 - 1.8 - 1.8 - - - software.amazon.lambda - powertools-idempotency - - - - - - - compile - - - - - ... - - - ``` - -=== "Gradle Java 11+" +=== "Gradle" ```groovy hl_lines="3 11" plugins { @@ -131,33 +93,13 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl } dependencies { - aspect 'software.amazon.lambda:powertools-idempotency:{{ powertools.version }}' + aspect 'software.amazon.lambda:powertools-idempotency-dynamodb:{{ powertools.version }}' } sourceCompatibility = 11 // or higher targetCompatibility = 11 // or higher ``` -=== "Gradle Java 1.8" - - ```groovy hl_lines="3 11" - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' - } - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-idempotency:{{ powertools.version }}' - } - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - ``` - ### Required resources Before getting started, you need to create a persistent storage layer where the idempotency utility can store its state - your Lambda functions will need read and write access to it. @@ -202,7 +144,7 @@ Resources: TableName: !Ref IdempotencyTable Environment: Variables: - IDEMPOTENCY_TABLE: !Ref IdempotencyTable + TABLE_NAME: !Ref IdempotencyTable ``` !!! warning "Warning: Large responses with DynamoDB persistence layer" @@ -495,7 +437,7 @@ To prevent against extended failed retries when a [Lambda function times out](ht Idempotency.config() .withPersistenceStore( DynamoDBPersistenceStore.builder() - .withTableName(System.getenv("IDEMPOTENCY_TABLE")) + .withTableName(System.getenv("TABLE_NAME")) .build()) .configure(); } @@ -650,6 +592,7 @@ IdempotencyConfig.builder() .withUseLocalCache(true) .withLocalCacheMaxItems(432) .withHashFunction("SHA-256") + .withResponseHook((responseData, dataRecord) -> responseData) .build() ``` @@ -657,13 +600,14 @@ These are the available options for further configuration: | Parameter | Default | Description | |---------------------------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------| -| **EventKeyJMESPath** | `""` | JMESPath expression to extract the idempotency key from the event record. See available [built-in functions](serialization) | +| **EventKeyJMESPath** | `""` | JMESPath expression to extract the idempotency key from the event record. See available [built-in functions](serialization) | | **PayloadValidationJMESPath** | `""` | JMESPath expression to validate whether certain parameters have changed in the event | -| **ThrowOnNoIdempotencyKey** | `False` | Throw exception if no idempotency key was found in the request | +| **ThrowOnNoIdempotencyKey** | `false` | Throw exception if no idempotency key was found in the request | | **ExpirationInSeconds** | 3600 | The number of seconds to wait before a record is expired | | **UseLocalCache** | `false` | Whether to locally cache idempotency results (LRU cache) | | **LocalCacheMaxItems** | 256 | Max number of items to store in local cache | | **HashFunction** | `MD5` | Algorithm to use for calculating hashes, as supported by `java.security.MessageDigest` (eg. SHA-1, SHA-256, ...) | +| **ResponseHook** | `null` | Response hook to apply modifications to idempotent responses | These features are detailed below. @@ -921,6 +865,58 @@ You can extend the `BasePersistenceStore` class and implement the abstract metho For example, the `putRecord` method needs to throw an exception if a non-expired record already exists in the data store with a matching key. +### Manipulating the Idempotent Response + +You can set up a response hook in the Idempotency configuration to manipulate the returned data when an operation is idempotent. The hook function will be called with the current de-serialized response `Object` and the Idempotency `DataRecord`. + +The example below shows how to append an HTTP header to an `APIGatewayProxyResponseEvent`. + +=== "Using an Idempotent Response Hook" + + ```java hl_lines="3-20" + Idempotency.config().withConfig( + IdempotencyConfig.builder() + .withResponseHook((responseData, dataRecord) -> { + if (responseData instanceof APIGatewayProxyResponseEvent) { + APIGatewayProxyResponseEvent proxyResponse = + (APIGatewayProxyResponseEvent) responseData; + final Map headers = new HashMap<>(); + headers.putAll(proxyResponse.getHeaders()); + // Append idempotency headers + headers.put("x-idempotency-response", "true"); + headers.put("x-idempotency-expiration", + String.valueOf(dataRecord.getExpiryTimestamp())); + + proxyResponse.setHeaders(headers); + + return proxyResponse; + } + + return responseData; + }) + .build()) + .withPersistenceStore( + DynamoDBPersistenceStore.builder() + .withTableName(System.getenv("TABLE_NAME")) + .build()) + .configure(); + ``` + +???+ info "Info: Using custom de-serialization?" + + The response hook is called after de-serialization so the payload you process will be the de-serialized Java object. + +#### Being a good citizen + +When using response hooks to manipulate returned data from idempotent operations, it's important to follow best practices to avoid introducing complexity or issues. Keep these guidelines in mind: + +1. **Response hook works exclusively when operations are idempotent.** The hook will not be called when an operation is not idempotent, or when the idempotent logic fails. + +2. **Catch and Handle Exceptions.** Your response hook code should catch and handle any exceptions that may arise from your logic. Unhandled exceptions will cause the Lambda function to fail unexpectedly. + +3. **Keep Hook Logic Simple** Response hooks should consist of minimal and straightforward logic for manipulating response data. Avoid complex conditional branching and aim for hooks that are easy to reason about. + + ## Compatibility with other utilities ### Validation utility @@ -1020,7 +1016,7 @@ To unit test your function with DynamoDB Local, you can refer to this guide to [ - idempotency + idempotency eu-central-1 @@ -1124,7 +1120,7 @@ To unit test your function with DynamoDB Local, you can refer to this guide to [ public App(DynamoDbClient ddbClient) { Idempotency.config().withPersistenceStore( DynamoDBPersistenceStore.builder() - .withTableName(System.getenv("IDEMPOTENCY_TABLE_NAME")) + .withTableName(System.getenv("TABLE_NAME")) .withDynamoDbClient(ddbClient) .build() ).configure(); @@ -1161,7 +1157,7 @@ To unit test your function with DynamoDB Local, you can refer to this guide to [ Idempotency.config().withPersistenceStore( DynamoDBPersistenceStore.builder() - .withTableName(System.getenv("IDEMPOTENCY_TABLE_NAME")) + .withTableName(System.getenv("TABLE_NAME")) .withDynamoDbClient(ddbBuilder.build()) .build() ).configure(); @@ -1206,7 +1202,7 @@ To unit test your function with DynamoDB Local, you can refer to this guide to [ ```json hl_lines="3" { "IdempotentFunction": { - "IDEMPOTENCY_TABLE_NAME": "idempotency" + "TABLE_NAME": "idempotency" } } ``` diff --git a/docs/utilities/large_messages.md b/docs/utilities/large_messages.md index c4947a6e8..38228afe9 100644 --- a/docs/utilities/large_messages.md +++ b/docs/utilities/large_messages.md @@ -6,12 +6,6 @@ description: Utility The large message utility handles SQS and SNS messages which have had their payloads offloaded to S3 if they are larger than the maximum allowed size (256 KB). -!!! Notice - The large message utility (available in the `powertools-sqs` module for versions v1.16.1 and earlier) is now deprecated - and replaced by the `powertools-large-messages` described in this page. - You can still get the documentation [here](sqs_large_message_handling.md) - and the migration guide [here](#migration-from-the-sqs-large-message-utility). - ## Features - Automatically retrieve the content of S3 objects when SQS or SNS messages have been offloaded to S3. @@ -100,53 +94,7 @@ of amazon-sns-java-extended-client-lib. ## Install -Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes. - -=== "Maven Java 11+" -```xml hl_lines="3-7 16 18 24-27" - -... - -software.amazon.lambda -powertools-large-messages -{{ powertools.version }} - -... - -... - - - -... - -dev.aspectj -aspectj-maven-plugin -1.13.1 - -11 -11 -11 - - -software.amazon.lambda -powertools-large-messages - - - - - - -compile - - - - -... - - -``` - -=== "Maven Java 1.8" +=== "Maven" ```xml hl_lines="3-7 16 18 24-27" @@ -164,34 +112,42 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ... - org.codehaus.mojo - aspectj-maven-plugin - 1.14.0 - - 1.8 - 1.8 - 1.8 - - - software.amazon.lambda - powertools-large-messages - - - - - - - compile - - - + dev.aspectj + aspectj-maven-plugin + 1.14 + + 11 + 11 + 11 + + + software.amazon.lambda + powertools-large-messages + + + + + + org.aspectj + aspectjtools + + 1.9.22 + + + + + + compile + + + ... ``` -=== "Gradle Java 11+" +=== "Gradle" ```groovy hl_lines="3 11" plugins { @@ -211,26 +167,6 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl targetCompatibility = 11 // or higher ``` -=== "Gradle Java 1.8" - - ```groovy hl_lines="3 11" - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' - } - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-large-messages:{{ powertools.version }}' - } - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - ``` - ## Permissions As the utility interacts with Amazon S3, the lambda function must have the following permissions @@ -416,4 +352,4 @@ If you need to customize this `S3Client`, you can leverage the `LargeMessageConf It gives more control, especially when dealing with partial failures with SQS (see the batch module). - The new module only provides an annotation, an equivalent to the `SqsUtils` class is not available anymore in this new version. -Finally, if you are still using the `powertools-sqs` library for batch processing, consider moving to `powertools-batch` at the same time to remove the dependency on this library completely; it has been deprecated and will be removed in v2. \ No newline at end of file +Finally, if you are still using the `powertools-sqs` library for batch processing, consider moving to `powertools-batch` at the same time to remove the dependency on this library completely; it has been deprecated and will be removed in v2. diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index 85d30d77e..beb460aa6 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -4,29 +4,46 @@ description: Utility --- -The parameters utility provides a way to retrieve parameter values from +The parameters utilities provide a way to retrieve parameter values from [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html), -[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/), or [Amazon DynamoDB](https://aws.amazon.com/dynamodb/). -It also provides a base class to create your parameter provider implementation. +[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/), [Amazon DynamoDB](https://aws.amazon.com/dynamodb/), +or [AWS AppConfig](https://aws.amazon.com/systems-manager/features/appconfig/). + +## Key features -**Key features** - -* Retrieve one or multiple parameters from the underlying provider +* Retrieve one or multiple parameters from an underlying provider in a standard way * Cache parameter values for a given amount of time (defaults to 5 seconds) * Transform parameter values from JSON or base 64 encoded strings +* GraalVM support ## Install -Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes. +In order to provide lightweight dependencies, each parameters module is available as its own +package: + +* **Secrets Manager** - `powertools-parameters-secrets` +* **SSM Parameter Store** - `powertools-parameters-ssm` +* **Amazon DynamoDB** -`powertools-parameters-dynamodb` +* **AWS AppConfig** - `powertools-parameters-appconfig` -=== "Maven Java 11+" +You can easily mix and match parameter providers within the same project for different needs. - ```xml hl_lines="3-7 16 18 24-27" +Note that you must provide the concrete parameters module you want to use below - see the TODOs! + +=== "Maven" + + ```xml hl_lines="4-12 17 24 30-34" ... software.amazon.lambda - powertools-parameters - {{ powertools.version }} + + + powertools-parameters-secrets + powertools-parameters-ssm + powertools-parameters-dynamodb + powertools-parameters-appconfig + + {{ powertools.version }} ... @@ -38,63 +55,27 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 11 11 11 + software.amazon.lambda - powertools-parameters - - - - - - - compile - - - - - ... - - - ``` - -=== "Maven Java 1.8" - - ```xml hl_lines="3-7 16 18 24-27" - - ... - - software.amazon.lambda - powertools-parameters - {{ powertools.version }} - - ... - - ... - - - - ... - - org.codehaus.mojo - aspectj-maven-plugin - 1.14.0 - - 1.8 - 1.8 - 1.8 - - - software.amazon.lambda - powertools-parameters + powertools-parameters-secrets + + + org.aspectj + aspectjtools + + 1.9.22 + + @@ -108,9 +89,9 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ``` -=== "Gradle Java 11+" +=== "Gradle" - ```groovy hl_lines="3 11" + ```groovy hl_lines="3 11 12" plugins { id 'java' id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0' @@ -121,94 +102,90 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl } dependencies { - aspect 'software.amazon.lambda:powertools-parameters:{{ powertools.version }}' + // TODO! Provide the parameters module you want to use here + aspect 'software.amazon.lambda:powertools-parameters-secrets:{{ powertools.version }}' } sourceCompatibility = 11 // or higher targetCompatibility = 11 // or higher ``` -=== "Gradle Java 1.8" - - ```groovy hl_lines="3 11" - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' - } - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-parameters:{{ powertools.version }}' - } - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - ``` - **IAM Permissions** This utility requires additional permissions to work as expected. See the table below: -Provider | Function/Method | IAM Permission -------------------------------------------------- |----------------------------------------------------------------------| --------------------------------------------------------------------------------- -SSM Parameter Store | `SSMProvider.get(String)` `SSMProvider.get(String, Class)` | `ssm:GetParameter` -SSM Parameter Store | `SSMProvider.getMultiple(String)` | `ssm:GetParametersByPath` -Secrets Manager | `SecretsProvider.get(String)` `SecretsProvider.get(String, Class)` | `secretsmanager:GetSecretValue` -DynamoDB | `DynamoDBProvider.get(String)` `DynamoDBProvider.getMultiple(string)` | `dynamodb:GetItem` `dynamoDB:Query` +| Provider | Function/Method | IAM Permission | +|-----------|-------------------------------------------------------------------------|---------------------------------------------------------------------------| +| SSM | `SSMProvider.get(String)` `SSMProvider.get(String, Class)` | `ssm:GetParameter` | +| SSM | `SSMProvider.getMultiple(String)` | `ssm:GetParametersByPath` | +| SSM | If using `withDecryption(true)` | You must add an additional permission `kms:Decrypt` | +| Secrets | `SecretsProvider.get(String)` `SecretsProvider.get(String, Class)` | `secretsmanager:GetSecretValue` | +| DynamoDB | `DynamoDBProvider.get(String)` `DynamoDBProvider.getMultiple(string)` | `dynamodb:GetItem` `dynamoDB:Query` | +| AppConfig | `AppConfigProvider.get(String)` `AppConfigProvider.getMultiple(string)` | `appconfig:StartConfigurationSession`, `appConfig:GetLatestConfiguration` | -## SSM Parameter Store +## Retrieving Parameters +You can retrieve parameters either using annotations or by using the `xParamProvider` class for each parameter +provider directly. The latter is useful if you need to configure the underlying SDK client, for example to use +a different region or credentials, the former is simpler to use. -You can retrieve a single parameter using SSMProvider.get() and pass the key of the parameter. -For multiple parameters, you can use SSMProvider.getMultiple() and pass the path to retrieve them all. +## Built-in provider classes -Alternatively, you can retrieve an instance of a provider and configure its underlying SDK client, -in order to get data from other regions or use specific credentials. +This section describes the built-in provider classes for each parameter store, providing +examples showing how to inject parameters using annotations, and how to use the provider +interface. In cases where a provider supports extra features, these will also be described. -=== "SSMProvider" +### Secrets Manager - ```java hl_lines="6" - import software.amazon.lambda.powertools.parameters.SSMProvider; - import software.amazon.lambda.powertools.parameters.ParamManager; +=== "Secrets Manager: @SecretsParam" - public class AppWithSSM implements RequestHandler { - // Get an instance of the SSM Provider - SSMProvider ssmProvider = ParamManager.getSsmProvider(); - - // Retrieve a single parameter - String value = ssmProvider.get("/my/parameter"); - - // Retrieve multiple parameters from a path prefix - // This returns a Map with the parameter name as key - Map values = ssmProvider.getMultiple("/my/path/prefix"); + ```java hl_lines="8 9" + import com.amazonaws.services.lambda.runtime.Context; + import com.amazonaws.services.lambda.runtime.RequestHandler; + import software.amazon.lambda.powertools.parameters.secrets.SecretsParam; + public class ParametersFunction implements RequestHandler { + + // Annotation-style injection from secrets manager + @SecretsParam(key = "/powertools-java/userpwd") + String secretParam; + + public string handleRequest(String request, Context context) { + // ... do something with the secretParam here + return "something"; + } } ``` -=== "SSMProvider with a custom client" +=== "Secrets Manager: SecretsProvider" - ```java hl_lines="5 7" - import software.amazon.lambda.powertools.parameters.SSMProvider; - import software.amazon.lambda.powertools.parameters.ParamManager; + ```java hl_lines="12-15 19" + import static software.amazon.lambda.powertools.parameters.transform.Transformer.base64; + + import com.amazonaws.services.lambda.runtime.Context; + import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; + import software.amazon.lambda.powertools.parameters.secrets.SecretsProvider; + import com.amazonaws.services.lambda.runtime.RequestHandler; - public class AppWithSSM implements RequestHandler { - SsmClient client = SsmClient.builder().region(Region.EU_CENTRAL_1).build(); - // Get an instance of the SSM Provider - SSMProvider ssmProvider = ParamManager.getSsmProvider(client); + public class RequestHandlerWithParams implements RequestHandler { - // Retrieve a single parameter - String value = ssmProvider.get("/my/parameter"); + // Get an instance of the SecretsProvider. We can provide a custom client here if we want, + // for instance to use a particular region. + SecretsProvider secretsProvider = SecretsProvider + .builder() + .withClient(SecretsManagerClient.builder().build()) + .build(); - // Retrieve multiple parameters from a path prefix - // This returns a Map with the parameter name as key - Map values = ssmProvider.getMultiple("/my/path/prefix"); + public String handleRequest(String input, Context context) { + // Retrieve a single secret + String value = secretsProvider.get("/my/secret"); + // ... do something with the secretParam here + return "something"; + } } ``` -### Additional arguments +### SSM Parameter Store The AWS Systems Manager Parameter Store provider supports two additional arguments for the `get()` and `getMultiple()` methods: @@ -217,9 +194,59 @@ The AWS Systems Manager Parameter Store provider supports two additional argumen | **withDecryption()** | `False` | Will automatically decrypt the parameter. | | **recursive()** | `False` | For `getMultiple()` only, will fetch all parameter values recursively based on a path prefix. | -**Example:** -=== "AppWithSSM.java" +=== "SSM Parameter Store: @SSMParam" + + ```java hl_lines="8 9" + import com.amazonaws.services.lambda.runtime.Context; + import com.amazonaws.services.lambda.runtime.RequestHandler; + import software.amazon.lambda.powertools.parameters.ssm.SSMParam; + + public class ParametersFunction implements RequestHandler { + + // Annotation-style injection from SSM Parameter Store + @SSMParam(key = "/powertools-java/param") + String ssmParam; + + public string handleRequest(String request, Context context) { + return ssmParam; // Request handler simply returns our configuration value + } + } + ``` + +=== "SSM Parameter Store: SSMProvider" + + ```java hl_lines="12-15 19-20 22" + import static software.amazon.lambda.powertools.parameters.transform.Transformer.base64; + + import com.amazonaws.services.lambda.runtime.Context; + import com.amazonaws.services.lambda.runtime.RequestHandler; + import software.amazon.awssdk.services.ssm.SsmClient; + import software.amazon.lambda.powertools.parameters.ssm.SSMProvider; + + public class RequestHandlerWithParams implements RequestHandler { + + // Get an instance of the SSMProvider. We can provide a custom client here if we want, + // for instance to use a particular region. + SSMProvider ssmProvider = SSMProvider + .builder() + .withClient(SsmClient.builder().build()) + .build(); + + public String handleRequest(String input, Context context) { + // Retrieve a single param + String value = ssmProvider + .get("/my/secret"); + // We might instead want to retrieve multiple parameters at once, returning a Map of key/value pairs + // .getMultiple("/my/secret/path"); + + // Return the result + return value; + } + } + ``` + +=== "SSM Parameter Store: Additional Options" ```java hl_lines="9 12" import software.amazon.lambda.powertools.parameters.SSMProvider; @@ -238,183 +265,159 @@ The AWS Systems Manager Parameter Store provider supports two additional argumen } ``` -## Secrets Manager +### DynamoDB -For secrets stored in Secrets Manager, use `getSecretsProvider`. +=== "DynamoDB: @DyanmoDbParam" -Alternatively, you can retrieve an instance of a provider and configure its underlying SDK client, -in order to get data from other regions or use specific credentials. + ```java hl_lines="8 9" + import com.amazonaws.services.lambda.runtime.Context; + import com.amazonaws.services.lambda.runtime.RequestHandler; + import software.amazon.lambda.powertools.parameters.dynamodb.DynamoDBParam; + public class ParametersFunction implements RequestHandler { -=== "SecretsProvider" - - ```java hl_lines="9" - import software.amazon.lambda.powertools.parameters.SecretsProvider; - import software.amazon.lambda.powertools.parameters.ParamManager; + // Annotation-style injection from DynamoDB + @DynamoDbParam(table = "my-test-tablename", key = "myKey") + String ddbParam; - public class AppWithSecrets implements RequestHandler { - // Get an instance of the Secrets Provider - SecretsProvider secretsProvider = ParamManager.getSecretsProvider(); - - // Retrieve a single secret - String value = secretsProvider.get("/my/secret"); - + public string handleRequest(String request, Context context) { + return ddbParam; // Request handler simply returns our configuration value + } } ``` -=== "SecretsProvider with a custom client" +=== "DynamoDB: DynamoDbProvider" - ```java hl_lines="5 7" - import software.amazon.lambda.powertools.parameters.SecretsProvider; - import software.amazon.lambda.powertools.parameters.ParamManager; + ```java hl_lines="12-15 19-20 22" + import static software.amazon.lambda.powertools.parameters.transform.Transformer.base64; + + import com.amazonaws.services.lambda.runtime.Context; + import com.amazonaws.services.lambda.runtime.RequestHandler; + import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + import software.amazon.lambda.powertools.parameters.dynamodb.DynamoDbProvider; - public class AppWithSecrets implements RequestHandler { - SecretsManagerClient client = SecretsManagerClient.builder().region(Region.EU_CENTRAL_1).build(); - // Get an instance of the Secrets Provider - SecretsProvider secretsProvider = ParamManager.getSecretsProvider(client); + public class RequestHandlerWithParams implements RequestHandler { - // Retrieve a single secret - String value = secretsProvider.get("/my/secret"); + // Get an instance of the DynamoDbProvider. We can provide a custom client here if we want, + // for instance to use a particular region. + DynamoDbProvider ddbProvider = DynamoDbProvider + .builder() + .withClient(DynamoDbClient.builder().build()) + .build(); + public String handleRequest(String input, Context context) { + // Retrieve a single param + String value = ddbProvider + .get("/my/secret"); + // We might instead want to retrieve multiple values at once, returning a Map of key/value pairs + // .getMultiple("my-partition-key-value"); + + // Return the result + return value; + } } ``` -## DynamoDB -To get secrets stored in DynamoDB, use `getDynamoDbProvider`, providing the name of the table that -contains the secrets. As with the other providers, an overloaded methods allows you to retrieve -a `DynamoDbProvider` providing a client if you need to configure it yourself. +### AppConfig -=== "DynamoDbProvider" +=== "AppConfig: @AppConfigParam" - ```java hl_lines="6 9" - import software.amazon.lambda.powertools.parameters.DynamoDbProvider; - import software.amazon.lambda.powertools.parameters.ParamManager; + ```java hl_lines="8 9" + import com.amazonaws.services.lambda.runtime.Context; + import com.amazonaws.services.lambda.runtime.RequestHandler; + import software.amazon.lambda.powertools.parameters.appconfig.AppConfigParam; - public class AppWithDynamoDbParameters implements RequestHandler { - // Get an instance of the DynamoDbProvider - DynamoDbProvider ddbProvider = ParamManager.getDynamoDbProvider("my-parameters-table"); + public class ParametersFunction implements RequestHandler { - // Retrieve a single parameter - String value = ddbProvider.get("my-key"); - } - ``` - -=== "DynamoDbProvider with a custom client" + // Annotation-style injection from AppConfig + @AppConfigParam(application = "my-app", environment = "my-env", key = "myKey") + String appConfigParam; - ```java hl_lines="9 10 11 12 15 18" - import software.amazon.lambda.powertools.parameters.DynamoDbProvider; - import software.amazon.lambda.powertools.parameters.ParamManager; - import software.amazon.awssdk.services.dynamodb.DynamoDbClient; - import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; - import software.amazon.awssdk.regions.Region; - - public class AppWithDynamoDbParameters implements RequestHandler { - // Get a DynamoDB Client with an explicit region - DynamoDbClient ddbClient = DynamoDbClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .region(Region.EU_CENTRAL_2) - .build(); - - // Get an instance of the DynamoDbProvider - DynamoDbProvider provider = ParamManager.getDynamoDbProvider(ddbClient, "test-table"); - - // Retrieve a single parameter - String value = ddbProvider.get("my-key"); - } + public string handleRequest(String request, Context context) { + return appConfigParam; // Request handler simply returns our configuration value + } + } ``` -## AppConfig -To get parameters stored in AppConfig, use `getAppConfigProvider`, providing the application and environment -name to retrieve configuration from. As with the other providers, an overloaded method allows you to retrieve -an `AppConfigProvider` providing a client if you need to configure it yourself. +=== "AppConfig: AppConfigProvider" -=== "AppConfigProvider" - - ```java hl_lines="6 9" - import software.amazon.lambda.powertools.parameters.AppConfigProvider; - import software.amazon.lambda.powertools.parameters.ParamManager; - - public class AppWitAppConfigParameters implements RequestHandler { - // Get an instance of the AppConfigProvider - AppConfigProvider appConfigProvider = ParamManager.getAppConfigProvider("my-environment", "my-app"); + ```java hl_lines="12-15 19-20" + import static software.amazon.lambda.powertools.parameters.transform.Transformer.base64; - // Retrieve a single parameter - String value = appConfigProvider.get("my-key"); - } - ``` - -=== "AppConfigProvider with a custom client" - - ```java hl_lines="9 10 11 12 15 18" - import software.amazon.lambda.powertools.parameters.AppConfigProvider; - import software.amazon.lambda.powertools.parameters.ParamManager; + import com.amazonaws.services.lambda.runtime.Context; + import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; - import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; - import software.amazon.awssdk.regions.Region; - - public class AppWithDynamoDbParameters implements RequestHandler { - // Get an AppConfig Client with an explicit region - AppConfigDataClient appConfigDataClient = AppConfigDataClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .region(Region.EU_CENTRAL_2) - .build(); + import software.amazon.lambda.powertools.parameters.appconfig.AppConfigProvider; - // Get an instance of the DynamoDbProvider - AppConfigProvider appConfigProvider = ParamManager.getAppConfigProvider(appConfigDataClient, "my-environment", "my-app"); + public class RequestHandlerWithParams implements RequestHandler { - // Retrieve a single parameter - String value = appConfigProvider.get("my-key"); - } + // Get an instance of the AppConfigProvider. We can provide a custom client here if we want, + // for instance to use a particular region. + AppConfigProvider appConfigProvider = AppConfigProvider + .builder() + .withClient(AppConfigDataClient.builder().build()) + .build(); + + public String handleRequest(String input, Context context) { + // Retrieve a single param + String value = appConfigProvider + .get("/my/secret"); + + // Return the result + return value; + } + } ``` - ## Advanced configuration ### Caching +Each provider uses the `CacheManager` to cache parameter values. When a value is retrieved using from the provider, a +custom cache duration can be provided using `withMaxAge(duration, unit)`. -By default, all parameters and their corresponding values are cached for 5 seconds. - -You can customize this default value using `defaultMaxAge`. You can also customize this value for each parameter using -`withMaxAge`. +If this is not specified, the default value set on the `CacheManager` itself will be used. This default can be customized +by calling `setDefaultExpirationTime(duration, unit)` on the `CacheManager`. -=== "Provider with default Max age" - - ```java hl_lines="9" - import software.amazon.lambda.powertools.parameters.SecretsProvider; - import software.amazon.lambda.powertools.parameters.ParamManager; +=== "Customize Cache" - public class AppWithSecrets implements RequestHandler { - // Get an instance of the Secrets Provider - SecretsProvider secretsProvider = ParamManager.getSecretsProvider() - .defaultMaxAge(10, ChronoUnit.SECONDS); - - String value = secretsProvider.get("/my/secret"); + ```java hl_lines="9 10 14 19 22-25" + import java.time.Duration; + import software.amazon.lambda.powertools.parameters.appconfig.AppConfigProvider; + import software.amazon.lambda.powertools.parameters.cache.CacheManager; - } - ``` - -=== "Provider with age for each param" - - ```java hl_lines="8" - import software.amazon.lambda.powertools.parameters.SecretsProvider; - import software.amazon.lambda.powertools.parameters.ParamManager; - - public class AppWithSecrets implements RequestHandler { - SecretsManagerClient client = SecretsManagerClient.builder().region(Region.EU_CENTRAL_1).build(); + public class CustomizeCache { - SecretsProvider secretsProvider = ParamManager.getSecretsProvider(client); - - String value = secretsProvider.withMaxAge(10, ChronoUnit.SECONDS).get("/my/secret"); + public void CustomizeCache() { + + CacheManager cacheManager = new CacheManager(); + cacheManager.setDefaultExpirationTime(Duration.ofSeconds(10)); + + AppConfigProvider paramProvider = AppConfigProvider + .builder() + .withCacheManager(cacheManager) + .withClient(AppConfigDataClient.builder().build()) + .build(); + // Will use the default specified above - 10 seconds + String myParam1 = paramProvider.get("myParam1"); + + // Will override the default above + String myParam2 = paramProvider + .withMaxAge(20, ChronoUnit.SECONDS) + .get("myParam2"); + + return myParam2; + } } ``` + ### Transform values Parameter values can be transformed using ```withTransformation(transformerClass)```. -Base64 and JSON transformations are provided. For more complex transformation, you need to specify how to deserialize- +Base64 and JSON transformations are provided. For more complex transformation, you need to specify how to deserialize. -!!! warning "`SSMProvider.getMultiple()` does not support transformation and will return simple Strings." +!!! warning "`getMultiple()` does not support transformation and will return simple Strings." === "Base64 Transformation" ```java @@ -431,14 +434,16 @@ Base64 and JSON transformations are provided. For more complex transformation, y .get("/my/parameter/json", MyObj.class); ``` -## Write your own Transformer + + +#### Create your own Transformer You can write your own transformer, by implementing the `Transformer` interface and the `applyTransformation()` method. For example, if you wish to deserialize XML into an object. === "XmlTransformer.java" - ```java hl_lines="1" + ```java public class XmlTransformer implements Transformer { private final XmlMapper mapper = new XmlMapper(); @@ -470,172 +475,142 @@ To simplify the use of the library, you can chain all method calls before a get. ```java ssmProvider - .defaultMaxAge(10, SECONDS) // will set 10 seconds as the default cache TTL .withMaxAge(1, MINUTES) // will set the cache TTL for this value at 1 minute .withTransformation(json) // json is a static import from Transformer.json .withDecryption() // enable decryption of the parameter value .get("/my/param", MyObj.class); // finally get the value ``` -## Create your own provider +### Create your own Provider +You can create your own custom parameter store provider by implementing a handful of classes: -You can create your own custom parameter store provider by inheriting the ```BaseProvider``` class and implementing the -```String getValue(String key)``` method to retrieve data from your underlying store. All transformation and caching logic is handled by the get() methods in the base class. - -=== "Example implementation using S3 as a custom parameter" +=== "CustomProvider.java" ```java - public class S3Provider extends BaseProvider { - - private final S3Client client; - private String bucket; - - S3Provider(CacheManager cacheManager) { - this(cacheManager, S3Client.create()); + import java.util.Map; + import software.amazon.lambda.powertools.parameters.BaseProvider; + import software.amazon.lambda.powertools.parameters.cache.CacheManager; + import software.amazon.lambda.powertools.parameters.transform.TransformationManager; + + /** + * Our custom parameter provider itself. This does the heavy lifting of retrieving + * parameters from whatever our underlying parameter store might be. + **/ + public class CustomProvider extends BaseProvider { + + public CustomProvider(CacheManager cacheManager, TransformationManager transformationManager) { + super(cacheManager, transformationManager); } - - S3Provider(CacheManager cacheManager, S3Client client) { - super(cacheManager); - this.client = client; - } - - public S3Provider withBucket(String bucket) { - this.bucket = bucket; - return this; + + public CustomProviderBuilder builder() { + return new CustomProviderBuilder(); } @Override protected String getValue(String key) { - if (bucket == null) { - throw new IllegalStateException("A bucket must be specified, using withBucket() method"); - } - - GetObjectRequest request = GetObjectRequest.builder().bucket(bucket).key(key).build(); - ResponseBytes response = client.getObject(request, ResponseTransformer.toBytes()); - return response.asUtf8String(); + throw new RuntimeException("TODO - return a single value"); } @Override protected Map getMultipleValues(String path) { - if (bucket == null) { - throw new IllegalStateException("A bucket must be specified, using withBucket() method"); - } - - ListObjectsV2Request listRequest = ListObjectsV2Request.builder().bucket(bucket).prefix(path).build(); - List s3Objects = client.listObjectsV2(listRequest).contents(); - - Map result = new HashMap<>(); - s3Objects.forEach(s3Object -> { - result.put(s3Object.key(), getValue(s3Object.key())); - }); - - return result; + throw new RuntimeException("TODO - Optional - return multiple values"); } - - @Override - protected void resetToDefaults() { - super.resetToDefaults(); - bucket = null; - } - } ``` -=== "Using custom parameter store" - - ```java hl_lines="3" - S3Provider provider = new S3Provider(ParamManager.getCacheManager()); - - provider.setTransformationManager(ParamManager.getTransformationManager()); - - String value = provider.withBucket("myBucket").get("myKey"); - ``` - -## Annotation +=== "CustomProviderBuilder.java" -You can make use of the annotation `@Param` to inject a parameter value in a variable. - -By default, it will use `SSMProvider` to retrieve the value from AWS System Manager Parameter Store. -You could specify a different provider as long as it extends `BaseProvider` and/or a `Transformer`. + ```java + /** + * Provides a builder-style interface to configure our @{link CustomProvider}. + **/ + public class CustomProviderBuilder { + private CacheManager cacheManager; + private TransformationManager transformationManager; + + /** + * Create a {@link CustomProvider} instance. + * + * @return a {@link CustomProvider} + */ + public CustomProvider build() { + if (cacheManager == null) { + cacheManager = new CacheManager(); + } + return new CustomProvider(cacheManager, transformationManager); + } -=== "Param Annotation" + /** + * Provide a CacheManager to the {@link CustomProvider} + * + * @param cacheManager the manager that will handle the cache of parameters + * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) + */ + public CustomProviderBuilder withCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; + return this; + } - ```java hl_lines="3" - public class AppWithAnnotation implements RequestHandler { - - @Param(key = "/my/parameter/json") - ObjectToDeserialize value; - + /** + * Provide a transformationManager to the {@link CustomProvider} + * + * @param transformationManager the manager that will handle transformation of parameters + * @return the builder to chain calls (eg.
builder.withTransformationManager().build()
) + */ + public CustomProviderBuilder withTransformationManager(TransformationManager transformationManager) { + this.transformationManager = transformationManager; + return this; + } } ``` -=== "Custom Provider Usage" - - ```java hl_lines="3" - public class AppWithAnnotation implements RequestHandler { - - @Param(key = "/my/parameter/json" provider = SecretsProvider.class, transformer = JsonTransformer.class) - ObjectToDeserialize value; - +=== "CustomProviderParam.java" + + ```java + import java.lang.annotation.ElementType; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + import software.amazon.lambda.powertools.parameters.transform.Transformer; + + /** + * Aspect to inject a parameter from our custom provider. Note that if you + * want to implement a provider _without_ an Aspect and field injection, you can + * skip implementing both this and the {@link CustomProviderAspect} class. + **/ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface CustomProviderParam { + // The parameter key + String key(); + + // The transformer to use + Class transformer() default Transformer.class; } ``` - In this case ```SecretsProvider``` will be used to retrieve a raw value that is then trasformed into the target Object by using ```JsonTransformer```. - To show the convenience of the annotation compare the following two code snippets. - - -### Install - -If you want to use the ```@Param``` annotation in your project add configuration to compile-time weave (CTW) the powertools-parameters aspects into your project. +=== "CustomProviderAspect.java" -=== "Maven" + ```java - ```xml - - - ... - - dev.aspectj - aspectj-maven-plugin - 1.13.1 - - ... - - ... - - software.amazon.lambda - powertools-parameters - - - - - - - compile - - - - - ... - - - ``` + /** + * Aspect to inject a parameter from our custom provider where the {@link CustomProviderParam} + * annotation is used. + **/ + @Aspect + public class CustomProviderAspect extends BaseParamAspect { -=== "Gradle" - - ```groovy - plugins{ - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.3.0' - } - - repositories { - mavenCentral() - } + @Pointcut("get(* *) && @annotation(ddbConfigParam)") + public void getParam(CustomProviderParam customConfigParam) { + } + + @Around("getParam(customConfigParam)") + public Object injectParam(final ProceedingJoinPoint joinPoint, final CustomProviderParam customConfigParam) { + BaseProvider provider = CustomProvider.builder().build(); - dependencies { - ... - aspect 'software.amazon.lambda:powertools-parameters:{{ powertools.version }}' - implementation 'org.aspectj:aspectjrt:1.9.19' + return getAndTransform(customConfigParam.key(), ddbConfigParam.transformer(), provider, + (FieldSignature) joinPoint.getSignature()); + } + } - ``` \ No newline at end of file + ``` diff --git a/docs/utilities/sqs_batch.md b/docs/utilities/sqs_batch.md deleted file mode 100644 index 658f7b085..000000000 --- a/docs/utilities/sqs_batch.md +++ /dev/null @@ -1,489 +0,0 @@ ---- -title: SQS Batch Processing (Deprecated) -description: Utility ---- - -!!! warning - The SQS batch module is now deprecated and will be removed in v2 of the library. Use the [batch module](batch.md), - and check out **[migrating to the batch library](#migrating-to-the-batch-library)** for migration instructions. - -The SQS batch processing utility provides a way to handle partial failures when processing batches of messages from SQS. -The utility handles batch processing for both -[standard](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues.html) and -[FIFO](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html) SQS queues. - -**Key Features** - -* Prevent successfully processed messages from being returned to SQS -* A simple interface for individually processing messages from a batch - -**Background** - -When using SQS as a Lambda event source mapping, Lambda functions can be triggered with a batch of messages from SQS. -If your function fails to process any message from the batch, the entire batch returns to your SQS queue, and your -Lambda function will be triggered with the same batch again. With this utility, messages within a batch will be handled individually - only messages that were not successfully processed -are returned to the queue. - -!!! warning - While this utility lowers the chance of processing messages more than once, it is not guaranteed. We recommend implementing processing logic in an idempotent manner wherever possible. - More details on how Lambda works with SQS can be found in the [AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html) - -## Install - -Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes. - -=== "Maven Java 11+" - - ```xml hl_lines="3-7 16 18 24-27"" - - ... - - software.amazon.lambda - powertools-sqs - {{ powertools.version }} - - ... - - ... - - - - ... - - dev.aspectj - aspectj-maven-plugin - 1.13.1 - - 11 - 11 - 11 - - - software.amazon.lambda - powertools-sqs - - - - - - - compile - - - - - ... - - - ``` - -=== "Maven Java 1.8" - - ```xml hl_lines="3-7 16 18 24-27" - - ... - - software.amazon.lambda - powertools-sqs - {{ powertools.version }} - - ... - - ... - - - - ... - - org.codehaus.mojo - aspectj-maven-plugin - 1.14.0 - - 1.8 - 1.8 - 1.8 - - - software.amazon.lambda - powertools-sqs - - - - - - - compile - - - - - ... - - - ``` - -=== "Gradle Java 11+" - - ```groovy hl_lines="3 11" - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0' - } - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-sqs:{{ powertools.version }}' - } - - sourceCompatibility = 11 // or higher - targetCompatibility = 11 // or higher - ``` - -=== "Gradle Java 1.8" - - ```groovy hl_lines="3 11" - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' - } - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-sqs:{{ powertools.version }}' - } - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - ``` - -## IAM Permissions - -This utility requires additional permissions to work as expected. Lambda functions using this utility require the `sqs:DeleteMessageBatch` permission. - -If you are also using [nonRetryableExceptions](#move-non-retryable-messages-to-a-dead-letter-queue) attribute, utility will need additional permission of `sqs:GetQueueAttributes` on source SQS. -It also needs `sqs:SendMessage` and `sqs:SendMessageBatch` on configured dead letter queue. - -If source or dead letter queue is configured to use encryption at rest using [AWS Key Management Service (KMS)](https://aws.amazon.com/kms/), function will need additional permissions of -`kms:GenerateDataKey` and `kms:Decrypt` on the KMS key being used for encryption. Refer [docs](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-key-management.html#compatibility-with-aws-services) for more details. - -Refer [example project](https://github.com/aws-samples/aws-lambda-powertools-examples/blob/main/java/SqsBatchProcessing/template.yaml#L105) for policy details example. - - -## Processing messages from SQS - -You can use either **[SqsBatch annotation](#sqsbatch-annotation)**, or **[SqsUtils Utility API](#sqsutils-utility-api)** as a fluent API. - -Both have nearly the same behaviour when it comes to processing messages from the batch: - -* **Entire batch has been successfully processed**, where your Lambda handler returned successfully, we will let SQS delete the batch to optimize your cost -* **Entire Batch has been partially processed successfully**, where exceptions were raised within your `SqsMessageHandler` interface implementation, we will: - - **1)** Delete successfully processed messages from the queue by directly calling `sqs:DeleteMessageBatch` - - **2)** If a message with a [message group ID](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagegroupid-property.html) fails, - the processing of the batch will be stopped and the remainder of the messages will be returned to SQS. - This behaviour [is required to handle SQS FIFO queues](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting). - - **3)** if non retryable exceptions occur, messages resulting in configured exceptions during processing will be immediately moved to the dead letter queue associated to the source SQS queue or deleted from the source SQS queue if `deleteNonRetryableMessageFromQueue` is set to `true`. - - **4)** Raise `SQSBatchProcessingException` to ensure failed messages return to your SQS queue - -The only difference is that **SqsUtils Utility API** will give you access to return from the processed messages if you need. Exception `SQSBatchProcessingException` thrown from the -utility will have access to both successful and failed messaged along with failure exceptions. - -## Functional Interface SqsMessageHandler - -Both [annotation](#sqsbatch-annotation) and [SqsUtils Utility API](#sqsutils-utility-api) requires an implementation of functional interface `SqsMessageHandler`. - -This implementation is responsible for processing each individual message from the batch, and to raise an exception if unable to process any of the messages sent. - -**Any non-exception/successful return from your record handler function** will instruct utility to queue up each individual message for deletion. - -### SqsBatch annotation - -When using this annotation, you need provide a class implementation of `SqsMessageHandler` that will process individual messages from the batch - It should raise an exception if it is unable to process the record. - -All records in the batch will be passed to this handler for processing, even if exceptions are thrown - Here's the behaviour after completing the batch: - -* **Any successfully processed messages**, we will delete them from the queue via `sqs:DeleteMessageBatch`. -* **if, nonRetryableExceptions attribute is used**, messages resulting in configured exceptions during processing will be immediately moved to the dead letter queue associated to the source SQS queue or deleted from the source SQS queue if `deleteNonRetryableMessageFromQueue` is set to `true`. -* **Any unprocessed messages detected**, we will raise `SQSBatchProcessingException` to ensure failed messages return to your SQS queue. - -!!! warning - You will not have access to the **processed messages** within the Lambda Handler - all processing logic will and should be performed by the implemented `#!java SqsMessageHandler#process()` function. - -=== "AppSqsEvent.java" - - ```java hl_lines="7" - import software.amazon.lambda.powertools.sqs.SqsBatch; - import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - import software.amazon.lambda.powertools.sqs.SqsUtils; - - public class AppSqsEvent implements RequestHandler { - @Override - @SqsBatch(SampleMessageHandler.class) - public String handleRequest(SQSEvent input, Context context) { - return "{\"statusCode\": 200}"; - } - - public class SampleMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - // This will be called for each individual message from a batch - // It should raise an exception if the message was not processed successfully - String returnVal = doSomething(message.getBody()); - return returnVal; - } - } - } - ``` - -=== "AppSqsEventWithNonRetryableExceptions.java" - - ```java hl_lines="7 21" - import software.amazon.lambda.powertools.sqs.SqsBatch; - import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - import software.amazon.lambda.powertools.sqs.SqsUtils; - - public class AppSqsEvent implements RequestHandler { - @Override - @SqsBatch(value = SampleMessageHandler.class, nonRetryableExceptions = {IllegalArgumentException.class}) - public String handleRequest(SQSEvent input, Context context) { - return "{\"statusCode\": 200}"; - } - - public class SampleMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - // This will be called for each individual message from a batch - // It should raise an exception if the message was not processed successfully - String returnVal = doSomething(message.getBody()); - - if(/**Business validation failure**/) { - throw new IllegalArgumentException("Failed business validation. No point of retrying. Move me to DLQ." + message.getMessageId()); - } - - return returnVal; - } - } - } - ``` - - -### SqsUtils Utility API - -If you require access to the result of processed messages, you can use this utility. The result from calling **`#!java SqsUtils#batchProcessor()`** on the context manager will be a list of all the return values -from your **`#!java SqsMessageHandler#process()`** function. - -You can also use the utility in functional way by providing inline implementation of functional interface **`#!java SqsMessageHandler#process()`** - - -=== "Utility API" - - ```java hl_lines="4" - public class AppSqsEvent implements RequestHandler> { - @Override - public List handleRequest(SQSEvent input, Context context) { - List returnValues = SqsUtils.batchProcessor(input, SampleMessageHandler.class); - - return returnValues; - } - - public class SampleMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - // This will be called for each individual message from a batch - // It should raise an exception if the message was not processed successfully - String returnVal = doSomething(message.getBody()); - return returnVal; - } - } - } - ``` - -=== "Function implementation" - - ```java hl_lines="5 6 7 8 9 10" - public class AppSqsEvent implements RequestHandler> { - - @Override - public List handleRequest(SQSEvent input, Context context) { - List returnValues = SqsUtils.batchProcessor(input, (message) -> { - // This will be called for each individual message from a batch - // It should raise an exception if the message was not processed successfully - String returnVal = doSomething(message.getBody()); - return returnVal; - }); - - return returnValues; - } - } - ``` - -## Passing custom SqsClient - -If you need to pass custom SqsClient such as region to the SDK, you can pass your own `SqsClient` to be used by utility either for -**[SqsBatch annotation](#sqsbatch-annotation)**, or **[SqsUtils Utility API](#sqsutils-utility-api)**. - -=== "App.java" - - ```java hl_lines="3 4" - public class AppSqsEvent implements RequestHandler> { - static { - SqsUtils.overrideSqsClient(SqsClient.builder() - .build()); - } - - @Override - public List handleRequest(SQSEvent input, Context context) { - List returnValues = SqsUtils.batchProcessor(input, SampleMessageHandler.class); - - return returnValues; - } - - public class SampleMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - // This will be called for each individual message from a batch - // It should raise an exception if the message was not processed successfully - String returnVal = doSomething(message.getBody()); - return returnVal; - } - } - } - ``` - -## Suppressing exceptions - -If you want to disable the default behavior where `SQSBatchProcessingException` is raised if there are any exception, you can pass the `suppressException` boolean argument. - -=== "Within SqsBatch annotation" - - ```java hl_lines="2" - @Override - @SqsBatch(value = SampleMessageHandler.class, suppressException = true) - public String handleRequest(SQSEvent input, Context context) { - return "{\"statusCode\": 200}"; - } - ``` - -=== "Within SqsUtils Utility API" - - ```java hl_lines="3" - @Override - public List handleRequest(SQSEvent input, Context context) { - List returnValues = SqsUtils.batchProcessor(input, true, SampleMessageHandler.class); - - return returnValues; - } - ``` - -## Move non retryable messages to a dead letter queue - -If you want certain exceptions to be treated as permanent failures during batch processing, i.e. exceptions where the result of retrying will -always be a failure and want these can be immediately moved to the dead letter queue associated to the source SQS queue, you can use `SqsBatch#nonRetryableExceptions()` -to configure such exceptions. - -If you want such messages to be deleted instead, set `SqsBatch#deleteNonRetryableMessageFromQueue()` to `true`. By default, its value is `false`. - -Same capability is also provided by [SqsUtils Utility API](#sqsutils-utility-api). - -!!! info - Make sure the lambda function has required permissions needed by utility. Refer [this section](#iam-permissions). - -=== "SqsBatch annotation" - - ```java hl_lines="7 21" - import software.amazon.lambda.powertools.sqs.SqsBatch; - import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - import software.amazon.lambda.powertools.sqs.SqsUtils; - - public class AppSqsEvent implements RequestHandler { - @Override - @SqsBatch(value = SampleMessageHandler.class, nonRetryableExceptions = {IllegalArgumentException.class}) - public String handleRequest(SQSEvent input, Context context) { - return "{\"statusCode\": 200}"; - } - - public class SampleMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - // This will be called for each individual message from a batch - // It should raise an exception if the message was not processed successfully - String returnVal = doSomething(message.getBody()); - - if(/**Business validation failure**/) { - throw new IllegalArgumentException("Failed business validation. No point of retrying. Move me to DLQ." + message.getMessageId()); - } - - return returnVal; - } - } - } - ``` - -=== "SqsBatch API" - - ```java hl_lines="9 23" - import software.amazon.lambda.powertools.sqs.SqsBatch; - import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - import software.amazon.lambda.powertools.sqs.SqsUtils; - - public class AppSqsEvent implements RequestHandler { - @Override - public String handleRequest(SQSEvent input, Context context) { - - SqsUtils.batchProcessor(input, BatchProcessor.class, IllegalArgumentException.class); - - return "{\"statusCode\": 200}"; - } - - public class SampleMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - // This will be called for each individual message from a batch - // It should raise an exception if the message was not processed successfully - String returnVal = doSomething(message.getBody()); - - if(/**Business validation failure**/) { - throw new IllegalArgumentException("Failed business validation. No point of retrying. Move me to DLQ." + message.getMessageId()); - } - - return returnVal; - } - } - } - ``` - -## Migrating to the Batch Library -The [batch processing library](batch.md) provides a way to process messages and gracefully handle partial failures for -SQS, Kinesis Streams, and DynamoDB Streams batch sources. In comparison the legacy SQS Batch library, it relies on -[Lambda partial batch responses](https://aws.amazon.com/about-aws/whats-new/2021/11/aws-lambda-partial-batch-response-sqs-event-source/), -which allows the library to provide a simpler, reliable interface for processing batches. - -In order to get started, check out the [processing messages from SQS](batch/#processing-messages-from-sqs) documentation. -In most cases, you will simply be able to retain your existing batch message handler function, and wrap it with the new -batch processing interface. Unlike this module, As the batch processor uses *partial batch responses* to communicate to -Lambda which messages have been processed and must be removed from the queue, the return of the handler's process function -must be returned to Lambda. - -The new library also no longer requires the `SQS:DeleteMessage` action on the Lambda function's role policy, as Lambda -itself now manages removal of messages from the queue. - -!!! info - Some tuneables from this library are no longer provided. - - * **Non-retryable Exceptions** - there is no mechanism to indicate in a partial batch response that a particular message - should not be retried and instead moved to DLQ - a message either succeeds, or fails and is retried. A message - will be moved to the DLQ once the normal retry process has expired. - * **Suppress Exception** - The new batch processor does not throw an exception on failure of a handler. Instead, - its result must be returned by your code from your message handler to Lambda, so that Lambda can manage - the completed messages and retry behaviour. \ No newline at end of file diff --git a/docs/utilities/sqs_large_message_handling.md b/docs/utilities/sqs_large_message_handling.md deleted file mode 100644 index 9d721f544..000000000 --- a/docs/utilities/sqs_large_message_handling.md +++ /dev/null @@ -1,296 +0,0 @@ ---- -title: SQS Large Message Handling (Deprecated) -description: Utility ---- - -!!! warning - This module is now deprecated and will be removed in version 2. - See [Large Message Handling](large_messages.md) and - [the migration guide](large_messages.md#migration-from-the-sqs-large-message-utility) - for the new module (`powertools-large-messages`) documentation - -The large message handling utility handles SQS messages which have had their payloads -offloaded to S3 due to them being larger than the SQS maximum. - -The utility automatically retrieves messages which have been offloaded to S3 using the -[amazon-sqs-java-extended-client-lib](https://github.com/awslabs/amazon-sqs-java-extended-client-lib) -client library. Once the message payloads have been processed successful the -utility can delete the message payloads from S3. - -This utility is compatible with versions *[1.1.0+](https://github.com/awslabs/amazon-sqs-java-extended-client-lib)* of -amazon-sqs-java-extended-client-lib. - -=== "Maven" - -```xml - - - com.amazonaws - amazon-sqs-java-extended-client-lib - 1.1.0 - -``` -=== "Gradle" - - ```groovy - dependencies { - implementation 'com.amazonaws:amazon-sqs-java-extended-client-lib:1.1.0' - } - ``` - -## Install -Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes. - -=== "Maven Java 11+" - -```xml hl_lines="3-7 16 18 24-27" - -... - -software.amazon.lambda -powertools-sqs -{{ powertools.version }} - -... - -... - - - -... - -dev.aspectj -aspectj-maven-plugin -1.13.1 - -11 -11 -11 - - -software.amazon.lambda -powertools-sqs - - - - - - -compile - - - - -... - - -``` - -=== "Maven Java 1.8" - - ```xml hl_lines="3-7 16 18 24-27" - - ... - - software.amazon.lambda - powertools-sqs - {{ powertools.version }} - - ... - - ... - - - - ... - - org.codehaus.mojo - aspectj-maven-plugin - 1.14.0 - - 1.8 - 1.8 - 1.8 - - - software.amazon.lambda - powertools-sqs - - - - - - - compile - - - - - ... - - - ``` - -=== "Gradle Java 11+" - - ```groovy hl_lines="3 11" - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0' - } - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-sqs:{{ powertools.version }}' - } - - sourceCompatibility = 11 // or higher - targetCompatibility = 11 // or higher - ``` - -=== "Gradle Java 1.8" - - ```groovy hl_lines="3 11" - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' - } - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-sqs:{{ powertools.version }}' - } - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - ``` - -## Lambda handler - -The annotation `@SqsLargeMessage` should be used with the handleRequest method of a class -which implements `com.amazonaws.services.lambda.runtime.RequestHandler` with -`com.amazonaws.services.lambda.runtime.events.SQSEvent` as the first parameter. - -=== "SqsMessageHandler.java" - - ```java hl_lines="6" - import software.amazon.lambda.powertools.sqs.SqsLargeMessage; - - public class SqsMessageHandler implements RequestHandler { - - @Override - @SqsLargeMessage - public String handleRequest(SQSEvent sqsEvent, Context context) { - // process messages - - return "ok"; - } - } - ``` - -`@SqsLargeMessage` creates a default S3 Client `AmazonS3 amazonS3 = AmazonS3ClientBuilder.defaultClient()`. - -!!! tip -When the Lambda function is invoked with an event from SQS, each received record -in the SQSEvent is checked to see to validate if it is offloaded to S3. -If it does then `getObject(bucket, key)` will be called, and the payload retrieved. -If there is an error during this process then the function will fail with a `FailedProcessingLargePayloadException` -exception. - - If the request handler method returns without error then each payload will be - deleted from S3 using `deleteObject(bucket, key)` - -To disable deletion of payloads setting the following annotation parameter: - -=== "Disable payload deletion" - - ```java hl_lines="3" - import software.amazon.lambda.powertools.sqs.SqsLargeMessage; - - @SqsLargeMessage(deletePayloads=false) - public class SqsMessageHandler implements RequestHandler { - - } - ``` - -## Utility - -If you want to avoid using annotation and have control over error that can happen during payload enrichment use `SqsUtils.enrichedMessageFromS3()`. -It provides you access with a list of `SQSMessage` object enriched from S3 payload. - -Original `SQSEvent` object is never mutated. You can also control if the S3 payload should be deleted after successful -processing. - -=== "Functional API without annotation" - - ```java hl_lines="9 10 11 14 15 16 17 18 19 20 21 22 27 28 29" - import software.amazon.lambda.powertools.sqs.SqsLargeMessage; - import software.amazon.lambda.powertools.sqs.SqsUtils; - - public class SqsMessageHandler implements RequestHandler { - - @Override - public String handleRequest(SQSEvent sqsEvent, Context context) { - - Map sqsMessage = SqsUtils.enrichedMessageFromS3(sqsEvent, sqsMessages -> { - // Some business logic - Map someBusinessLogic = new HashMap<>(); - someBusinessLogic.put("Message", sqsMessages.get(0).getBody()); - return someBusinessLogic; - }); - - // Do not delete payload after processing. - Map sqsMessage = SqsUtils.enrichedMessageFromS3(sqsEvent, false, sqsMessages -> { - // Some business logic - Map someBusinessLogic = new HashMap<>(); - someBusinessLogic.put("Message", sqsMessages.get(0).getBody()); - return someBusinessLogic; - }); - - // Better control over exception during enrichment - try { - // Do not delete payload after processing. - SqsUtils.enrichedMessageFromS3(sqsEvent, false, sqsMessages -> { - // Some business logic - }); - } catch (FailedProcessingLargePayloadException e) { - // handle any exception. - } - - return "ok"; - } - } - ``` - -## Overriding the default S3Client - -If you require customisations to the default S3Client, you can create your own `S3Client` and pass it to be used by utility either for -**[SqsLargeMessage annotation](#lambda-handler)**, or **[SqsUtils Utility API](#utility)**. - -=== "App.java" - - ```java hl_lines="4 5 11" - import software.amazon.lambda.powertools.sqs.SqsLargeMessage; - - static { - SqsUtils.overrideS3Client(S3Client.builder() - .build()); - } - - public class SqsMessageHandler implements RequestHandler { - - @Override - @SqsLargeMessage - public String handleRequest(SQSEvent sqsEvent, Context context) { - // process messages - - return "ok"; - } - } - ``` \ No newline at end of file diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index 928ffb6c8..96bdd142b 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -8,13 +8,12 @@ This utility provides JSON Schema validation for payloads held within events and **Key features** * Validate incoming events and responses -* Built-in validation for most common events (API Gateway, SNS, SQS, ...) +* Built-in validation for most common events (API Gateway, SNS, SQS, ...) and support for partial batch failures (SQS, Kinesis) * JMESPath support validate only a sub part of the event ## Install -Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes. -=== "Maven Java 11+" +=== "Maven" ```xml hl_lines="3-7 16 18 24-27" ... @@ -33,7 +32,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 11 11 @@ -45,6 +44,14 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl + + + org.aspectj + aspectjtools + + 1.9.22 + + @@ -58,52 +65,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ``` -=== "Maven Java 1.8" - - ```xml hl_lines="3-7 16 18 24-27" - - ... - - software.amazon.lambda - powertools-validation - {{ powertools.version }} - - ... - - ... - - - - ... - - org.codehaus.mojo - aspectj-maven-plugin - 1.14.0 - - 1.8 - 1.8 - 1.8 - - - software.amazon.lambda - powertools-validation - - - - - - - compile - - - - - ... - - - ``` - -=== "Gradle Java 11+" +=== "Gradle" ```groovy hl_lines="3 11" plugins { @@ -123,40 +85,29 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl targetCompatibility = 11 // or higher ``` -=== "Gradle Java 1.8" - - ```groovy hl_lines="3 11" - plugins { - id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' - } - - repositories { - mavenCentral() - } - - dependencies { - aspect 'software.amazon.lambda:powertools-validation:{{ powertools.version }}' - } - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - ``` - - ## Validating events You can validate inbound and outbound events using `@Validation` annotation. You can also use the `Validator#validate()` methods, if you want more control over the validation process such as handling a validation error. -We support JSON schema version 4, 6, 7 and 201909 (from [jmespath-jackson library](https://github.com/burtcorp/jmespath-java)). +We support JSON schema version 4, 6, 7, 2019-09 and 2020-12 using the [NetworkNT JSON Schema Validator](https://github.com/networknt/json-schema-validator). ([Compatibility with JSON Schema versions](https://github.com/networknt/json-schema-validator/blob/master/doc/compatibility.md)). + +The validator is configured to enable format assertions by default even for 2019-09 and 2020-12. ### Validation annotation `@Validation` annotation is used to validate either inbound events or functions' response. -It will fail fast with `ValidationException` if an event or response doesn't conform with given JSON Schema. +It will fail fast if an event or response doesn't conform with given JSON Schema. For most type of events a `ValidationException` will be thrown. + +For API gateway events associated with REST APIs and HTTP APIs - `APIGatewayProxyRequestEvent` and `APIGatewayV2HTTPEvent` - the `@Validation` +annotation will build and return a custom 400 / "Bad Request" response, with a body containing the validation errors. This saves you from having +to catch the validation exception and map it back to a meaningful user error yourself. + +For SQS and Kinesis events - `SQSEvent` and `KinesisEvent`- the `@Validation` annotation will add the invalid messages +to the batch item failures list in the response, respectively `SQSBatchResponse` and `StreamsEventResponse` +and removed from the event so that you do not process them within the handler. While it is easier to specify a json schema file in the classpath (using the notation `"classpath:/path/to/schema.json"`), you can also provide a JSON String containing the schema. @@ -212,33 +163,33 @@ You can also gracefully handle schema validation errors by catching `ValidationE For the following events and responses, the Validator will automatically perform validation on the content. -** Events ** - - Type of event | Class | Path to content | - ------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- - API Gateway REST | APIGatewayProxyRequestEvent | `body` - API Gateway HTTP | APIGatewayV2HTTPEvent | `body` - Application Load Balancer | ApplicationLoadBalancerRequestEvent | `body` - Cloudformation Custom Resource | CloudFormationCustomResourceEvent | `resourceProperties` - CloudWatch Logs | CloudWatchLogsEvent | `awslogs.powertools_base64_gzip(data)` - EventBridge / Cloudwatch | ScheduledEvent | `detail` - Kafka | KafkaEvent | `records[*][*].value` - Kinesis | KinesisEvent | `Records[*].kinesis.powertools_base64(data)` - Kinesis Firehose | KinesisFirehoseEvent | `Records[*].powertools_base64(data)` - Kinesis Analytics from Firehose | KinesisAnalyticsFirehoseInputPreprocessingEvent | `Records[*].powertools_base64(data)` - Kinesis Analytics from Streams | KinesisAnalyticsStreamsInputPreprocessingEvent | `Records[*].powertools_base64(data)` - SNS | SNSEvent | `Records[*].Sns.Message` - SQS | SQSEvent | `Records[*].body` - -** Responses ** - - Type of response | Class | Path to content (envelope) - ------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- - API Gateway REST | APIGatewayProxyResponseEvent} | `body` - API Gateway HTTP | APIGatewayV2HTTPResponse} | `body` - API Gateway WebSocket | APIGatewayV2WebSocketResponse} | `body` - Load Balancer | ApplicationLoadBalancerResponseEvent} | `body` - Kinesis Analytics | KinesisAnalyticsInputPreprocessingResponse} | `Records[*].powertools_base64(data)`` +**Events** + +| Type of event | Class | Path to content | +|---------------------------------|-------------------------------------------------|----------------------------------------------| +| API Gateway REST | APIGatewayProxyRequestEvent | `body` | +| API Gateway HTTP | APIGatewayV2HTTPEvent | `body` | +| Application Load Balancer | ApplicationLoadBalancerRequestEvent | `body` | +| Cloudformation Custom Resource | CloudFormationCustomResourceEvent | `resourceProperties` | +| CloudWatch Logs | CloudWatchLogsEvent | `awslogs.powertools_base64_gzip(data)` | +| EventBridge / Cloudwatch | ScheduledEvent | `detail` | +| Kafka | KafkaEvent | `records[*][*].value` | +| Kinesis | KinesisEvent | `Records[*].kinesis.powertools_base64(data)` | +| Kinesis Firehose | KinesisFirehoseEvent | `Records[*].powertools_base64(data)` | +| Kinesis Analytics from Firehose | KinesisAnalyticsFirehoseInputPreprocessingEvent | `Records[*].powertools_base64(data)` | +| Kinesis Analytics from Streams | KinesisAnalyticsStreamsInputPreprocessingEvent | `Records[*].powertools_base64(data)` | +| SNS | SNSEvent | `Records[*].Sns.Message` | +| SQS | SQSEvent | `Records[*].body` | + +**Responses** + +| Type of response | Class | Path to content (envelope) | +|-----------------------|---------------------------------------------|---------------------------------------| +| API Gateway REST | APIGatewayProxyResponseEvent} | `body` | +| API Gateway HTTP | APIGatewayV2HTTPResponse} | `body` | +| API Gateway WebSocket | APIGatewayV2WebSocketResponse} | `body` | +| Load Balancer | ApplicationLoadBalancerResponseEvent} | `body` | +| Kinesis Analytics | KinesisAnalyticsInputPreprocessingResponse} | `Records[*].powertools_base64(data)` | ## Custom events and responses @@ -292,7 +243,8 @@ and [function](https://jmespath.org/tutorial.html#functions) expressions, where ## Change the schema version -By default, powertools-validation is configured with [V7](https://json-schema.org/draft-07/json-schema-release-notes.html). +By default, powertools-validation is configured to use [V7](https://json-schema.org/draft-07/json-schema-release-notes.html) as the default dialect if [`$schema`](https://json-schema.org/understanding-json-schema/reference/schema#schema) is not explicitly specified within the schema. If [`$schema`](https://json-schema.org/understanding-json-schema/reference/schema#schema) is explicitly specified within the schema, the validator will use the specified dialect. + You can use the `ValidationConfig` to change that behaviour. === "Handler with custom schema version" diff --git a/examples/README.md b/examples/README.md index 6dbe00185..41640b5ad 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,16 +5,15 @@ Each example can be copied from its subdirectory and used independently of the r ## Examples -* [powertools-examples-core](powertools-examples-core) - Demonstrates the core logging, tracing, and metrics modules with different build tools and languages - * [CDK](./powertools-examples-core/cdk) - * [Gradle](./powertools-examples-core/gradle) - * [SAM](./powertools-examples-core/sam) - * [Serverless](./powertools-examples-core/serverless) - * [Kotlin](./powertools-examples-core/kotlin) +* [powertools-examples-core-utilities](powertools-examples-core-utilities) - Demonstrates the core logging, tracing, and metrics modules with different build tools and languages + * [CDK](./powertools-examples-core-utilities/cdk) + * [Gradle](./powertools-examples-core-utilities/gradle) + * [SAM](./powertools-examples-core-utilities/sam) + * [Serverless](./powertools-examples-core-utilities/serverless) + * [Kotlin](./powertools-examples-core-utilities/kotlin) * [powertools-examples-idempotency](powertools-examples-idempotency) - An idempotent HTTP API * [powertools-examples-parameters](powertools-examples-parameters) - Uses the parameters module to provide runtime parameters to a function * [powertools-examples-serialization](powertools-examples-serialization) - Uses the serialization module to serialize and deserialize API Gateway & SQS payloads -* [powertools-examples-sqs](powertools-examples-sqs) - Processes SQS batch requests (**Deprecated** - will be replaced by `powertools-examples-batch` in version 2 of this library) * [powertools-examples-validation](powertools-examples-validation) - Uses the validation module to validate user requests received via API Gateway * [powertools-examples-cloudformation](powertools-examples-cloudformation) - Deploys a Cloudformation custom resource * [powertools-examples-batch](powertools-examples-batch) - Examples for each of the different batch processing deployments @@ -63,8 +62,6 @@ If you're not using SAM, you can look for examples for other tools under [powert You can find more examples in the https://github.com/aws/aws-sam-cli-app-templates project: -* [Java 8 + Maven](https://github.com/aws/aws-sam-cli-app-templates/tree/master/java8/hello-pt-maven) -* [Java 8 on Amazon Linux 2 + Maven](https://github.com/aws/aws-sam-cli-app-templates/tree/master/java8.al2/hello-pt-maven) * [Java 11 + Maven](https://github.com/aws/aws-sam-cli-app-templates/tree/master/java11/hello-pt-maven) * [Java 17 + Maven](https://github.com/aws/aws-sam-cli-app-templates/tree/master/java17/hello-pt-maven) * [Java 17 + Gradle](https://github.com/aws/aws-sam-cli-app-templates/tree/master/java17/hello-pt-gradle) diff --git a/examples/pom.xml b/examples/pom.xml index 5c423f251..3462be0b7 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -20,24 +20,25 @@ software.amazon.lambda powertools-examples - 1.19.0-SNAPSHOT + 2.0.0-RC1 pom - Powertools for AWS Lambda (Java) library Examples + Powertools for AWS Lambda (Java) - Examples A suite of examples accompanying for Powertools for AWS Lambda (Java). - powertools-examples-core/sam - powertools-examples-core/cdk/app - powertools-examples-core/cdk/infra - powertools-examples-core/serverless - powertools-examples-core/terraform + powertools-examples-core-utilities/sam + powertools-examples-core-utilities/sam-graalvm + powertools-examples-core-utilities/cdk/app + powertools-examples-core-utilities/cdk/infra + powertools-examples-core-utilities/serverless + powertools-examples-core-utilities/terraform powertools-examples-idempotency - powertools-examples-parameters + powertools-examples-parameters/sam + powertools-examples-parameters/sam-graalvm powertools-examples-serialization - powertools-examples-sqs powertools-examples-batch powertools-examples-validation powertools-examples-cloudformation @@ -49,6 +50,7 @@ org.apache.maven.plugins maven-deploy-plugin + 3.1.2 true diff --git a/examples/powertools-examples-batch/deploy/sqs/template.yml b/examples/powertools-examples-batch/deploy/sqs/template.yml index 764ba4863..2f1d6c363 100644 --- a/examples/powertools-examples-batch/deploy/sqs/template.yml +++ b/examples/powertools-examples-batch/deploy/sqs/template.yml @@ -7,12 +7,10 @@ Globals: Function: Timeout: 20 Runtime: java11 - MemorySize: 512 - Tracing: Active + MemorySize: 5400 Environment: Variables: POWERTOOLS_LOG_LEVEL: INFO - POWERTOOLS_LOGGER_SAMPLE_RATE: 1.0 POWERTOOLS_LOGGER_LOG_EVENT: true Resources: @@ -45,6 +43,9 @@ Resources: AliasName: alias/powertools-batch-sqs-demo TargetKeyId: !Ref CustomerKey + Bucket: + Type: AWS::S3::Bucket + DemoDlqSqsQueue: Type: AWS::SQS::Queue Properties: @@ -96,11 +97,57 @@ Resources: DemoSQSConsumerFunction: Type: AWS::Serverless::Function Properties: + Tracing: Active CodeUri: ../.. Handler: org.demo.batch.sqs.SqsBatchHandler::handleRequest Environment: Variables: POWERTOOLS_SERVICE_NAME: sqs-demo + BUCKET: !Ref Bucket + Policies: + - Statement: + - Sid: SQSDeleteGetAttribute + Effect: Allow + Action: + - sqs:DeleteMessageBatch + - sqs:GetQueueAttributes + Resource: !GetAtt DemoSqsQueue.Arn + - Sid: SQSSendMessageBatch + Effect: Allow + Action: + - sqs:SendMessageBatch + - sqs:SendMessage + Resource: !GetAtt DemoDlqSqsQueue.Arn + - Sid: SQSKMSKey + Effect: Allow + Action: + - kms:GenerateDataKey + - kms:Decrypt + Resource: !GetAtt CustomerKey.Arn + - Sid: WriteToS3 + Effect: Allow + Action: + - s3:PutObject + Resource: !Sub ${Bucket.Arn}/* + +# Events: +# MySQSEvent: +# Type: SQS +# Properties: +# Queue: !GetAtt DemoSqsQueue.Arn +# BatchSize: 100 +# MaximumBatchingWindowInSeconds: 60 + + DemoSQSParallelConsumerFunction: + Type: AWS::Serverless::Function + Properties: + Tracing: Active + CodeUri: ../.. + Handler: org.demo.batch.sqs.SqsParallelBatchHandler::handleRequest + Environment: + Variables: + POWERTOOLS_SERVICE_NAME: sqs-demo + BUCKET: !Ref Bucket Policies: - Statement: - Sid: SQSDeleteGetAttribute @@ -121,13 +168,19 @@ Resources: - kms:GenerateDataKey - kms:Decrypt Resource: !GetAtt CustomerKey.Arn + - Sid: WriteToS3 + Effect: Allow + Action: + - s3:PutObject + Resource: !Sub ${Bucket.Arn}/* + Events: MySQSEvent: Type: SQS Properties: Queue: !GetAtt DemoSqsQueue.Arn - BatchSize: 2 - MaximumBatchingWindowInSeconds: 300 + BatchSize: 100 + MaximumBatchingWindowInSeconds: 60 Outputs: DemoSqsQueue: diff --git a/examples/powertools-examples-batch/pom.xml b/examples/powertools-examples-batch/pom.xml index d3539d919..275a3c96f 100644 --- a/examples/powertools-examples-batch/pom.xml +++ b/examples/powertools-examples-batch/pom.xml @@ -5,16 +5,16 @@ 4.0.0 software.amazon.lambda.examples - 1.19.0-SNAPSHOT + 2.0.0-RC1 powertools-examples-batch jar - Powertools for AWS Lambda (Java) library Examples - Batch + Powertools for AWS Lambda (Java) - Examples - Batch - 2.20.0 - 1.8 - 1.8 - 2.21.1 + 11 + 11 + 1.9.20.1 + 2.30.31 @@ -25,7 +25,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} @@ -42,6 +42,17 @@ software.amazon.awssdk sdk-core ${sdk.version} + + + org.slf4j + slf4j-api + + + + + software.amazon.awssdk + s3 + ${sdk.version} software.amazon.awssdk @@ -63,6 +74,11 @@ kinesis ${sdk.version} + + org.aspectj + aspectjrt + ${aspectj.version} + @@ -70,7 +86,7 @@ dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 ${maven.compiler.source} ${maven.compiler.target} @@ -93,11 +109,18 @@ + + + org.aspectj + aspectjtools + ${aspectj.version} + + org.apache.maven.plugins maven-shade-plugin - 3.5.2 + 3.6.0 package @@ -124,81 +147,11 @@ org.apache.maven.plugins maven-deploy-plugin + 3.1.2 true - - - - jdk8 - - (,11) - - - 1.9.7 - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - dev.aspectj - aspectj-maven-plugin - ${aspectj.plugin.version} - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-logging - - - - - - - compile - test-compile - - - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - \ No newline at end of file diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBStreamBatchHandler.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBStreamBatchHandler.java index 988c49e86..e3c27c093 100644 --- a/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBStreamBatchHandler.java +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBStreamBatchHandler.java @@ -4,14 +4,14 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.batch.BatchMessageHandlerBuilder; import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; public class DynamoDBStreamBatchHandler implements RequestHandler { - private final static Logger LOGGER = LogManager.getLogger(DynamoDBStreamBatchHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDBStreamBatchHandler.class); private final BatchMessageHandler handler; public DynamoDBStreamBatchHandler() { diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBWriter.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBWriter.java index 953ba8f23..fc1b0747b 100644 --- a/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBWriter.java +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBWriter.java @@ -8,8 +8,8 @@ import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.demo.batch.model.DdbProduct; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; @@ -21,7 +21,7 @@ public class DynamoDBWriter implements RequestHandler { - private static final Logger LOGGER = LogManager.getLogger(DynamoDBWriter.class); + private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDBWriter.class); private final DynamoDbEnhancedClient enhancedClient; diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchHandler.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchHandler.java index b188df501..f5d7102b5 100644 --- a/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchHandler.java +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchHandler.java @@ -4,15 +4,15 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.KinesisEvent; import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.demo.batch.model.Product; import software.amazon.lambda.powertools.batch.BatchMessageHandlerBuilder; import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; public class KinesisBatchHandler implements RequestHandler { - private final static Logger LOGGER = LogManager.getLogger(KinesisBatchHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(KinesisBatchHandler.class); private final BatchMessageHandler handler; public KinesisBatchHandler() { diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchSender.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchSender.java index 0bc7dc42c..dadead1a2 100644 --- a/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchSender.java +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchSender.java @@ -10,8 +10,8 @@ import java.security.SecureRandom; import java.util.List; import java.util.stream.IntStream; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.demo.batch.model.Product; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; @@ -28,7 +28,7 @@ */ public class KinesisBatchSender implements RequestHandler { - private static final Logger LOGGER = LogManager.getLogger(KinesisBatchSender.class); + private static final Logger LOGGER = LoggerFactory.getLogger(KinesisBatchSender.class); private final KinesisClient kinesisClient; private final SecureRandom random; diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/AbstractSqsBatchHandler.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/AbstractSqsBatchHandler.java new file mode 100644 index 000000000..25dba47bb --- /dev/null +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/AbstractSqsBatchHandler.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package org.demo.batch.sqs; + +import com.amazonaws.services.lambda.runtime.Context; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Random; +import org.demo.batch.model.Product; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.tracing.Tracing; +import software.amazon.lambda.powertools.tracing.TracingUtils; + +public class AbstractSqsBatchHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSqsBatchHandler.class); + private final ObjectMapper mapper = new ObjectMapper(); + private final String bucket = System.getenv("BUCKET"); + private final S3Client s3 = S3Client.builder().httpClient(UrlConnectionHttpClient.create()).build(); + private final Random r = new Random(); + + /** + * Simulate some processing (I/O + S3 put request) + * @param p deserialized product + * @param context Lambda context + */ + @Logging + @Tracing + protected void processMessage(Product p, Context context) { + TracingUtils.putAnnotation("productId", p.getId()); + TracingUtils.putAnnotation("Thread", Thread.currentThread().getName()); + MDC.put("product", String.valueOf(p.getId())); + LOGGER.info("Processing product {}", p); + + char c = (char)(r.nextInt(26) + 'a'); + char[] chars = new char[1024 * 1000]; + Arrays.fill(chars, c); + p.setName(new String(chars)); + try { + File file = new File("/tmp/"+p.getId()+".json"); + mapper.writeValue(file, p); + s3.putObject( + PutObjectRequest.builder().bucket(bucket).key(p.getId()+".json").build(), RequestBody.fromFile(file)); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + MDC.remove("product"); + } + } +} diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchHandler.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchHandler.java index bb9d704d3..bc0f57cb8 100644 --- a/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchHandler.java +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchHandler.java @@ -4,14 +4,16 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.demo.batch.model.Product; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.batch.BatchMessageHandlerBuilder; import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.tracing.Tracing; -public class SqsBatchHandler implements RequestHandler { - private final static Logger LOGGER = LogManager.getLogger(SqsBatchHandler.class); +public class SqsBatchHandler extends AbstractSqsBatchHandler implements RequestHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(SqsBatchHandler.class); private final BatchMessageHandler handler; public SqsBatchHandler() { @@ -20,14 +22,11 @@ public SqsBatchHandler() { .buildWithMessageHandler(this::processMessage, Product.class); } + @Logging + @Tracing @Override public SQSBatchResponse handleRequest(SQSEvent sqsEvent, Context context) { + LOGGER.info("Processing batch of {} messages", sqsEvent.getRecords().size()); return handler.processBatch(sqsEvent, context); } - - - private void processMessage(Product p, Context c) { - LOGGER.info("Processing product " + p); - } - } diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchSender.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchSender.java index af78bed5a..58b24d735 100644 --- a/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchSender.java +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchSender.java @@ -10,14 +10,13 @@ import java.security.SecureRandom; import java.util.List; import java.util.stream.IntStream; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.demo.batch.model.Product; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest; import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchResponse; /** @@ -27,7 +26,7 @@ */ public class SqsBatchSender implements RequestHandler { - private static final Logger LOGGER = LogManager.getLogger(SqsBatchSender.class); + private static final Logger LOGGER = LoggerFactory.getLogger(SqsBatchSender.class); private final SqsClient sqsClient; private final SecureRandom random; @@ -45,16 +44,12 @@ public SqsBatchSender() { public String handleRequest(ScheduledEvent scheduledEvent, Context context) { String queueUrl = System.getenv("QUEUE_URL"); - LOGGER.info("handleRequest"); - - // Push 5 messages on each invoke. - List batchRequestEntries = IntStream.range(0, 5) + List batchRequestEntries = IntStream.range(0, 50) .mapToObj(value -> { - long id = random.nextLong(); - float price = random.nextFloat(); + long id = Math.abs(random.nextLong()); + float price = Math.abs(random.nextFloat() * 3465); Product product = new Product(id, "product-" + id, price); try { - return SendMessageBatchRequestEntry.builder() .id(scheduledEvent.getId() + value) .messageBody(objectMapper.writeValueAsString(product)) @@ -65,12 +60,12 @@ public String handleRequest(ScheduledEvent scheduledEvent, Context context) { } }).collect(toList()); - SendMessageBatchResponse sendMessageBatchResponse = sqsClient.sendMessageBatch(SendMessageBatchRequest.builder() - .queueUrl(queueUrl) - .entries(batchRequestEntries) - .build()); - - LOGGER.info("Sent Message {}", sendMessageBatchResponse); + for (int i = 0; i < 50; i += 10) { + sqsClient.sendMessageBatch(SendMessageBatchRequest.builder() + .queueUrl(queueUrl) + .entries(batchRequestEntries.subList(i, i + 10)) + .build()); + } return "Success"; } diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsParallelBatchHandler.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsParallelBatchHandler.java new file mode 100644 index 000000000..0151c0a32 --- /dev/null +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsParallelBatchHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package org.demo.batch.sqs; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import org.demo.batch.model.Product; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import software.amazon.lambda.powertools.batch.BatchMessageHandlerBuilder; +import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.tracing.Tracing; + +public class SqsParallelBatchHandler extends AbstractSqsBatchHandler implements RequestHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(SqsParallelBatchHandler.class); + private final BatchMessageHandler handler; + + public SqsParallelBatchHandler() { + handler = new BatchMessageHandlerBuilder() + .withSqsBatchHandler() + .buildWithMessageHandler(this::processMessage, Product.class); + } + + @Logging + @Tracing + @Override + public SQSBatchResponse handleRequest(SQSEvent sqsEvent, Context context) { + LOGGER.info("Processing batch of {} messages", sqsEvent.getRecords().size()); + MDC.put("requestId", context.getAwsRequestId()); // should be propagated to other threads + return handler.processBatchInParallel(sqsEvent, context); + } +} diff --git a/examples/powertools-examples-batch/src/main/resources/LogLayout.json b/examples/powertools-examples-batch/src/main/resources/LogLayout.json new file mode 100644 index 000000000..60f102e09 --- /dev/null +++ b/examples/powertools-examples-batch/src/main/resources/LogLayout.json @@ -0,0 +1,75 @@ +{ + "level": { + "$resolver": "level", + "field": "name" + }, + "message": { + "$resolver": "message" + }, + "error": { + "message": { + "$resolver": "exception", + "field": "message" + }, + "name": { + "$resolver": "exception", + "field": "className" + }, + "stack": { + "$resolver": "exception", + "field": "stackTrace", + "stackTrace": { + "stringified": true + } + } + }, + "cold_start": { + "$resolver": "powertools", + "field": "cold_start" + }, + "thread": { + "$resolver": "thread", + "field": "name" + }, + "function_arn": { + "$resolver": "powertools", + "field": "function_arn" + }, + "function_memory_size": { + "$resolver": "powertools", + "field": "function_memory_size" + }, + "function_name": { + "$resolver": "powertools", + "field": "function_name" + }, + "function_request_id": { + "$resolver": "powertools", + "field": "function_request_id" + }, + "function_version": { + "$resolver": "powertools", + "field": "function_version" + }, + "sampling_rate": { + "$resolver": "powertools", + "field": "sampling_rate" + }, + "service": { + "$resolver": "powertools", + "field": "service" + }, + "timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + }, + "xray_trace_id": { + "$resolver": "powertools", + "field": "xray_trace_id" + }, + "": { + "$resolver": "powertools" + } +} \ No newline at end of file diff --git a/examples/powertools-examples-batch/src/main/resources/log4j2.xml b/examples/powertools-examples-batch/src/main/resources/log4j2.xml index ea3ecf474..c48ca3ef4 100644 --- a/examples/powertools-examples-batch/src/main/resources/log4j2.xml +++ b/examples/powertools-examples-batch/src/main/resources/log4j2.xml @@ -2,7 +2,8 @@ - + + diff --git a/examples/powertools-examples-cloudformation/README.md b/examples/powertools-examples-cloudformation/README.md index 4b53ff0aa..f0a15f1cd 100644 --- a/examples/powertools-examples-cloudformation/README.md +++ b/examples/powertools-examples-cloudformation/README.md @@ -2,18 +2,27 @@ This project contains an example of Lambda function using the CloudFormation module of Powertools for AWS Lambda in Java. For more information on this module, please refer to the [documentation](https://awslabs.github.io/aws-lambda-powertools-java/utilities/custom_resources/). +In this example you pass in a bucket name as a parameter and upon CloudFormation events a call is made to a lambda. That lambda attempts to create the bucket on CREATE events, create a new bucket if the name changes with an UPDATE event and delete the bucket upon DELETE events. + ## Deploy the sample application This sample can be used either with the Serverless Application Model (SAM) or with CDK. ### Deploy with SAM CLI To deploy it using the SAM CLI, check out the instructions for getting started in [the examples directory](../README.md) +Run the following in your shell: + +```bash +cd infra/sam +sam build +sam deploy --guided --parameter-overrides BucketNameParam=my-unique-bucket-20230718 +``` ### Deploy with CDK To use CDK you need the following tools. * CDK - [Install CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) -* Java 8 - [Install Java 8](https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/downloads-list.html) +* Java 11 - [Install Java 11](https://docs.aws.amazon.com/corretto/latest/corretto-11-ug/downloads-list.html) * Maven - [Install Maven](https://maven.apache.org/install.html) * Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index 5e3403f4e..edadd6005 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -3,19 +3,20 @@ 4.0.0 software.amazon.lambda.examples - 1.19.0-SNAPSHOT + 2.0.0-RC1 powertools-examples-cloudformation jar - AWS Lambda Powertools for Java library Examples - CloudFormation + Powertools for AWS Lambda (Java) - Examples - CloudFormation - 2.22.1 - 1.8 - 1.8 + 11 + 11 1.2.3 - 3.11.4 - 2.25.6 + 3.15.0 + 2.31.40 + 1.9.20.1 + @@ -47,18 +48,13 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.apache.logging.log4j - log4j-api - ${log4j.version} + org.aspectj + aspectjrt + ${aspectj.version} software.amazon.awssdk @@ -84,14 +80,6 @@ - - org.apache.logging.log4j - log4j-jcl - ${log4j.version} - - - - @@ -99,7 +87,7 @@ dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 ${maven.compiler.source} ${maven.compiler.target} @@ -118,11 +106,18 @@ + + + org.aspectj + aspectjtools + ${aspectj.version} + + org.apache.maven.plugins maven-shade-plugin - 3.5.2 + 3.6.0 package @@ -149,77 +144,11 @@ org.apache.maven.plugins maven-deploy-plugin + 3.1.2 true - - - - jdk8 - - (,11) - - - 1.9.7 - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - dev.aspectj - aspectj-maven-plugin - ${aspectj.plugin.version} - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-logging - - - - - - - compile - test-compile - - - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - diff --git a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java index 54f13244c..c35ad8fb9 100644 --- a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java @@ -3,8 +3,8 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; import java.util.Objects; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.waiters.WaiterResponse; @@ -24,7 +24,7 @@ */ public class App extends AbstractCustomResourceHandler { - private final static Logger log = LogManager.getLogger(App.class); + private static final Logger log = LoggerFactory.getLogger(App.class); private final S3Client s3Client; public App() { @@ -47,7 +47,7 @@ protected Response create(CloudFormationCustomResourceEvent cloudFormationCustom Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"), "BucketName cannot be null."); - log.info(cloudFormationCustomResourceEvent); + log.info(cloudFormationCustomResourceEvent.toString()); String bucketName = (String) cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"); log.info("Bucket Name {}", bucketName); try { @@ -57,7 +57,7 @@ protected Response create(CloudFormationCustomResourceEvent cloudFormationCustom return Response.success(bucketName); } catch (AwsServiceException | SdkClientException e) { // In case of error, return a failed response, with the bucketName as the physicalResourceId - log.error(e); + log.error("Unable to create bucket", e); return Response.failed(bucketName); } } @@ -77,7 +77,7 @@ protected Response update(CloudFormationCustomResourceEvent cloudFormationCustom Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"), "BucketName cannot be null."); - log.info(cloudFormationCustomResourceEvent); + log.info(cloudFormationCustomResourceEvent.toString()); // Get the physicalResourceId. physicalResourceId is the value returned to CloudFormation in the Create request, and passed in on subsequent requests (e.g. UPDATE or DELETE) String physicalResourceId = cloudFormationCustomResourceEvent.getPhysicalResourceId(); log.info("Physical Resource ID {}", physicalResourceId); @@ -94,7 +94,7 @@ protected Response update(CloudFormationCustomResourceEvent cloudFormationCustom // Return a successful response with the newBucketName return Response.success(newBucketName); } catch (AwsServiceException | SdkClientException e) { - log.error(e); + log.error("Unable to create bucket", e); return Response.failed(newBucketName); } } else { @@ -120,7 +120,7 @@ protected Response delete(CloudFormationCustomResourceEvent cloudFormationCustom Objects.requireNonNull(cloudFormationCustomResourceEvent.getPhysicalResourceId(), "PhysicalResourceId cannot be null."); - log.info(cloudFormationCustomResourceEvent); + log.info(cloudFormationCustomResourceEvent.toString()); // Get the physicalResourceId. physicalResourceId is the value provided to CloudFormation in the Create request. String bucketName = cloudFormationCustomResourceEvent.getPhysicalResourceId(); log.info("Bucket Name {}", bucketName); @@ -135,7 +135,7 @@ protected Response delete(CloudFormationCustomResourceEvent cloudFormationCustom return Response.success(bucketName); } catch (AwsServiceException | SdkClientException e) { // Return a failed response in case of errors during the bucket deletion - log.error(e); + log.error("Unable to delete bucket", e); return Response.failed(bucketName); } } else { @@ -166,7 +166,7 @@ private void createBucket(String bucketName) { s3Client.createBucket(createBucketRequest); WaiterResponse waiterResponse = waiter.waitUntilBucketExists(HeadBucketRequest.builder().bucket(bucketName).build()); - waiterResponse.matched().response().ifPresent(log::info); + waiterResponse.matched().response().ifPresent(res -> log.info(res.toString())); log.info("Bucket Created {}", bucketName); } } \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/.gitignore b/examples/powertools-examples-core-utilities/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/examples/powertools-examples-core/README.md b/examples/powertools-examples-core-utilities/README.md similarity index 100% rename from examples/powertools-examples-core/README.md rename to examples/powertools-examples-core-utilities/README.md diff --git a/examples/powertools-examples-core/cdk/README.md b/examples/powertools-examples-core-utilities/cdk/README.md similarity index 100% rename from examples/powertools-examples-core/cdk/README.md rename to examples/powertools-examples-core-utilities/cdk/README.md diff --git a/examples/powertools-examples-core/cdk/app/events/event.json b/examples/powertools-examples-core-utilities/cdk/app/events/event.json similarity index 100% rename from examples/powertools-examples-core/cdk/app/events/event.json rename to examples/powertools-examples-core-utilities/cdk/app/events/event.json diff --git a/examples/powertools-examples-core/sam/pom.xml b/examples/powertools-examples-core-utilities/cdk/app/pom.xml similarity index 56% rename from examples/powertools-examples-core/sam/pom.xml rename to examples/powertools-examples-core-utilities/cdk/app/pom.xml index 6ef2c0ecb..11d5497da 100644 --- a/examples/powertools-examples-core/sam/pom.xml +++ b/examples/powertools-examples-core-utilities/cdk/app/pom.xml @@ -2,17 +2,19 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 + Powertools for AWS Lambda (Java) - Examples - Core Utilities (logging, tracing, metrics) with CDK software.amazon.lambda.examples - 1.19.0-SNAPSHOT - powertools-examples-core-sam + + 2.0.0-RC1 + powertools-examples-core-utilities-cdk jar - Powertools for AWS Lambda (Java) library Examples - Core - - 2.22.1 - 1.8 - 1.8 + 2.24.3 + 11 + 11 + 1.9.20.1 @@ -39,7 +41,7 @@ com.amazonaws aws-lambda-java-events - 3.11.4 + 3.15.0 org.apache.logging.log4j @@ -51,14 +53,20 @@ log4j-api ${log4j.version} + + org.aspectj + aspectjrt + ${aspectj.version} + + helloworld-lambda dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 ${maven.compiler.source} ${maven.compiler.target} @@ -85,11 +93,18 @@ + + + org.aspectj + aspectjtools + ${aspectj.version} + + org.apache.maven.plugins maven-shade-plugin - 3.5.2 + 3.6.0 package @@ -113,89 +128,15 @@ - + org.apache.maven.plugins maven-deploy-plugin + 3.1.2 true - - - - jdk8 - - (,11) - - - 1.9.7 - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - dev.aspectj - aspectj-maven-plugin - ${aspectj.plugin.version} - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-metrics - - - - - - - compile - test-compile - - - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - diff --git a/examples/powertools-examples-core/cdk/app/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/App.java similarity index 77% rename from examples/powertools-examples-core/cdk/app/src/main/java/helloworld/App.java rename to examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/App.java index 988da2a73..bb21f84d3 100644 --- a/examples/powertools-examples-core/cdk/app/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/App.java @@ -14,8 +14,6 @@ package helloworld; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; import com.amazonaws.services.lambda.runtime.Context; @@ -29,13 +27,15 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.tracing.TracingUtils; @@ -44,26 +44,26 @@ * Handler for requests to Lambda function. */ public class App implements RequestHandler { - private final static Logger log = LogManager.getLogger(App.class); + private static final Logger log = LoggerFactory.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) 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"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> - { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); - }); + DimensionSet dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); - LoggingUtils.appendKey("test", "willBeLogged"); + MDC.put("test", "willBeLogged"); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); @@ -73,8 +73,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv TracingUtils.putAnnotation("Test", "New"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - TracingUtils.withSubsegment("loggingResponse", subsegment -> - { + TracingUtils.withSubsegment("loggingResponse", subsegment -> { String sampled = "log something out"; log.info(sampled); log.info(output); diff --git a/examples/powertools-examples-core/sam/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/AppStream.java similarity index 94% rename from examples/powertools-examples-core/sam/src/main/java/helloworld/AppStream.java rename to examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/AppStream.java index 94806cc38..8bc57b201 100644 --- a/examples/powertools-examples-core/sam/src/main/java/helloworld/AppStream.java +++ b/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/AppStream.java @@ -26,7 +26,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import java.io.InputStreamReader; import java.io.BufferedReader; @@ -40,7 +40,7 @@ public class AppStream implements RequestStreamHandler { @Override @Logging(logEvent = true) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) // RequestStreamHandler can be used instead of RequestHandler for cases when you'd like to deserialize request body or serialize response body yourself, instead of allowing that to happen automatically // Note that you still need to return a proper JSON for API Gateway to handle // See Lambda Response format for examples: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html diff --git a/examples/powertools-examples-core/cdk/app/src/main/resources/log4j2.xml b/examples/powertools-examples-core-utilities/cdk/app/src/main/resources/log4j2.xml similarity index 100% rename from examples/powertools-examples-core/cdk/app/src/main/resources/log4j2.xml rename to examples/powertools-examples-core-utilities/cdk/app/src/main/resources/log4j2.xml diff --git a/examples/powertools-examples-core/cdk/infra/cdk.json b/examples/powertools-examples-core-utilities/cdk/infra/cdk.json similarity index 100% rename from examples/powertools-examples-core/cdk/infra/cdk.json rename to examples/powertools-examples-core-utilities/cdk/infra/cdk.json diff --git a/examples/powertools-examples-core/cdk/infra/pom.xml b/examples/powertools-examples-core-utilities/cdk/infra/pom.xml similarity index 86% rename from examples/powertools-examples-core/cdk/infra/pom.xml rename to examples/powertools-examples-core-utilities/cdk/infra/pom.xml index 05e643c4d..0b67f20ae 100644 --- a/examples/powertools-examples-core/cdk/infra/pom.xml +++ b/examples/powertools-examples-core-utilities/cdk/infra/pom.xml @@ -4,28 +4,28 @@ 4.0.0 software.amazon.lambda.examples cdk - 1.19.0-SNAPSHOT + 2.0.0-RC1 UTF-8 - 2.130.0 + 2.162.1 [10.0.0,11.0.0) - 5.10.0 + 5.11.1 org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 - 1.8 - 1.8 + 11 + 11 org.codehaus.mojo exec-maven-plugin - 3.2.0 + 3.3.0 cdk.CdkApp @@ -34,6 +34,7 @@ org.apache.maven.plugins maven-deploy-plugin + 3.1.2 true diff --git a/examples/powertools-examples-core/cdk/infra/src/main/java/cdk/CdkApp.java b/examples/powertools-examples-core-utilities/cdk/infra/src/main/java/cdk/CdkApp.java similarity index 100% rename from examples/powertools-examples-core/cdk/infra/src/main/java/cdk/CdkApp.java rename to examples/powertools-examples-core-utilities/cdk/infra/src/main/java/cdk/CdkApp.java diff --git a/examples/powertools-examples-core/cdk/infra/src/main/java/cdk/CdkStack.java b/examples/powertools-examples-core-utilities/cdk/infra/src/main/java/cdk/CdkStack.java similarity index 100% rename from examples/powertools-examples-core/cdk/infra/src/main/java/cdk/CdkStack.java rename to examples/powertools-examples-core-utilities/cdk/infra/src/main/java/cdk/CdkStack.java diff --git a/examples/powertools-examples-core-utilities/cdk/infra/src/test/java/cdk/CdkStackTest.java b/examples/powertools-examples-core-utilities/cdk/infra/src/test/java/cdk/CdkStackTest.java new file mode 100644 index 000000000..e69de29bb diff --git a/examples/powertools-examples-core/gradle/README.md b/examples/powertools-examples-core-utilities/gradle/README.md similarity index 94% rename from examples/powertools-examples-core/gradle/README.md rename to examples/powertools-examples-core-utilities/gradle/README.md index c7c798d5d..adc7fe635 100644 --- a/examples/powertools-examples-core/gradle/README.md +++ b/examples/powertools-examples-core-utilities/gradle/README.md @@ -1,7 +1,7 @@ # Powertools for AWS Lambda (Java) - Core Utilities Example with Gradle This project demonstrates the Lambda for Powertools Java module deployed using [Serverless Application Model](https://aws.amazon.com/serverless/sam/) with -[Gradle](https://gradle.org/) running the build. This example is configured for Java 1.8 only; in order to use a newer version, check out the Gradle +[Gradle](https://gradle.org/) running the build. This example is configured for Java 11 only; in order to use a newer version, check out the Gradle configuration guide [in the main project README](../../../README.md). You can also use `sam init` to create a new Gradle-powered Powertools application - choose to use the **AWS Quick Start Templates**, diff --git a/examples/powertools-examples-core/gradle/build.gradle b/examples/powertools-examples-core-utilities/gradle/build.gradle similarity index 56% rename from examples/powertools-examples-core/gradle/build.gradle rename to examples/powertools-examples-core-utilities/gradle/build.gradle index 520b689cb..bf53c31eb 100644 --- a/examples/powertools-examples-core/gradle/build.gradle +++ b/examples/powertools-examples-core-utilities/gradle/build.gradle @@ -1,16 +1,16 @@ plugins { id 'java' - id "io.freefair.aspectj.post-compile-weaving" version "6.6.3" + id "io.freefair.aspectj.post-compile-weaving" version "8.2.2" } wrapper { - gradleVersion = "7.6.1" + gradleVersion = "8.5" } compileJava { - sourceCompatibility = "1.8" - targetCompatibility = "1.8" + sourceCompatibility = "11" + targetCompatibility = "11" ajc { enabled = true @@ -28,8 +28,9 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' implementation 'com.amazonaws:aws-lambda-java-events:3.11.0' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.2' - aspect 'software.amazon.lambda:powertools-tracing:1.19.0-SNAPSHOT' - aspect 'software.amazon.lambda:powertools-logging:1.19.0-SNAPSHOT' - aspect 'software.amazon.lambda:powertools-metrics:1.19.0-SNAPSHOT' + implementation 'org.aspectj:aspectjrt:1.9.20.1' + aspect 'software.amazon.lambda:powertools-tracing:2.0.0-RC1' + aspect 'software.amazon.lambda:powertools-logging-log4j:2.0.0-RC1' + aspect 'software.amazon.lambda:powertools-metrics:2.0.0-RC1' } diff --git a/examples/powertools-examples-core/gradle/events/event.json b/examples/powertools-examples-core-utilities/gradle/events/event.json similarity index 100% rename from examples/powertools-examples-core/gradle/events/event.json rename to examples/powertools-examples-core-utilities/gradle/events/event.json diff --git a/examples/powertools-examples-core/gradle/gradle/wrapper/.gitignore b/examples/powertools-examples-core-utilities/gradle/gradle/wrapper/.gitignore similarity index 100% rename from examples/powertools-examples-core/gradle/gradle/wrapper/.gitignore rename to examples/powertools-examples-core-utilities/gradle/gradle/wrapper/.gitignore diff --git a/examples/powertools-examples-core/gradle/gradle/wrapper/gradle-wrapper.jar b/examples/powertools-examples-core-utilities/gradle/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from examples/powertools-examples-core/gradle/gradle/wrapper/gradle-wrapper.jar rename to examples/powertools-examples-core-utilities/gradle/gradle/wrapper/gradle-wrapper.jar diff --git a/examples/powertools-examples-core/gradle/gradle/wrapper/gradle-wrapper.properties b/examples/powertools-examples-core-utilities/gradle/gradle/wrapper/gradle-wrapper.properties similarity index 93% rename from examples/powertools-examples-core/gradle/gradle/wrapper/gradle-wrapper.properties rename to examples/powertools-examples-core-utilities/gradle/gradle/wrapper/gradle-wrapper.properties index 0d23ac00c..1af9e0930 100644 --- a/examples/powertools-examples-core/gradle/gradle/wrapper/gradle-wrapper.properties +++ b/examples/powertools-examples-core-utilities/gradle/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/examples/powertools-examples-core/gradle/gradlew b/examples/powertools-examples-core-utilities/gradle/gradlew similarity index 100% rename from examples/powertools-examples-core/gradle/gradlew rename to examples/powertools-examples-core-utilities/gradle/gradlew diff --git a/examples/powertools-examples-core/gradle/gradlew.bat b/examples/powertools-examples-core-utilities/gradle/gradlew.bat similarity index 100% rename from examples/powertools-examples-core/gradle/gradlew.bat rename to examples/powertools-examples-core-utilities/gradle/gradlew.bat diff --git a/examples/powertools-examples-core/serverless/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java similarity index 71% rename from examples/powertools-examples-core/serverless/src/main/java/helloworld/App.java rename to examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java index dacd7f1d4..39ed6f8d8 100644 --- a/examples/powertools-examples-core/serverless/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java @@ -14,8 +14,7 @@ package helloworld; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; +import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; import com.amazonaws.services.lambda.runtime.Context; @@ -29,13 +28,15 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.tracing.TracingUtils; @@ -44,35 +45,33 @@ * Handler for requests to Lambda function. */ public class App implements RequestHandler { - private final static Logger log = LogManager.getLogger(App.class); + private static final Logger log = LoggerFactory.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); - // This is controlled by POWERTOOLS_LOGGER_SAMPLE_RATE environment variable - // @Logging(logEvent = true, samplingRate = 0.7) - // This is controlled by POWERTOOLS_METRICS_NAMESPACE environment variable - // @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) - // This is controlled by POWERTOOLS_TRACER_CAPTURE_ERROR environment variable + @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) 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"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> - { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); - }); + DimensionSet dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1" + ); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); - LoggingUtils.appendKey("test", "willBeLogged"); + MDC.put("test", "willBeLogged"); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); try { final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); - log.info(pageContents); + log.info("", entry("ip", pageContents)); TracingUtils.putAnnotation("Test", "New"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); @@ -94,6 +93,11 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv } } + @Tracing + private void log() { + log.info("inside threaded logging for function"); + } + @Tracing(namespace = "getPageContents", captureMode = CaptureMode.DISABLED) private String getPageContents(String address) throws IOException { URL url = new URL(address); diff --git a/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/AppStream.java new file mode 100644 index 000000000..8bc57b201 --- /dev/null +++ b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/AppStream.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.metrics.FlushMetrics; + +import java.io.InputStreamReader; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +public class AppStream implements RequestStreamHandler { + private static final ObjectMapper mapper = new ObjectMapper(); + private final static Logger log = LogManager.getLogger(AppStream.class); + + @Override + @Logging(logEvent = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + // RequestStreamHandler can be used instead of RequestHandler for cases when you'd like to deserialize request body or serialize response body yourself, instead of allowing that to happen automatically + // Note that you still need to return a proper JSON for API Gateway to handle + // See Lambda Response format for examples: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + public void handleRequest(InputStream input, OutputStream output, Context context) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); + PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8)))) { + + log.info("Received: " + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(mapper.readTree(reader))); + + writer.write("{\"body\": \"" + System.currentTimeMillis() + "\"} "); + } catch (IOException e) { + log.error("Something has gone wrong: ", e); + } + } +} + diff --git a/examples/powertools-examples-core/kotlin/template.yaml b/examples/powertools-examples-core-utilities/gradle/template.yaml similarity index 100% rename from examples/powertools-examples-core/kotlin/template.yaml rename to examples/powertools-examples-core-utilities/gradle/template.yaml diff --git a/examples/powertools-examples-core/kotlin/README.md b/examples/powertools-examples-core-utilities/kotlin/README.md similarity index 94% rename from examples/powertools-examples-core/kotlin/README.md rename to examples/powertools-examples-core-utilities/kotlin/README.md index da9ec5b52..422e12742 100644 --- a/examples/powertools-examples-core/kotlin/README.md +++ b/examples/powertools-examples-core-utilities/kotlin/README.md @@ -1,7 +1,7 @@ # Powertools for AWS Lambda (Kotlin) - Core Utilities Example This project demonstrates the Lambda for Powertools Kotlin module deployed using [Serverless Application Model](https://aws.amazon.com/serverless/sam/) with -[Gradle](https://gradle.org/) running the build. This example is configured for Java 1.8 only; in order to use a newer version, check out the Gradle +[Gradle](https://gradle.org/) running the build. This example is configured for Java 11 only; in order to use a newer version, check out the Gradle configuration guide [in the main project README](../../../README.md). You can also use `sam init` to create a new Gradle-powered Powertools application - choose to use the **AWS Quick Start Templates**, diff --git a/examples/powertools-examples-core-utilities/kotlin/build.gradle.kts b/examples/powertools-examples-core-utilities/kotlin/build.gradle.kts new file mode 100644 index 000000000..9dcac3510 --- /dev/null +++ b/examples/powertools-examples-core-utilities/kotlin/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("io.freefair.aspectj.post-compile-weaving") version "8.2.2" + kotlin("jvm") version "1.9.10" +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation("com.amazonaws:aws-lambda-java-core:1.2.3") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.15.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + implementation("com.amazonaws:aws-lambda-java-events:3.11.3") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2") + implementation("org.aspectj:aspectjrt:1.9.20.1") + aspect("software.amazon.lambda:powertools-tracing:2.0.0-RC1") + aspect("software.amazon.lambda:powertools-logging-log4j:2.0.0-RC1") + aspect("software.amazon.lambda:powertools-metrics:2.0.0-RC1") + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.24") +} + +kotlin { + jvmToolchain(11) +} \ No newline at end of file diff --git a/examples/powertools-examples-core/kotlin/events/event.json b/examples/powertools-examples-core-utilities/kotlin/events/event.json similarity index 100% rename from examples/powertools-examples-core/kotlin/events/event.json rename to examples/powertools-examples-core-utilities/kotlin/events/event.json diff --git a/examples/powertools-examples-core/kotlin/gradle/wrapper/.gitignore b/examples/powertools-examples-core-utilities/kotlin/gradle/wrapper/.gitignore similarity index 100% rename from examples/powertools-examples-core/kotlin/gradle/wrapper/.gitignore rename to examples/powertools-examples-core-utilities/kotlin/gradle/wrapper/.gitignore diff --git a/examples/powertools-examples-core/kotlin/gradle/wrapper/gradle-wrapper.jar b/examples/powertools-examples-core-utilities/kotlin/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from examples/powertools-examples-core/kotlin/gradle/wrapper/gradle-wrapper.jar rename to examples/powertools-examples-core-utilities/kotlin/gradle/wrapper/gradle-wrapper.jar diff --git a/examples/powertools-examples-core/kotlin/gradle/wrapper/gradle-wrapper.properties b/examples/powertools-examples-core-utilities/kotlin/gradle/wrapper/gradle-wrapper.properties similarity index 93% rename from examples/powertools-examples-core/kotlin/gradle/wrapper/gradle-wrapper.properties rename to examples/powertools-examples-core-utilities/kotlin/gradle/wrapper/gradle-wrapper.properties index 0d23ac00c..1af9e0930 100644 --- a/examples/powertools-examples-core/kotlin/gradle/wrapper/gradle-wrapper.properties +++ b/examples/powertools-examples-core-utilities/kotlin/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/examples/powertools-examples-core/kotlin/gradlew b/examples/powertools-examples-core-utilities/kotlin/gradlew similarity index 100% rename from examples/powertools-examples-core/kotlin/gradlew rename to examples/powertools-examples-core-utilities/kotlin/gradlew diff --git a/examples/powertools-examples-core/kotlin/gradlew.bat b/examples/powertools-examples-core-utilities/kotlin/gradlew.bat similarity index 96% rename from examples/powertools-examples-core/kotlin/gradlew.bat rename to examples/powertools-examples-core-utilities/kotlin/gradlew.bat index 6689b85be..93e3f59f1 100644 --- a/examples/powertools-examples-core/kotlin/gradlew.bat +++ b/examples/powertools-examples-core-utilities/kotlin/gradlew.bat @@ -1,92 +1,92 @@ -@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 - -@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. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -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 +@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 + +@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. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +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/examples/powertools-examples-core/kotlin/src/main/kotlin/helloworld/App.kt b/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt similarity index 75% rename from examples/powertools-examples-core/kotlin/src/main/kotlin/helloworld/App.kt rename to examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt index ed4cf267a..b05e0d055 100644 --- a/examples/powertools-examples-core/kotlin/src/main/kotlin/helloworld/App.kt +++ b/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt @@ -18,44 +18,49 @@ import com.amazonaws.services.lambda.runtime.RequestHandler import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent import com.amazonaws.xray.entities.Subsegment -import org.apache.logging.log4j.LogManager -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger -import software.amazon.cloudwatchlogs.emf.model.DimensionSet -import software.amazon.cloudwatchlogs.emf.model.Unit +import org.slf4j.LoggerFactory +import org.slf4j.MDC import software.amazon.lambda.powertools.logging.Logging -import software.amazon.lambda.powertools.logging.LoggingUtils +import software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry import software.amazon.lambda.powertools.metrics.Metrics -import software.amazon.lambda.powertools.metrics.MetricsUtils +import software.amazon.lambda.powertools.metrics.Metrics +import software.amazon.lambda.powertools.metrics.MetricsFactory +import software.amazon.lambda.powertools.metrics.model.DimensionSet +import software.amazon.lambda.powertools.metrics.model.MetricUnit import software.amazon.lambda.powertools.tracing.CaptureMode import software.amazon.lambda.powertools.tracing.Tracing import software.amazon.lambda.powertools.tracing.TracingUtils -import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader import java.net.URL -import java.util.stream.Collectors /** * Handler for requests to Lambda function. */ class App : RequestHandler { + private val log = LoggerFactory.getLogger(this::class.java) + private val metrics: Metrics = MetricsFactory.getMetricsInstance() + @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) - + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) override fun handleRequest(input: APIGatewayProxyRequestEvent?, context: Context?): APIGatewayProxyResponseEvent { val headers = mapOf("Content-Type" to "application/json", "X-Custom-Header" to "application/json") - MetricsUtils.metricsLogger().putMetric("CustomMetric1", 1.0, Unit.COUNT) - MetricsUtils.withSingleMetric("CustomMetrics2", 1.0, Unit.COUNT, "Another") { metric: MetricsLogger -> - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")) - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")) - } - LoggingUtils.appendKey("test", "willBeLogged") + + metrics.addMetric("CustomMetric1", 1.0, MetricUnit.COUNT) + + val dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1" + ) + metrics.flushSingleMetric("CustomMetric2", 1.0, MetricUnit.COUNT, "Another", dimensionSet) + + MDC.put("test", "willBeLogged") val response = APIGatewayProxyResponseEvent().withHeaders(headers) return try { val pageContents = getPageContents("https://checkip.amazonaws.com") - log.info(pageContents) + log.info("", entry("ip", pageContents)); TracingUtils.putAnnotation("Test", "New") val output = """ { @@ -91,6 +96,4 @@ class App : RequestHandler = mapper.readValue(input, MutableMap::class.java) diff --git a/examples/powertools-examples-core/gradle/template.yaml b/examples/powertools-examples-core-utilities/kotlin/template.yaml similarity index 98% rename from examples/powertools-examples-core/gradle/template.yaml rename to examples/powertools-examples-core-utilities/kotlin/template.yaml index a717c2998..1a1572fca 100644 --- a/examples/powertools-examples-core/gradle/template.yaml +++ b/examples/powertools-examples-core-utilities/kotlin/template.yaml @@ -26,7 +26,7 @@ Resources: Properties: CodeUri: . Handler: helloworld.App::handleRequest - Runtime: java8 + Runtime: java11 MemorySize: 512 Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object Variables: @@ -43,7 +43,7 @@ Resources: Properties: CodeUri: . Handler: helloworld.AppStream::handleRequest - Runtime: java8 + Runtime: java11 MemorySize: 512 Tracing: Active Environment: diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/Dockerfile b/examples/powertools-examples-core-utilities/sam-graalvm/Dockerfile new file mode 100644 index 000000000..a690606ad --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/Dockerfile @@ -0,0 +1,14 @@ +#Use the official AWS SAM base image for Java 21 +FROM public.ecr.aws/sam/build-java21:latest + +#Install GraalVM dependencies +RUN curl -4 -L curl https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-x64_bin.tar.gz | tar -xvz +RUN mv graalvm-jdk-21.* /usr/lib/graalvm + +#Make native image and mvn available on CLI +RUN ln -s /usr/lib/graalvm/bin/native-image /usr/bin/native-image +RUN ln -s /usr/lib/maven/bin/mvn /usr/bin/mvn + +#Set GraalVM as default +ENV JAVA_HOME=/usr/lib/graalvm +ENV PATH=/usr/lib/graalvm/bin:$PATH diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/Makefile b/examples/powertools-examples-core-utilities/sam-graalvm/Makefile new file mode 100644 index 000000000..f8abe6870 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/Makefile @@ -0,0 +1,6 @@ +build-HelloWorldFunction: + mvn clean package -P native-image + chmod +x target/hello-world + cp target/hello-world $(ARTIFACTS_DIR) # (ARTIFACTS_DIR --> https://github.com/aws/aws-lambda-builders/blob/develop/aws_lambda_builders/workflows/custom_make/DESIGN.md#implementation) + chmod +x src/main/config/bootstrap + cp src/main/config/bootstrap $(ARTIFACTS_DIR) diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/README.md b/examples/powertools-examples-core-utilities/sam-graalvm/README.md new file mode 100644 index 000000000..5572684ad --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/README.md @@ -0,0 +1,64 @@ +# Powertools for AWS Lambda (Java) - Core Utilities Example with SAM on GraalVM + +This project demonstrates the Lambda for Powertools Java module deployed using [Serverless Application Model](https://aws.amazon.com/serverless/sam/) running as a GraalVM native image. + +For general information on the deployed example itself, you can refer to the parent [README](../README.md) + +## Configuration + +- SAM uses [template.yaml](template.yaml) to define the application's AWS resources. + This file defines the Lambda function to be deployed as well as API Gateway for it. + +- Set the environment to use GraalVM + +```shell +export JAVA_HOME= +``` + +## Build the sample application + +- Build the Docker image that will be used as the environment for SAM build: + +```shell +docker build --platform linux/amd64 . -t powertools-examples-core-sam-graalvm +``` + +- Build the SAM project using the docker image + +```shell +sam build --use-container --build-image powertools-examples-core-sam-graalvm +``` + +#### [Optional] Building with -SNAPSHOT versions of PowerTools + +- If you are testing the example with a -SNAPSHOT version of PowerTools, the maven build inside the docker image will fail. This is because the -SNAPSHOT version of the PowerTools library that you are working on is still not available in maven central/snapshot repository. + To get around this, follow these steps: + - Create the native image using the `docker` command below on your development machine. The native image is created in the `target` directory. + - `` docker run --platform linux/amd64 -it -v `pwd`:`pwd` -w `pwd` -v ~/.m2:/root/.m2 powertools-examples-core-sam-graalvm mvn clean -Pnative-image package -DskipTests `` + - Edit the [`Makefile`](Makefile) remove this line + - `mvn clean package -P native-image` + - Build the SAM project using the docker image + - `sam build --use-container --build-image powertools-examples-core-sam-graalvm` + +## Deploy the sample application + +- SAM deploy + +```shell +sam deploy +``` + +To deploy the example, check out the instructions for getting +started with SAM in [the examples directory](../../README.md) + +## Additional notes + +You can watch the trace information or log information using the SAM CLI: + +```bash +# Tail the logs +sam logs --tail $MY_STACK + +# Tail the traces +sam traces --tail +``` diff --git a/examples/powertools-examples-core/sam/events/event.json b/examples/powertools-examples-core-utilities/sam-graalvm/events/event.json similarity index 100% rename from examples/powertools-examples-core/sam/events/event.json rename to examples/powertools-examples-core-utilities/sam-graalvm/events/event.json diff --git a/examples/powertools-examples-core/cdk/app/pom.xml b/examples/powertools-examples-core-utilities/sam-graalvm/pom.xml similarity index 56% rename from examples/powertools-examples-core/cdk/app/pom.xml rename to examples/powertools-examples-core-utilities/sam-graalvm/pom.xml index 4a8383925..011a29c60 100644 --- a/examples/powertools-examples-core/cdk/app/pom.xml +++ b/examples/powertools-examples-core-utilities/sam-graalvm/pom.xml @@ -2,17 +2,17 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 + Powertools for AWS Lambda (Java) - Examples - Core Utilities (logging, tracing, metrics) with SAM GraalVM software.amazon.lambda.examples - 1.17.0 - powertools-examples-core-cdk + 2.0.0-RC1 + powertools-examples-core-utilities-sam-graalvm jar - Powertools for AWS Lambda (Java) library Examples - Core - - 2.22.1 - 1.8 - 1.8 + 2.24.0 + 11 + 11 + 1.9.20.1 @@ -23,7 +23,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} @@ -39,7 +39,22 @@ com.amazonaws aws-lambda-java-events - 3.11.4 + 3.11.3 + + + org.aspectj + aspectjrt + ${aspectj.version} + + + com.amazonaws + aws-lambda-java-runtime-interface-client + 2.1.1 + + + org.apache.logging.log4j + log4j-api + ${log4j.version} org.apache.logging.log4j @@ -48,18 +63,22 @@ org.apache.logging.log4j - log4j-api + log4j-slf4j2-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-layout-template-json ${log4j.version} - helloworld-lambda - dev.aspectj + org.codehaus.mojo aspectj-maven-plugin - 1.13.1 + 1.15.0 ${maven.compiler.source} ${maven.compiler.target} @@ -86,11 +105,18 @@ + + + org.aspectj + aspectjtools + ${aspectj.version} + + org.apache.maven.plugins maven-shade-plugin - 3.5.2 + 3.6.0 package @@ -117,6 +143,7 @@ org.apache.maven.plugins maven-deploy-plugin + 3.1.2 true @@ -124,77 +151,35 @@ - - jdk8 - - (,11) - - - 1.9.7 - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - + native-image - - - - dev.aspectj - aspectj-maven-plugin - ${aspectj.plugin.version} - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-metrics - - - - - - - compile - test-compile - - - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - + + + org.graalvm.buildtools + native-maven-plugin + 0.10.1 + true + + + build-native + + build + + package + + + + hello-world + com.amazonaws.services.lambda.runtime.api.client.AWSLambda + + + --enable-url-protocols=http + --add-opens java.base/java.util=ALL-UNNAMED + + + + diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/config/bootstrap b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/config/bootstrap new file mode 100644 index 000000000..8e7928cd3 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/config/bootstrap @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +./hello-world $_HANDLER \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/java/helloworld/App.java new file mode 100644 index 000000000..68664feec --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/java/helloworld/App.java @@ -0,0 +1,110 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package helloworld; + +import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; +import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; + +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 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.metrics.FlushMetrics; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.tracing.CaptureMode; +import software.amazon.lambda.powertools.tracing.Tracing; +import software.amazon.lambda.powertools.tracing.TracingUtils; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + private static final Logger log = LoggerFactory.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + + @Logging(logEvent = true, samplingRate = 0.7) + @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + 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"); + + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + + DimensionSet dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); + + metrics.addMetric("CustomMetric3", 1, MetricUnit.COUNT, MetricResolution.HIGH); + + MDC.put("test", "willBeLogged"); + + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() + .withHeaders(headers); + try { + final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); + log.info("", entry("ip", pageContents)); + TracingUtils.putAnnotation("Test", "New"); + String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); + + TracingUtils.withSubsegment("loggingResponse", subsegment -> { + String sampled = "log something out"; + log.info(sampled); + log.info(output); + }); + + log.info("After output"); + return response + .withStatusCode(200) + .withBody(output); + } catch (RuntimeException | IOException e) { + return response + .withBody("{}") + .withStatusCode(500); + } + } + + @Tracing + private void log() { + log.info("inside threaded logging for function"); + } + + @Tracing(namespace = "getPageContents", captureMode = CaptureMode.DISABLED) + private String getPageContents(String address) throws IOException { + URL url = new URL(address); + putMetadata("getPageContents", address); + try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { + return br.lines().collect(Collectors.joining(System.lineSeparator())); + } + } +} diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json new file mode 100644 index 000000000..2780aca09 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json @@ -0,0 +1,13 @@ +[ + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntime", + "methods":[{"name":"","parameterTypes":[] }], + "fields":[{"name":"logger"}], + "allPublicMethods":true + }, + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal", + "methods":[{"name":"","parameterTypes":[] }], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json new file mode 100644 index 000000000..ddda5d5f1 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json @@ -0,0 +1,35 @@ +[ + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$ProxyRequestContext", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$RequestIdentity", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json new file mode 100644 index 000000000..d30696750 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json @@ -0,0 +1,11 @@ +[ + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClientException", + "methods":[{"name":"","parameterTypes":["java.lang.String","int"] }] + }, + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.InvocationRequest", + "fields":[{"name":"id"}, {"name":"invokedFunctionArn"}, {"name":"deadlineTimeInMs"}, {"name":"xrayTraceId"}, {"name":"clientContext"}, {"name":"cognitoIdentity"}, {"name":"content"}], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties new file mode 100644 index 000000000..20f8b7801 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-build-time=jdk.xml.internal.SecuritySupport \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json new file mode 100644 index 000000000..106edef38 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json @@ -0,0 +1,34 @@ +[ + { + "name":"com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers[]" + }, + { + "name":"com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntime", + "fields":[{"name":"logger"}] + }, + { + "name":"java.lang.Void", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"java.util.Collections$UnmodifiableMap", + "fields":[{"name":"m"}] + }, + { + "name":"jdk.internal.module.IllegalAccessLogger", + "fields":[{"name":"logger"}] + }, + { + "name":"sun.misc.Unsafe", + "fields":[{"name":"theUnsafe"}] + }, + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.InvocationRequest", + "fields":[{"name":"id"}, {"name":"invokedFunctionArn"}, {"name":"deadlineTimeInMs"}, {"name":"xrayTraceId"}, {"name":"clientContext"}, {"name":"cognitoIdentity"}, {"name":"content"}], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json new file mode 100644 index 000000000..7cc78a494 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json @@ -0,0 +1,19 @@ +{ + "resources": { + "includes": [ + { + "pattern": "\\Qaarch64/aws-lambda-runtime-interface-client.glibc.so\\E" + }, + { + "pattern": "\\Qaarch64/aws-lambda-runtime-interface-client.musl.so\\E" + }, + { + "pattern": "\\Qx86_64/aws-lambda-runtime-interface-client.glibc.so\\E" + }, + { + "pattern": "\\Qx86_64/aws-lambda-runtime-interface-client.musl.so\\E" + } + ] + }, + "bundles": [] +} \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json new file mode 100644 index 000000000..9890688f9 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json @@ -0,0 +1,25 @@ +[ + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers[]" + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7HandlersImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ser.Serializers[]" + }, + { + "name": "org.joda.time.DateTime", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/native-image.properties b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/native-image.properties new file mode 100644 index 000000000..db5ebaa55 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/native-image.properties @@ -0,0 +1 @@ +Args = --enable-url-protocols=http,https \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/reflect-config.json b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/reflect-config.json new file mode 100644 index 000000000..06ea9ce2f --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/reflect-config.json @@ -0,0 +1,11 @@ +[ + { + "name": "helloworld.App", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/resource-config.json b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/resource-config.json new file mode 100644 index 000000000..be6aac3f6 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/resource-config.json @@ -0,0 +1,7 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\Qlog4j2.xml\\E" + }]}, + "bundles":[] +} diff --git a/powertools-logging/src/test/resources/log4j2.xml b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/log4j2.xml similarity index 84% rename from powertools-logging/src/test/resources/log4j2.xml rename to examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/log4j2.xml index 22a44ee8b..60975d487 100644 --- a/powertools-logging/src/test/resources/log4j2.xml +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/resources/log4j2.xml @@ -1,9 +1,9 @@ - + - + diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/template.yaml b/examples/powertools-examples-core-utilities/sam-graalvm/template.yaml new file mode 100644 index 000000000..feb55743b --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam-graalvm/template.yaml @@ -0,0 +1,52 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + CoreUtilities + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 20 + MemorySize: 512 + Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html + Environment: + Variables: + # Powertools for AWS Lambda (Java) env vars: https://docs.powertools.aws.dev/lambda/java/#environment-variables + POWERTOOLS_LOG_LEVEL: INFO + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 + POWERTOOLS_LOGGER_LOG_EVENT: true + POWERTOOLS_METRICS_NAMESPACE: Coreutilities + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: . + Handler: helloworld.App::handleRequest + Runtime: provided.al2023 + MemorySize: 512 + Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object + Variables: + POWERTOOLS_SERVICE_NAME: hello + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + Metadata: + BuildMethod: makefile + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/examples/powertools-examples-core/sam/README.md b/examples/powertools-examples-core-utilities/sam/README.md similarity index 100% rename from examples/powertools-examples-core/sam/README.md rename to examples/powertools-examples-core-utilities/sam/README.md diff --git a/examples/powertools-examples-sqs/events/event.json b/examples/powertools-examples-core-utilities/sam/events/event.json similarity index 100% rename from examples/powertools-examples-sqs/events/event.json rename to examples/powertools-examples-core-utilities/sam/events/event.json diff --git a/examples/powertools-examples-core-utilities/sam/pom.xml b/examples/powertools-examples-core-utilities/sam/pom.xml new file mode 100644 index 000000000..d43094922 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam/pom.xml @@ -0,0 +1,127 @@ + + 4.0.0 + + Powertools for AWS Lambda (Java) - Examples - Core Utilities (logging, tracing, metrics) with SAM + software.amazon.lambda.examples + 2.0.0-RC1 + powertools-examples-core-utilities-sam + jar + + + 11 + 11 + 1.9.20.1 + + + + + software.amazon.lambda + powertools-tracing + ${project.version} + + + software.amazon.lambda + powertools-logging-log4j + ${project.version} + + + software.amazon.lambda + powertools-metrics + ${project.version} + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.15.0 + + + org.aspectj + aspectjrt + ${aspectj.version} + + + + + + + dev.aspectj + aspectj-maven-plugin + 1.14 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-tracing + + + software.amazon.lambda + powertools-logging + + + software.amazon.lambda + powertools-metrics + + + + + + + compile + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + false + + + + + + + + + org.apache.logging.log4j + log4j-transform-maven-shade-plugin-extensions + 0.1.0 + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.2 + + true + + + + + diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java new file mode 100644 index 000000000..2675c96eb --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java @@ -0,0 +1,113 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package helloworld; + +import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; +import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; + +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +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 software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.metrics.FlushMetrics; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.tracing.CaptureMode; +import software.amazon.lambda.powertools.tracing.Tracing; +import software.amazon.lambda.powertools.tracing.TracingUtils; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + private static final Logger log = LoggerFactory.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + + @Logging(logEvent = true, samplingRate = 0.7) + @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + 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"); + + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension("AnotherService", "CustomService"); + dimensionSet.addDimension("AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); + + metrics.addMetric("CustomMetric3", 1, MetricUnit.COUNT, MetricResolution.HIGH); + + MDC.put("test", "willBeLogged"); + + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() + .withHeaders(headers); + try { + final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); + log.info("", entry("ip", pageContents)); + TracingUtils.putAnnotation("Test", "New"); + String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); + + TracingUtils.withSubsegment("loggingResponse", subsegment -> { + String sampled = "log something out"; + log.info(sampled); + log.info(output); + }); + + log.info("After output"); + return response + .withStatusCode(200) + .withBody(output); + } catch (RuntimeException | IOException e) { + return response + .withBody("{}") + .withStatusCode(500); + } + } + + @Tracing + private void log() { + log.info("inside threaded logging for function"); + } + + @Tracing(namespace = "getPageContents", captureMode = CaptureMode.DISABLED) + private String getPageContents(String address) throws IOException { + URL url = new URL(address); + putMetadata("getPageContents", address); + try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { + return br.lines().collect(Collectors.joining(System.lineSeparator())); + } + } +} diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java new file mode 100644 index 000000000..8bc57b201 --- /dev/null +++ b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.metrics.FlushMetrics; + +import java.io.InputStreamReader; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +public class AppStream implements RequestStreamHandler { + private static final ObjectMapper mapper = new ObjectMapper(); + private final static Logger log = LogManager.getLogger(AppStream.class); + + @Override + @Logging(logEvent = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + // RequestStreamHandler can be used instead of RequestHandler for cases when you'd like to deserialize request body or serialize response body yourself, instead of allowing that to happen automatically + // Note that you still need to return a proper JSON for API Gateway to handle + // See Lambda Response format for examples: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + public void handleRequest(InputStream input, OutputStream output, Context context) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); + PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8)))) { + + log.info("Received: " + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(mapper.readTree(reader))); + + writer.write("{\"body\": \"" + System.currentTimeMillis() + "\"} "); + } catch (IOException e) { + log.error("Something has gone wrong: ", e); + } + } +} + diff --git a/examples/powertools-examples-core/sam/src/main/resources/log4j2.xml b/examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml similarity index 100% rename from examples/powertools-examples-core/sam/src/main/resources/log4j2.xml rename to examples/powertools-examples-core-utilities/sam/src/main/resources/log4j2.xml diff --git a/examples/powertools-examples-core/sam/template.yaml b/examples/powertools-examples-core-utilities/sam/template.yaml similarity index 100% rename from examples/powertools-examples-core/sam/template.yaml rename to examples/powertools-examples-core-utilities/sam/template.yaml diff --git a/examples/powertools-examples-core/serverless/README.md b/examples/powertools-examples-core-utilities/serverless/README.md similarity index 100% rename from examples/powertools-examples-core/serverless/README.md rename to examples/powertools-examples-core-utilities/serverless/README.md diff --git a/examples/powertools-examples-core-utilities/serverless/pom.xml b/examples/powertools-examples-core-utilities/serverless/pom.xml new file mode 100644 index 000000000..fce13b792 --- /dev/null +++ b/examples/powertools-examples-core-utilities/serverless/pom.xml @@ -0,0 +1,128 @@ + + 4.0.0 + + Powertools for AWS Lambda (Java) - Examples - Core Utilities (logging, tracing, metrics) with Serverless + software.amazon.lambda.examples + 2.0.0-RC1 + powertools-examples-core-utilities-serverless + jar + + + 11 + 11 + 1.9.20.1 + + + + + software.amazon.lambda + powertools-tracing + ${project.version} + + + software.amazon.lambda + powertools-logging-log4j + ${project.version} + + + software.amazon.lambda + powertools-metrics + ${project.version} + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.15.0 + + + org.aspectj + aspectjrt + ${aspectj.version} + + + + + helloworld-lambda + + + dev.aspectj + aspectj-maven-plugin + 1.14 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-tracing + + + software.amazon.lambda + powertools-logging + + + software.amazon.lambda + powertools-metrics + + + + + + + compile + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + false + + + + + + + + + org.apache.logging.log4j + log4j-transform-maven-shade-plugin-extensions + 0.1.0 + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.2 + + true + + + + + diff --git a/examples/powertools-examples-core/serverless/serverless.yml b/examples/powertools-examples-core-utilities/serverless/serverless.yml similarity index 100% rename from examples/powertools-examples-core/serverless/serverless.yml rename to examples/powertools-examples-core-utilities/serverless/serverless.yml diff --git a/examples/powertools-examples-core/sam/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/App.java similarity index 76% rename from examples/powertools-examples-core/sam/src/main/java/helloworld/App.java rename to examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/App.java index fccc63b9a..771f5c1f1 100644 --- a/examples/powertools-examples-core/sam/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/App.java @@ -14,8 +14,7 @@ package helloworld; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; +import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; import com.amazonaws.services.lambda.runtime.Context; @@ -31,11 +30,13 @@ import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; +import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.tracing.TracingUtils; @@ -44,37 +45,36 @@ * Handler for requests to Lambda function. */ public class App implements RequestHandler { - private final static Logger log = LogManager.getLogger(App.class); + private static final Logger log = LogManager.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) 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"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> - { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); - }); + DimensionSet dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); - LoggingUtils.appendKey("test", "willBeLogged"); + MDC.put("test", "willBeLogged"); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); try { final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); - log.info(pageContents); + log.info("", entry("ip", pageContents)); TracingUtils.putAnnotation("Test", "New"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - TracingUtils.withSubsegment("loggingResponse", subsegment -> - { + TracingUtils.withSubsegment("loggingResponse", subsegment -> { String sampled = "log something out"; log.info(sampled); log.info(output); @@ -91,11 +91,6 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv } } - @Tracing - private void log() { - log.info("inside threaded logging for function"); - } - @Tracing(namespace = "getPageContents", captureMode = CaptureMode.DISABLED) private String getPageContents(String address) throws IOException { URL url = new URL(address); diff --git a/examples/powertools-examples-core/gradle/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/AppStream.java similarity index 89% rename from examples/powertools-examples-core/gradle/src/main/java/helloworld/AppStream.java rename to examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/AppStream.java index 401ef8c48..c13ab9f2e 100644 --- a/examples/powertools-examples-core/gradle/src/main/java/helloworld/AppStream.java +++ b/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/AppStream.java @@ -22,14 +22,14 @@ import java.io.OutputStream; import java.util.Map; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; public class AppStream implements RequestStreamHandler { private static final ObjectMapper mapper = new ObjectMapper(); @Override @Logging(logEvent = true) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { Map map = mapper.readValue(input, Map.class); diff --git a/examples/powertools-examples-core/serverless/src/main/resources/log4j2.xml b/examples/powertools-examples-core-utilities/serverless/src/main/resources/log4j2.xml similarity index 100% rename from examples/powertools-examples-core/serverless/src/main/resources/log4j2.xml rename to examples/powertools-examples-core-utilities/serverless/src/main/resources/log4j2.xml diff --git a/examples/powertools-examples-core/terraform/.tflint.hcl b/examples/powertools-examples-core-utilities/terraform/.tflint.hcl similarity index 100% rename from examples/powertools-examples-core/terraform/.tflint.hcl rename to examples/powertools-examples-core-utilities/terraform/.tflint.hcl diff --git a/examples/powertools-examples-core/terraform/README.md b/examples/powertools-examples-core-utilities/terraform/README.md similarity index 100% rename from examples/powertools-examples-core/terraform/README.md rename to examples/powertools-examples-core-utilities/terraform/README.md diff --git a/examples/powertools-examples-core/terraform/infra/api-gateway.tf b/examples/powertools-examples-core-utilities/terraform/infra/api-gateway.tf similarity index 100% rename from examples/powertools-examples-core/terraform/infra/api-gateway.tf rename to examples/powertools-examples-core-utilities/terraform/infra/api-gateway.tf diff --git a/examples/powertools-examples-core/terraform/infra/lambda.tf b/examples/powertools-examples-core-utilities/terraform/infra/lambda.tf similarity index 100% rename from examples/powertools-examples-core/terraform/infra/lambda.tf rename to examples/powertools-examples-core-utilities/terraform/infra/lambda.tf diff --git a/examples/powertools-examples-core/terraform/main.tf b/examples/powertools-examples-core-utilities/terraform/main.tf similarity index 100% rename from examples/powertools-examples-core/terraform/main.tf rename to examples/powertools-examples-core-utilities/terraform/main.tf diff --git a/examples/powertools-examples-core/terraform/pom.xml b/examples/powertools-examples-core-utilities/terraform/pom.xml similarity index 52% rename from examples/powertools-examples-core/terraform/pom.xml rename to examples/powertools-examples-core-utilities/terraform/pom.xml index 8781b70f4..dd9ed4685 100644 --- a/examples/powertools-examples-core/terraform/pom.xml +++ b/examples/powertools-examples-core-utilities/terraform/pom.xml @@ -2,17 +2,16 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 + Powertools for AWS Lambda (Java) - Examples - Core Utilities (logging, tracing, metrics) with Terraform software.amazon.lambda.examples - 1.19.0-SNAPSHOT - powertools-examples-core-terraform + 2.0.0-RC1 + powertools-examples-core-utilities-terraform jar - Powertools for AWS Lambda (Java) library Examples - Core - - 2.22.1 - 1.8 - 1.8 + 11 + 11 + 1.9.20.1 @@ -23,7 +22,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} @@ -34,22 +33,17 @@ com.amazonaws aws-lambda-java-core - 1.2.2 + 1.2.3 com.amazonaws aws-lambda-java-events - 3.11.4 - - - org.apache.logging.log4j - log4j-core - ${log4j.version} + 3.15.0 - org.apache.logging.log4j - log4j-api - ${log4j.version} + org.aspectj + aspectjrt + ${aspectj.version} @@ -59,7 +53,7 @@ dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 ${maven.compiler.source} ${maven.compiler.target} @@ -86,11 +80,18 @@ + + + org.aspectj + aspectjtools + ${aspectj.version} + + org.apache.maven.plugins maven-shade-plugin - 3.5.2 + 3.6.0 package @@ -100,7 +101,7 @@ false - + @@ -117,6 +118,7 @@ org.apache.maven.plugins maven-deploy-plugin + 3.1.2 true @@ -124,7 +126,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.5 + 3.1.2 handler @@ -133,79 +135,4 @@ - - - - jdk8 - - (,11) - - - 1.9.7 - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - dev.aspectj - aspectj-maven-plugin - ${aspectj.plugin.version} - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-metrics - - - - - - - compile - test-compile - - - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - diff --git a/examples/powertools-examples-core/gradle/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java similarity index 76% rename from examples/powertools-examples-core/gradle/src/main/java/helloworld/App.java rename to examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java index fccc63b9a..771f5c1f1 100644 --- a/examples/powertools-examples-core/gradle/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java @@ -14,8 +14,7 @@ package helloworld; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; +import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; import com.amazonaws.services.lambda.runtime.Context; @@ -31,11 +30,13 @@ import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; +import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.tracing.TracingUtils; @@ -44,37 +45,36 @@ * Handler for requests to Lambda function. */ public class App implements RequestHandler { - private final static Logger log = LogManager.getLogger(App.class); + private static final Logger log = LogManager.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) 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"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> - { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); - }); + DimensionSet dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); - LoggingUtils.appendKey("test", "willBeLogged"); + MDC.put("test", "willBeLogged"); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); try { final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); - log.info(pageContents); + log.info("", entry("ip", pageContents)); TracingUtils.putAnnotation("Test", "New"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - TracingUtils.withSubsegment("loggingResponse", subsegment -> - { + TracingUtils.withSubsegment("loggingResponse", subsegment -> { String sampled = "log something out"; log.info(sampled); log.info(output); @@ -91,11 +91,6 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv } } - @Tracing - private void log() { - log.info("inside threaded logging for function"); - } - @Tracing(namespace = "getPageContents", captureMode = CaptureMode.DISABLED) private String getPageContents(String address) throws IOException { URL url = new URL(address); diff --git a/examples/powertools-examples-core/terraform/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/AppStream.java similarity index 89% rename from examples/powertools-examples-core/terraform/src/main/java/helloworld/AppStream.java rename to examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/AppStream.java index 401ef8c48..c13ab9f2e 100644 --- a/examples/powertools-examples-core/terraform/src/main/java/helloworld/AppStream.java +++ b/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/AppStream.java @@ -22,14 +22,14 @@ import java.io.OutputStream; import java.util.Map; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; public class AppStream implements RequestStreamHandler { private static final ObjectMapper mapper = new ObjectMapper(); @Override @Logging(logEvent = true) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { Map map = mapper.readValue(input, Map.class); diff --git a/examples/powertools-examples-core/terraform/src/main/resources/log4j2.xml b/examples/powertools-examples-core-utilities/terraform/src/main/resources/log4j2.xml similarity index 100% rename from examples/powertools-examples-core/terraform/src/main/resources/log4j2.xml rename to examples/powertools-examples-core-utilities/terraform/src/main/resources/log4j2.xml diff --git a/examples/powertools-examples-core/kotlin/build.gradle.kts b/examples/powertools-examples-core/kotlin/build.gradle.kts deleted file mode 100644 index 4a167f225..000000000 --- a/examples/powertools-examples-core/kotlin/build.gradle.kts +++ /dev/null @@ -1,39 +0,0 @@ -plugins { - id("io.freefair.aspectj.post-compile-weaving") version "6.6.3" - kotlin("jvm") version "1.9.10" -} - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - implementation("com.amazonaws:aws-lambda-java-core:1.2.2") - implementation("com.fasterxml.jackson.core:jackson-annotations:2.13.2") - implementation("com.fasterxml.jackson.core:jackson-databind:2.13.2.2") - implementation("com.amazonaws:aws-lambda-java-events:3.11.0") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.2") - aspect("software.amazon.lambda:powertools-tracing:1.19.0-SNAPSHOT") - aspect("software.amazon.lambda:powertools-logging:1.19.0-SNAPSHOT") - aspect("software.amazon.lambda:powertools-metrics:1.19.0-SNAPSHOT") - testImplementation("junit:junit:4.13.2") - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") -} - -tasks.compileKotlin { - kotlinOptions { - jvmTarget = "1.8" - } -} - -tasks.compileTestKotlin { - kotlinOptions { - jvmTarget = "1.8" - } -} - -// If using JDK 11 or higher, use the following instead: -//kotlin { -// jvmToolchain(11) -//} \ No newline at end of file diff --git a/examples/powertools-examples-core/kotlin/src/test/kotlin/helloworld/AppTest.kt b/examples/powertools-examples-core/kotlin/src/test/kotlin/helloworld/AppTest.kt deleted file mode 100644 index 8aae081e8..000000000 --- a/examples/powertools-examples-core/kotlin/src/test/kotlin/helloworld/AppTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package helloworld - - -import org.junit.Assert -import org.junit.Test - -class AppTest { - @Test - fun successfulResponse() { - val app = App() - val result = app.handleRequest(null, null) - Assert.assertEquals(200, result.statusCode.toLong()) - Assert.assertEquals("application/json", result.headers["Content-Type"]) - val content = result.body - Assert.assertNotNull(content) - Assert.assertTrue(""""message"""" in content) - Assert.assertTrue(""""hello world"""" in content) - Assert.assertTrue(""""location"""" in content) - } -} diff --git a/examples/powertools-examples-core/serverless/pom.xml b/examples/powertools-examples-core/serverless/pom.xml deleted file mode 100644 index ef324056d..000000000 --- a/examples/powertools-examples-core/serverless/pom.xml +++ /dev/null @@ -1,203 +0,0 @@ - - 4.0.0 - - software.amazon.lambda.examples - 1.19.0-SNAPSHOT - powertools-examples-core-serverless - jar - - Powertools for AWS Lambda (Java) library Examples - Core - - - 2.22.1 - 1.8 - 1.8 - - - - - software.amazon.lambda - powertools-tracing - ${project.version} - - - software.amazon.lambda - powertools-logging - ${project.version} - - - software.amazon.lambda - powertools-metrics - ${project.version} - - - com.amazonaws - aws-lambda-java-core - 1.2.3 - - - com.amazonaws - aws-lambda-java-events - 3.11.4 - - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - - - - - helloworld-lambda - - - dev.aspectj - aspectj-maven-plugin - 1.13.1 - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-metrics - - - - - - - compile - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.5.2 - - - package - - shade - - - false - - - - - - - - - - com.github.edwgiz - maven-shade-plugin.log4j2-cachefile-transformer - 2.15 - - - - - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - - - - - - jdk8 - - (,11) - - - 1.9.7 - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - dev.aspectj - aspectj-maven-plugin - ${aspectj.plugin.version} - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-metrics - - - - - - - compile - test-compile - - - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - - diff --git a/examples/powertools-examples-core/terraform/src/main/java/helloworld/App.java b/examples/powertools-examples-core/terraform/src/main/java/helloworld/App.java deleted file mode 100644 index dacd7f1d4..000000000 --- a/examples/powertools-examples-core/terraform/src/main/java/helloworld/App.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package helloworld; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; -import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; - -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 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 org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; -import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; -import software.amazon.lambda.powertools.metrics.Metrics; -import software.amazon.lambda.powertools.tracing.CaptureMode; -import software.amazon.lambda.powertools.tracing.Tracing; -import software.amazon.lambda.powertools.tracing.TracingUtils; - -/** - * Handler for requests to Lambda function. - */ -public class App implements RequestHandler { - private final static Logger log = LogManager.getLogger(App.class); - - // This is controlled by POWERTOOLS_LOGGER_SAMPLE_RATE environment variable - // @Logging(logEvent = true, samplingRate = 0.7) - // This is controlled by POWERTOOLS_METRICS_NAMESPACE environment variable - // @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) - // This is controlled by POWERTOOLS_TRACER_CAPTURE_ERROR environment variable - @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - 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"); - - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); - - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> - { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); - }); - - LoggingUtils.appendKey("test", "willBeLogged"); - - APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() - .withHeaders(headers); - try { - final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); - log.info(pageContents); - TracingUtils.putAnnotation("Test", "New"); - String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - - TracingUtils.withSubsegment("loggingResponse", subsegment -> - { - String sampled = "log something out"; - log.info(sampled); - log.info(output); - }); - - log.info("After output"); - return response - .withStatusCode(200) - .withBody(output); - } catch (RuntimeException | IOException e) { - return response - .withBody("{}") - .withStatusCode(500); - } - } - - @Tracing(namespace = "getPageContents", captureMode = CaptureMode.DISABLED) - private String getPageContents(String address) throws IOException { - URL url = new URL(address); - putMetadata("getPageContents", address); - try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { - return br.lines().collect(Collectors.joining(System.lineSeparator())); - } - } -} diff --git a/examples/powertools-examples-idempotency/pom.xml b/examples/powertools-examples-idempotency/pom.xml index e6d205827..f0fcf545a 100644 --- a/examples/powertools-examples-idempotency/pom.xml +++ b/examples/powertools-examples-idempotency/pom.xml @@ -17,15 +17,15 @@ 4.0.0 software.amazon.lambda.examples - 1.19.0-SNAPSHOT + 2.0.0-RC1 powertools-examples-idempotency jar - Powertools for AWS Lambda (Java) library Examples - Idempotency + Powertools for AWS Lambda (Java) - Examples - Idempotency - 2.22.1 - 1.8 - 1.8 + 11 + 11 + 1.9.20.1 @@ -36,12 +36,12 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb ${project.version} @@ -52,17 +52,25 @@ com.amazonaws aws-lambda-java-events - 3.11.4 + 3.15.0 - org.apache.logging.log4j - log4j-core - ${log4j.version} + org.aspectj + aspectjrt + ${aspectj.version} + + + + org.mockito + mockito-core + 4.11.0 + test - org.apache.logging.log4j - log4j-api - ${log4j.version} + org.junit.jupiter + junit-jupiter-api + 5.11.1 + test @@ -71,7 +79,7 @@ dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 ${maven.compiler.source} ${maven.compiler.target} @@ -87,7 +95,7 @@ software.amazon.lambda - powertools-idempotency + powertools-idempotency-core @@ -98,6 +106,13 @@ + + + org.aspectj + aspectjtools + ${aspectj.version} + + org.apache.maven.plugins @@ -120,7 +135,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.5 + 3.1.2 ${project.build.directory}/native-libs @@ -135,7 +150,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.5.2 + 3.6.0 package @@ -145,8 +160,7 @@ false - + @@ -159,89 +173,14 @@ - org.apache.maven.plugins maven-deploy-plugin + 3.1.2 true - - - - jdk8 - - (,11) - - - 1.9.7 - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - dev.aspectj - aspectj-maven-plugin - ${aspectj.plugin.version} - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-idempotency - - - - - - - compile - test-compile - - - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - diff --git a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java index 72fa621ad..029877c73 100644 --- a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java @@ -26,18 +26,18 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.lambda.powertools.idempotency.Idempotency; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; import software.amazon.lambda.powertools.idempotency.Idempotent; -import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore; +import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.DynamoDBPersistenceStore; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.utilities.JsonConfig; public class App implements RequestHandler { - private final static Logger log = LogManager.getLogger(App.class); + private static final Logger log = LoggerFactory.getLogger(App.class); public App() { this(null); @@ -45,20 +45,36 @@ public App() { public App(DynamoDbClient client) { Idempotency.config().withConfig( - IdempotencyConfig.builder() - .withEventKeyJMESPath( - "powertools_json(body).address") // will retrieve the address field in the body which is a string transformed to json with `powertools_json` - .build()) + IdempotencyConfig.builder() + .withEventKeyJMESPath("powertools_json(body).address") + .withResponseHook((responseData, dataRecord) -> { + if (responseData instanceof APIGatewayProxyResponseEvent) { + APIGatewayProxyResponseEvent proxyResponse = (APIGatewayProxyResponseEvent) responseData; + final Map headers = new HashMap<>(); + headers.putAll(proxyResponse.getHeaders()); + headers.put("x-idempotency-response", "true"); + headers.put("x-idempotency-expiration", + String.valueOf(dataRecord.getExpiryTimestamp())); + + proxyResponse.setHeaders(headers); + + return proxyResponse; + } + + return responseData; + }) + .build()) .withPersistenceStore( DynamoDBPersistenceStore.builder() .withDynamoDbClient(client) .withTableName(System.getenv("IDEMPOTENCY_TABLE")) - .build() - ).configure(); + .build()) + .configure(); } /** - * This is our Lambda event handler. It accepts HTTP POST requests from API gateway and returns the contents of the given URL. Requests are made idempotent + * This is your Lambda event handler. It accepts HTTP POST requests from API gateway and returns the contents of the + * given URL. Requests are made idempotent * by the idempotency library, and results are cached for the default 1h expiry time. *

* You can test the endpoint like this: @@ -67,8 +83,10 @@ public App(DynamoDbClient client) { * curl -X POST https://[REST-API-ID].execute-api.[REGION].amazonaws.com/Prod/helloidem/ -H "Content-Type: application/json" -d '{"address": "https://checkip.amazonaws.com"}' * *

    - *
  • First call will execute the handleRequest normally, and store the response in the idempotency table (Look into DynamoDB)
  • - *
  • Second call (and next ones) will retrieve from the cache (if cache is enabled, which is by default) or from the store, the handler won't be called. Until the expiration happens (by default 1 hour).
  • + *
  • First call will execute the handleRequest normally, and store the response in the idempotency table (Look + * into DynamoDB)
  • + *
  • Second call (and next ones) will retrieve from the cache (if cache is enabled, which is by default) or from + * the store, the handler won't be called. Until the expiration happens (by default 1 hour).
  • *
*/ @Idempotent // The magic is here! @@ -101,14 +119,14 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv } } - /** * Helper to retrieve the contents of the given URL and return them as a string. *

* We could also put the @Idempotent annotation here if we only wanted this sub-operation to be idempotent. Putting * it on the handler, however, reduces total execution time and saves us time! * - * @param address The URL to fetch + * @param address + * The URL to fetch * @return The contents of the given URL * @throws IOException */ @@ -118,4 +136,4 @@ private String getPageContents(String address) throws IOException { return br.lines().collect(Collectors.joining(System.lineSeparator())); } } -} \ No newline at end of file +} diff --git a/examples/powertools-examples-parameters/pom.xml b/examples/powertools-examples-parameters/pom.xml deleted file mode 100644 index 5ef7d69a2..000000000 --- a/examples/powertools-examples-parameters/pom.xml +++ /dev/null @@ -1,143 +0,0 @@ - - 4.0.0 - software.amazon.lambda.examples - 1.19.0-SNAPSHOT - powertools-examples-parameters - jar - Powertools for AWS Lambda (Java) library Examples - Parameters - - - 1.8 - 1.8 - - - - - software.amazon.lambda - powertools-logging - ${project.version} - - - software.amazon.lambda - powertools-parameters - ${project.version} - - - com.amazonaws - aws-lambda-java-core - 1.2.3 - - - com.amazonaws - aws-lambda-java-events - 3.11.4 - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - 3.2.5 - - - dev.aspectj - aspectj-maven-plugin - 1.13.1 - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-parameters - - - - - - - compile - - - - - - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - - - - - - jdk8 - - (,11) - - - 1.9.7 - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - dev.aspectj - aspectj-maven-plugin - ${aspectj.plugin.version} - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - ignore - UTF-8 - - - - process-sources - - compile - test-compile - - - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - - diff --git a/examples/powertools-examples-parameters/sam-graalvm/Dockerfile b/examples/powertools-examples-parameters/sam-graalvm/Dockerfile new file mode 100644 index 000000000..a690606ad --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/Dockerfile @@ -0,0 +1,14 @@ +#Use the official AWS SAM base image for Java 21 +FROM public.ecr.aws/sam/build-java21:latest + +#Install GraalVM dependencies +RUN curl -4 -L curl https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-x64_bin.tar.gz | tar -xvz +RUN mv graalvm-jdk-21.* /usr/lib/graalvm + +#Make native image and mvn available on CLI +RUN ln -s /usr/lib/graalvm/bin/native-image /usr/bin/native-image +RUN ln -s /usr/lib/maven/bin/mvn /usr/bin/mvn + +#Set GraalVM as default +ENV JAVA_HOME=/usr/lib/graalvm +ENV PATH=/usr/lib/graalvm/bin:$PATH diff --git a/examples/powertools-examples-parameters/sam-graalvm/Makefile b/examples/powertools-examples-parameters/sam-graalvm/Makefile new file mode 100644 index 000000000..5a3a00a69 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/Makefile @@ -0,0 +1,6 @@ +build-ParametersFunction: + mvn clean package -P native-image + chmod +x target/hello-world + cp target/hello-world $(ARTIFACTS_DIR) # (ARTIFACTS_DIR --> https://github.com/aws/aws-lambda-builders/blob/develop/aws_lambda_builders/workflows/custom_make/DESIGN.md#implementation) + chmod +x src/main/config/bootstrap + cp src/main/config/bootstrap $(ARTIFACTS_DIR) diff --git a/examples/powertools-examples-parameters/sam-graalvm/README.md b/examples/powertools-examples-parameters/sam-graalvm/README.md new file mode 100644 index 000000000..3797c778b --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/README.md @@ -0,0 +1,80 @@ +# Powertools for AWS Lambda (Java) - Parameters Example with SAM on GraalVM + +This project contains an example of Lambda function using the parameters module of Powertools for AWS Lambda (Java). For more information on this module, please refer to the [documentation](https://docs.powertools.aws.dev/lambda-java/utilities/parameters/). + +The example uses the [SSM Parameter Store](https://docs.powertools.aws.dev/lambda/java/utilities/parameters/#ssm-parameter-store) +and the [Secrets Manager](https://docs.powertools.aws.dev/lambda/java/utilities/parameters/#secrets-manager) to inject +runtime parameters into the application. +Have a look at [ParametersFunction.java](src/main/java/org/demo/parameters/ParametersFunction.java) for the full details. + +## Configuration + +- SAM uses [template.yaml](template.yaml) to define the application's AWS resources. + This file defines the Lambda function to be deployed as well as API Gateway for it. + +- Set the environment to use GraalVM + +```shell +export JAVA_HOME= +``` + +## Build the sample application + +- Build the Docker image that will be used as the environment for SAM build: + +```shell +docker build --platform linux/amd64 . -t powertools-examples-parameters-sam-graalvm +``` + +- Build the SAM project using the docker image + +```shell +sam build --use-container --build-image powertools-examples-parameters-sam-graalvm +``` + +#### [Optional] Building with -SNAPSHOT versions of PowerTools + +- If you are testing the example with a -SNAPSHOT version of PowerTools, the maven build inside the docker image will fail. This is because the -SNAPSHOT version of the PowerTools library that you are working on is still not available in maven central/snapshot repository. + To get around this, follow these steps: + - Create the native image using the `docker` command below on your development machine. The native image is created in the `target` directory. + - `` docker run --platform linux/amd64 -it -v `pwd`:`pwd` -w `pwd` -v ~/.m2:/root/.m2 powertools-examples-parameters-sam-graalvm mvn clean -Pnative-image package -DskipTests `` + - Edit the [`Makefile`](Makefile) remove this line + - `mvn clean package -P native-image` + - Build the SAM project using the docker image + - `sam build --use-container --build-image powertools-examples-parameters-sam-graalvm` + +## Deploy the sample application + +- SAM deploy + +```shell +sam deploy +``` + +This sample is based on Serverless Application Model (SAM). To deploy it, check out the instructions for getting +started with SAM in [the examples directory](../../README.md) + +## Test the application + +First, hit the URL of the application. You can do this with curl or your browser: + +```bash + curl https://[REST-API-ID].execute-api.[REGION].amazonaws.com/Prod/params/ +``` +You will get your IP address back. The contents of the logs will be more interesting, and show you the values +of the parameters injected into the handler: + +```bash +sam logs --stack-name $MY_STACK_NAME --tail +``` + +```json +{ + ... + "thread": "main", + "level": "INFO", + "loggerName": "org.demo.parameters.ParametersFunction", + "message": "secretjsonobj=MyObject{id=23443, code='hk38543oj24kn796kp67bkb234gkj679l68'}\n", + ... +} +``` diff --git a/examples/powertools-examples-parameters/sam-graalvm/pom.xml b/examples/powertools-examples-parameters/sam-graalvm/pom.xml new file mode 100644 index 000000000..8031710b1 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/pom.xml @@ -0,0 +1,204 @@ + + 4.0.0 + software.amazon.lambda.examples + 2.0.0-RC1 + powertools-examples-parameters-sam-graalvm + jar + Powertools for AWS Lambda (Java) - Examples - Parameters GraalVM + + + 2.24.0 + 11 + 11 + 1.9.20.1 + + + + + software.amazon.lambda + powertools-logging-log4j + ${project.version} + + + software.amazon.lambda + powertools-parameters-ssm + ${project.version} + + + software.amazon.lambda + powertools-parameters-secrets + ${project.version} + + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.15.0 + + + org.aspectj + aspectjrt + ${aspectj.version} + + + com.amazonaws + aws-lambda-java-runtime-interface-client + 2.1.1 + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-slf4j2-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-layout-template-json + ${log4j.version} + + + + + org.mockito + mockito-core + 5.1.1 + test + + + org.junit.jupiter + junit-jupiter-api + 5.11.1 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.3 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + 3.2.5 + + + dev.aspectj + aspectj-maven-plugin + 1.14 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-parameters + + + + + + + compile + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + false + + + + + + + + + org.apache.logging.log4j + log4j-transform-maven-shade-plugin-extensions + 0.1.0 + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.2 + + true + + + + + + + native-image + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.1 + true + + + build-native + + build + + package + + + + hello-world + com.amazonaws.services.lambda.runtime.api.client.AWSLambda + + + --enable-url-protocols=http + --add-opens java.base/java.util=ALL-UNNAMED + + + + + + + + diff --git a/examples/powertools-examples-parameters/sam-graalvm/src/main/config/bootstrap b/examples/powertools-examples-parameters/sam-graalvm/src/main/config/bootstrap new file mode 100644 index 000000000..8e7928cd3 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/config/bootstrap @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +./hello-world $_HANDLER \ No newline at end of file diff --git a/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/MyObject.java b/examples/powertools-examples-parameters/sam-graalvm/src/main/java/org/demo/parameters/MyObject.java similarity index 100% rename from examples/powertools-examples-parameters/src/main/java/org/demo/parameters/MyObject.java rename to examples/powertools-examples-parameters/sam-graalvm/src/main/java/org/demo/parameters/MyObject.java diff --git a/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/ParametersFunction.java b/examples/powertools-examples-parameters/sam-graalvm/src/main/java/org/demo/parameters/ParametersFunction.java similarity index 80% rename from examples/powertools-examples-parameters/src/main/java/org/demo/parameters/ParametersFunction.java rename to examples/powertools-examples-parameters/sam-graalvm/src/main/java/org/demo/parameters/ParametersFunction.java index 5b691cfd9..9c3c422cf 100644 --- a/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/ParametersFunction.java +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/java/org/demo/parameters/ParametersFunction.java @@ -29,19 +29,32 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import software.amazon.lambda.powertools.parameters.ParamManager; -import software.amazon.lambda.powertools.parameters.SSMProvider; -import software.amazon.lambda.powertools.parameters.SecretsProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.parameters.secrets.SecretsParam; +import software.amazon.lambda.powertools.parameters.secrets.SecretsProvider; +import software.amazon.lambda.powertools.parameters.ssm.SSMParam; +import software.amazon.lambda.powertools.parameters.ssm.SSMProvider; public class ParametersFunction implements RequestHandler { - private final static Logger log = LogManager.getLogger(ParametersFunction.class); + private static final Logger log = LoggerFactory.getLogger(ParametersFunction.class); - SSMProvider ssmProvider = ParamManager.getSsmProvider(); - SecretsProvider secretsProvider = ParamManager.getSecretsProvider(); + // Annotation-style injection from secrets manager + @SecretsParam(key = "/powertools-java/userpwd") + String secretParamInjected; - String simpleValue = ssmProvider.defaultMaxAge(30, SECONDS).get("/powertools-java/sample/simplekey"); + // Annotation-style injection from Systems Manager + @SSMParam(key = "/powertools-java/sample/simplekey") + String ssmParamInjected; + + SSMProvider ssmProvider = SSMProvider + .builder() + .build(); + SecretsProvider secretsProvider = SecretsProvider + .builder() + .build(); + + String simpleValue = ssmProvider.withMaxAge(30, SECONDS).get("/powertools-java/sample/simplekey"); String listValue = ssmProvider.withMaxAge(60, SECONDS).get("/powertools-java/sample/keylist"); MyObject jsonObj = ssmProvider.withTransformation(json).get("/powertools-java/sample/keyjson", MyObject.class); Map allValues = ssmProvider.getMultiple("/powertools-java/sample"); diff --git a/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json new file mode 100644 index 000000000..2780aca09 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json @@ -0,0 +1,13 @@ +[ + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntime", + "methods":[{"name":"","parameterTypes":[] }], + "fields":[{"name":"logger"}], + "allPublicMethods":true + }, + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal", + "methods":[{"name":"","parameterTypes":[] }], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json new file mode 100644 index 000000000..ddda5d5f1 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json @@ -0,0 +1,35 @@ +[ + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$ProxyRequestContext", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$RequestIdentity", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] \ No newline at end of file diff --git a/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json new file mode 100644 index 000000000..d30696750 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json @@ -0,0 +1,11 @@ +[ + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClientException", + "methods":[{"name":"","parameterTypes":["java.lang.String","int"] }] + }, + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.InvocationRequest", + "fields":[{"name":"id"}, {"name":"invokedFunctionArn"}, {"name":"deadlineTimeInMs"}, {"name":"xrayTraceId"}, {"name":"clientContext"}, {"name":"cognitoIdentity"}, {"name":"content"}], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties new file mode 100644 index 000000000..20f8b7801 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-build-time=jdk.xml.internal.SecuritySupport \ No newline at end of file diff --git a/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json new file mode 100644 index 000000000..106edef38 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json @@ -0,0 +1,34 @@ +[ + { + "name":"com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers[]" + }, + { + "name":"com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntime", + "fields":[{"name":"logger"}] + }, + { + "name":"java.lang.Void", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"java.util.Collections$UnmodifiableMap", + "fields":[{"name":"m"}] + }, + { + "name":"jdk.internal.module.IllegalAccessLogger", + "fields":[{"name":"logger"}] + }, + { + "name":"sun.misc.Unsafe", + "fields":[{"name":"theUnsafe"}] + }, + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.InvocationRequest", + "fields":[{"name":"id"}, {"name":"invokedFunctionArn"}, {"name":"deadlineTimeInMs"}, {"name":"xrayTraceId"}, {"name":"clientContext"}, {"name":"cognitoIdentity"}, {"name":"content"}], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json new file mode 100644 index 000000000..7cc78a494 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json @@ -0,0 +1,19 @@ +{ + "resources": { + "includes": [ + { + "pattern": "\\Qaarch64/aws-lambda-runtime-interface-client.glibc.so\\E" + }, + { + "pattern": "\\Qaarch64/aws-lambda-runtime-interface-client.musl.so\\E" + }, + { + "pattern": "\\Qx86_64/aws-lambda-runtime-interface-client.glibc.so\\E" + }, + { + "pattern": "\\Qx86_64/aws-lambda-runtime-interface-client.musl.so\\E" + } + ] + }, + "bundles": [] +} \ No newline at end of file diff --git a/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json new file mode 100644 index 000000000..9890688f9 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json @@ -0,0 +1,25 @@ +[ + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers[]" + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7HandlersImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ser.Serializers[]" + }, + { + "name": "org.joda.time.DateTime", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] diff --git a/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/native-image.properties b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/native-image.properties new file mode 100644 index 000000000..db5ebaa55 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/native-image.properties @@ -0,0 +1 @@ +Args = --enable-url-protocols=http,https \ No newline at end of file diff --git a/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/reflect-config.json b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/reflect-config.json new file mode 100644 index 000000000..16a37f483 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/reflect-config.json @@ -0,0 +1,20 @@ +[ + { + "name": "org.demo.parameters.ParametersFunction", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "org.demo.parameters.MyObject", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] diff --git a/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/resource-config.json b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/resource-config.json new file mode 100644 index 000000000..be6aac3f6 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/META-INF/native-image/helloworld/resource-config.json @@ -0,0 +1,7 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\Qlog4j2.xml\\E" + }]}, + "bundles":[] +} diff --git a/examples/powertools-examples-parameters/src/main/resources/log4j2.xml b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/log4j2.xml similarity index 84% rename from examples/powertools-examples-parameters/src/main/resources/log4j2.xml rename to examples/powertools-examples-parameters/sam-graalvm/src/main/resources/log4j2.xml index 033da8a11..fe943d707 100644 --- a/examples/powertools-examples-parameters/src/main/resources/log4j2.xml +++ b/examples/powertools-examples-parameters/sam-graalvm/src/main/resources/log4j2.xml @@ -2,7 +2,7 @@ - + diff --git a/examples/powertools-examples-parameters/sam-graalvm/template.yaml b/examples/powertools-examples-parameters/sam-graalvm/template.yaml new file mode 100644 index 000000000..b4a4d6907 --- /dev/null +++ b/examples/powertools-examples-parameters/sam-graalvm/template.yaml @@ -0,0 +1,100 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + validation demo + +Globals: + Function: + Timeout: 20 + MemorySize: 512 + Tracing: Active + + +Resources: + ParametersFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: . + Handler: org.demo.parameters.ParametersFunction::handleRequest + Runtime: provided.al2023 + MemorySize: 512 + Tracing: Active + Environment: + Variables: + LOG_LEVEL: INFO + Policies: + - AWSSecretsManagerGetSecretValuePolicy: + SecretArn: !Ref UserPwd + - AWSSecretsManagerGetSecretValuePolicy: + SecretArn: !Ref SecretConfig + - Statement: + - Sid: SSMGetParameterPolicy + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + - ssm:GetParametersByPath + Resource: '*' + Events: + HelloWorld: + Type: Api + Properties: + Path: /params + Method: get + + UserPwd: + Type: AWS::SecretsManager::Secret + Properties: + Name: /powertools-java/userpwd + Description: Generated secret for lambda-powertools-java powertools-parameters + module + GenerateSecretString: + SecretStringTemplate: '{"username": "test-user"}' + GenerateStringKey: password + PasswordLength: 15 + ExcludeCharacters: '"@/\' + SecretConfig: + Type: AWS::SecretsManager::Secret + Properties: + Name: /powertools-java/secretcode + Description: Json secret for lambda-powertools-java powertools-parameters module + SecretString: '{"id":23443,"code":"hk38543oj24kn796kp67bkb234gkj679l68"}' + BasicParameter: + Type: AWS::SSM::Parameter + Properties: + Name: /powertools-java/sample/simplekey + Type: String + Value: simplevalue + Description: Simple SSM Parameter for lambda-powertools-java powertools-parameters + module + ParameterList: + Type: AWS::SSM::Parameter + Properties: + Name: /powertools-java/sample/keylist + Type: StringList + Value: value1,value2,value3 + Description: SSM Parameter List for lambda-powertools-java powertools-parameters + module + JsonParameter: + Type: AWS::SSM::Parameter + Properties: + Name: /powertools-java/sample/keyjson + Type: String + Value: '{"id":23443,"code":"hk38543oj24kn796kp67bkb234gkj679l68"}' + Description: Json SSM Parameter for lambda-powertools-java powertools-parameters + module + Base64Parameter: + Type: AWS::SSM::Parameter + Properties: + Name: /powertools-java/sample/keybase64 + Type: String + Value: aGVsbG8gd29ybGQ= + Description: Base64 SSM Parameter for lambda-powertools-java powertools-parameters module + +Outputs: + ParametersApi: + Description: "API Gateway endpoint URL for Prod stage for Parameters function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/params/" + ParametersFunction: + Description: "Parameters Lambda Function ARN" + Value: !GetAtt ParametersFunction.Arn \ No newline at end of file diff --git a/examples/powertools-examples-parameters/README.md b/examples/powertools-examples-parameters/sam/README.md similarity index 96% rename from examples/powertools-examples-parameters/README.md rename to examples/powertools-examples-parameters/sam/README.md index a65307f69..661752957 100644 --- a/examples/powertools-examples-parameters/README.md +++ b/examples/powertools-examples-parameters/sam/README.md @@ -10,7 +10,7 @@ Have a look at [ParametersFunction.java](src/main/java/org/demo/parameters/Param ## Deploy the sample application This sample is based on Serverless Application Model (SAM). To deploy it, check out the instructions for getting -started with SAM in [the examples directory](../README.md) +started with SAM in [the examples directory](../../README.md) ## Test the application diff --git a/examples/powertools-examples-parameters/sam/pom.xml b/examples/powertools-examples-parameters/sam/pom.xml new file mode 100644 index 000000000..02b991d68 --- /dev/null +++ b/examples/powertools-examples-parameters/sam/pom.xml @@ -0,0 +1,118 @@ + + 4.0.0 + software.amazon.lambda.examples + 2.0.0-RC1 + powertools-examples-parameters-sam + jar + Powertools for AWS Lambda (Java) - Examples - Parameters + + + 11 + 11 + 1.9.20.1 + + + + + software.amazon.lambda + powertools-logging-log4j + ${project.version} + + + software.amazon.lambda + powertools-parameters-ssm + ${project.version} + + + software.amazon.lambda + powertools-parameters-secrets + ${project.version} + + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.15.0 + + + org.aspectj + aspectjrt + ${aspectj.version} + + + + + org.mockito + mockito-core + 5.1.1 + test + + + org.junit.jupiter + junit-jupiter-api + 5.11.1 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.3 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + 3.2.5 + + + dev.aspectj + aspectj-maven-plugin + 1.14 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-parameters + + + + + + + compile + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.2 + + true + + + + + diff --git a/examples/powertools-examples-parameters/sam/src/main/java/org/demo/parameters/MyObject.java b/examples/powertools-examples-parameters/sam/src/main/java/org/demo/parameters/MyObject.java new file mode 100644 index 000000000..d406ae3df --- /dev/null +++ b/examples/powertools-examples-parameters/sam/src/main/java/org/demo/parameters/MyObject.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package org.demo.parameters; + +public class MyObject { + + private long id; + private String code; + + public MyObject() { + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + @Override + public String toString() { + return "MyObject{" + + "id=" + id + + ", code='" + code + '\'' + + '}'; + } +} diff --git a/examples/powertools-examples-parameters/sam/src/main/java/org/demo/parameters/ParametersFunction.java b/examples/powertools-examples-parameters/sam/src/main/java/org/demo/parameters/ParametersFunction.java new file mode 100644 index 000000000..9c3c422cf --- /dev/null +++ b/examples/powertools-examples-parameters/sam/src/main/java/org/demo/parameters/ParametersFunction.java @@ -0,0 +1,109 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package org.demo.parameters; + +import static java.time.temporal.ChronoUnit.SECONDS; +import static software.amazon.lambda.powertools.parameters.transform.Transformer.base64; +import static software.amazon.lambda.powertools.parameters.transform.Transformer.json; + +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 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.parameters.secrets.SecretsParam; +import software.amazon.lambda.powertools.parameters.secrets.SecretsProvider; +import software.amazon.lambda.powertools.parameters.ssm.SSMParam; +import software.amazon.lambda.powertools.parameters.ssm.SSMProvider; + +public class ParametersFunction implements RequestHandler { + private static final Logger log = LoggerFactory.getLogger(ParametersFunction.class); + + // Annotation-style injection from secrets manager + @SecretsParam(key = "/powertools-java/userpwd") + String secretParamInjected; + + // Annotation-style injection from Systems Manager + @SSMParam(key = "/powertools-java/sample/simplekey") + String ssmParamInjected; + + SSMProvider ssmProvider = SSMProvider + .builder() + .build(); + SecretsProvider secretsProvider = SecretsProvider + .builder() + .build(); + + String simpleValue = ssmProvider.withMaxAge(30, SECONDS).get("/powertools-java/sample/simplekey"); + String listValue = ssmProvider.withMaxAge(60, SECONDS).get("/powertools-java/sample/keylist"); + MyObject jsonObj = ssmProvider.withTransformation(json).get("/powertools-java/sample/keyjson", MyObject.class); + Map allValues = ssmProvider.getMultiple("/powertools-java/sample"); + String b64value = ssmProvider.withTransformation(base64).get("/powertools-java/sample/keybase64"); + + Map secretJson = + secretsProvider.withTransformation(json).get("/powertools-java/userpwd", Map.class); + MyObject secretJsonObj = secretsProvider.withMaxAge(42, SECONDS).withTransformation(json) + .get("/powertools-java/secretcode", MyObject.class); + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + + log.info("\n=============== SSM Parameter Store ==============="); + log.info("simplevalue={}, listvalue={}, b64value={}\n", simpleValue, listValue, b64value); + log.info("jsonobj={}\n", jsonObj); + + log.info("allvalues (multiple):"); + allValues.forEach((key, value) -> log.info("- {}={}\n", key, value)); + + log.info("\n=============== Secrets Manager ==============="); + log.info("secretjson:"); + secretJson.forEach((key, value) -> log.info("- {}={}\n", key, value)); + log.info("secretjsonobj={}\n", secretJsonObj); + + 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); + + 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(), "UTF-8"))) { + return br.lines().collect(Collectors.joining(System.lineSeparator())); + } + } +} diff --git a/powertools-test-suite/src/test/resources/log4j2.xml b/examples/powertools-examples-parameters/sam/src/main/resources/log4j2.xml similarity index 57% rename from powertools-test-suite/src/test/resources/log4j2.xml rename to examples/powertools-examples-parameters/sam/src/main/resources/log4j2.xml index 8ac9b34ce..fe943d707 100644 --- a/powertools-test-suite/src/test/resources/log4j2.xml +++ b/examples/powertools-examples-parameters/sam/src/main/resources/log4j2.xml @@ -1,18 +1,14 @@ - - - + + + - - - - diff --git a/examples/powertools-examples-parameters/template.yaml b/examples/powertools-examples-parameters/sam/template.yaml similarity index 100% rename from examples/powertools-examples-parameters/template.yaml rename to examples/powertools-examples-parameters/sam/template.yaml diff --git a/examples/powertools-examples-serialization/pom.xml b/examples/powertools-examples-serialization/pom.xml index e63ecbc5a..560c08a56 100644 --- a/examples/powertools-examples-serialization/pom.xml +++ b/examples/powertools-examples-serialization/pom.xml @@ -2,20 +2,20 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.lambda.examples - 1.19.0-SNAPSHOT + 2.0.0-RC1 powertools-examples-serialization jar - Powertools for AWS Lambda (Java) library Examples - Serialization + Powertools for AWS Lambda (Java) - Examples - Serialization - 1.8 - 1.8 + 11 + 11 software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} @@ -31,7 +31,7 @@ com.amazonaws aws-lambda-java-events - 3.11.4 + 3.15.0 @@ -41,6 +41,15 @@ org.apache.maven.plugins maven-deploy-plugin + 3.1.2 + + true + + + + dev.aspectj + aspectj-maven-plugin + 1.14 true @@ -48,68 +57,4 @@ - - - - jdk8 - - (,11) - - - 1.9.7 - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - dev.aspectj - aspectj-maven-plugin - ${aspectj.plugin.version} - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - ignore - UTF-8 - - - - process-sources - - compile - test-compile - - - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - diff --git a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java index e70b37959..3ca75cf4a 100644 --- a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java +++ b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java @@ -22,18 +22,21 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import java.util.HashMap; import java.util.Map; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class APIGatewayRequestDeserializationFunction implements RequestHandler { - private final static Logger LOGGER = LogManager.getLogger(APIGatewayRequestDeserializationFunction.class); - private static final Map HEADERS = new HashMap() {{ + private static final Logger LOGGER = LoggerFactory.getLogger(APIGatewayRequestDeserializationFunction.class); + private static final Map HEADERS = new HashMap() { + private static final long serialVersionUID = 7074189990115081999L; + { put("Content-Type", "application/json"); put("X-Custom-Header", "application/json"); - }}; + } + }; public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) { diff --git a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java index 36dbed074..79097e19c 100644 --- a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java +++ b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java @@ -20,13 +20,13 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.SQSEvent; import java.util.List; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class SQSEventDeserializationFunction implements RequestHandler { - private final static Logger LOGGER = LogManager.getLogger(SQSEventDeserializationFunction.class); + private static final Logger LOGGER = LoggerFactory.getLogger(SQSEventDeserializationFunction.class); public String handleRequest(SQSEvent event, Context context) { List products = extractDataFrom(event).asListOf(Product.class); diff --git a/examples/powertools-examples-serialization/src/main/resources/log4j2.xml b/examples/powertools-examples-serialization/src/main/resources/log4j2.xml index 033da8a11..fe943d707 100644 --- a/examples/powertools-examples-serialization/src/main/resources/log4j2.xml +++ b/examples/powertools-examples-serialization/src/main/resources/log4j2.xml @@ -2,7 +2,7 @@ - + diff --git a/examples/powertools-examples-sqs/README.md b/examples/powertools-examples-sqs/README.md deleted file mode 100644 index 45f4a4a74..000000000 --- a/examples/powertools-examples-sqs/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Powertools for AWS Lambda (Java) - SQS Batch Processing Example - -This project contains an example of Lambda function using the batch processing utilities module of Powertools for AWS Lambda (Java). -For more information on this module, please refer to the [documentation](https://docs.powertools.aws.dev/lambda/java/utilities/batch/). - -The project contains two functions: - -* [SqsMessageSender](src/main/java/org/demo/sqs/SqsMessageSender.java) - Sends a set of messages to an SQS queue. -This function is triggered every 5 minutes by an EventBridge schedule rule. -* [SqsPoller](src/main/java/org/demo/sqs/SqsPoller.java) - Listens to the same queue, processing items off in batches - -The poller intentionally fails intermittently processing messages to demonstrate the replay behaviour of the batch -module: - -

- -SqsPoller.java - -[SqsPoller.java:43](src/main/java/org/demo/sqs/SqsPoller.java) - -```java - public String process(SQSMessage message) { - log.info("Processing message with id {}", message.getMessageId()); - - int nextInt = random.nextInt(100); - - if(nextInt <= 10) { - log.info("Randomly picked message with id {} as business validation failure.", message.getMessageId()); - throw new IllegalArgumentException("Failed business validation. No point of retrying. Move me to DLQ." + message.getMessageId()); - } - - if(nextInt > 90) { - log.info("Randomly picked message with id {} as intermittent failure.", message.getMessageId()); - throw new RuntimeException("Failed due to intermittent issue. Will be sent back for retry." + message.getMessageId()); - } - - return "Success"; - } -``` - -
- -## Deploy the sample application - -This sample is based on Serverless Application Model (SAM). To deploy it, check out the instructions for getting -started with SAM in [the examples directory](../README.md) - -## Test the application - -As the test is pushing through a batch every 5 minutes, we can simply watch the logs to see the batches being processed: - -```bash - sam logs --tail --stack-name $MY_STACK -``` - -As the handler intentionally introduces intermittent failures, we should expect to see error messages too! diff --git a/examples/powertools-examples-sqs/pom.xml b/examples/powertools-examples-sqs/pom.xml deleted file mode 100644 index 01bbfd8d2..000000000 --- a/examples/powertools-examples-sqs/pom.xml +++ /dev/null @@ -1,203 +0,0 @@ - - 4.0.0 - software.amazon.lambda.examples - 1.19.0-SNAPSHOT - powertools-examples-sqs - jar - Powertools for AWS Lambda (Java) library Examples - SQS - - - 2.22.1 - 1.8 - 1.8 - - - - - software.amazon.lambda - powertools-logging - ${project.version} - - - software.amazon.lambda - powertools-sqs - ${project.version} - - - software.amazon.awssdk - url-connection-client - 2.25.6 - - - com.amazonaws - aws-lambda-java-core - 1.2.3 - - - com.amazonaws - aws-lambda-java-events - 3.11.4 - - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-joda - 2.15.2 - - - - junit - junit - 4.13.2 - test - - - - - - - dev.aspectj - aspectj-maven-plugin - 1.13.1 - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-sqs - - - - - - - compile - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.5.2 - - - package - - shade - - - false - - - - - - - - - org.apache.logging.log4j - log4j-transform-maven-shade-plugin-extensions - 0.1.0 - - - - - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - - - - - - jdk8 - - (,11) - - - 1.9.7 - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - dev.aspectj - aspectj-maven-plugin - ${aspectj.plugin.version} - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-sqs - - - - - - - compile - test-compile - - - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - - diff --git a/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsMessageSender.java b/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsMessageSender.java deleted file mode 100644 index 701d6808f..000000000 --- a/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsMessageSender.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package org.demo.sqs; - -import static java.util.stream.Collectors.toList; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.joda.JodaModule; -import java.security.SecureRandom; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.stream.IntStream; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; -import software.amazon.awssdk.services.sqs.SqsClient; -import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchResponse; -import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; - -public class SqsMessageSender implements RequestHandler { - - private static final Logger log = LogManager.getLogger(SqsMessageSender.class); - - private static final SqsClient sqsClient = SqsClient.builder() - .httpClient(UrlConnectionHttpClient.create()) - .build(); - - private static final Random random = new SecureRandom(); - - private static final ObjectMapper objectMapper; - - static { - objectMapper = new ObjectMapper(); - objectMapper.registerModule(new JodaModule()); - LoggingUtils.defaultObjectMapper(objectMapper); - } - - @Logging(logEvent = true) - public String handleRequest(final ScheduledEvent input, final Context context) { - String queueUrl = System.getenv("QUEUE_URL"); - - // Push 5 messages on each invoke. - List batchRequestEntries = IntStream.range(0, 5) - .mapToObj(value -> - { - Map attributeValueHashMap = new HashMap<>(); - attributeValueHashMap.put("Key" + value, MessageAttributeValue.builder() - .dataType("String") - .stringValue("Value" + value) - .build()); - - byte[] array = new byte[7]; - random.nextBytes(array); - - return SendMessageBatchRequestEntry.builder() - .messageAttributes(attributeValueHashMap) - .id(input.getId() + value) - .messageBody("Sample Message " + value) - .build(); - }).collect(toList()); - - SendMessageBatchResponse sendMessageBatchResponse = sqsClient.sendMessageBatch(SendMessageBatchRequest.builder() - .queueUrl(queueUrl) - .entries(batchRequestEntries) - .build()); - - log.info("Sent Message {}", sendMessageBatchResponse); - - return "Success"; - } -} \ No newline at end of file diff --git a/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsPoller.java b/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsPoller.java deleted file mode 100644 index 9ad5c7868..000000000 --- a/examples/powertools-examples-sqs/src/main/java/org/demo/sqs/SqsPoller.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package org.demo.sqs; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import java.security.SecureRandom; -import java.util.Random; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; -import software.amazon.awssdk.services.sqs.SqsClient; -import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsMessageHandler; -import software.amazon.lambda.powertools.sqs.SqsUtils; - -/** - * Handler for requests to Lambda function. - */ -public class SqsPoller implements RequestHandler { - - static { - // https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/lambda-optimize-starttime.html - SqsUtils.overrideSqsClient(SqsClient.builder() - .httpClient(UrlConnectionHttpClient.create()) - .build()); - } - - Logger log = LogManager.getLogger(SqsPoller.class); - Random random = new SecureRandom(); - - @SqsBatch(value = BatchProcessor.class, nonRetryableExceptions = {IllegalArgumentException.class}) - @Logging(logEvent = true) - public String handleRequest(final SQSEvent input, final Context context) { - return "Success"; - } - - private class BatchProcessor implements SqsMessageHandler { - @Override - public String process(SQSMessage message) { - log.info("Processing message with id {}", message.getMessageId()); - - int nextInt = random.nextInt(100); - - if (nextInt <= 10) { - log.info("Randomly picked message with id {} as business validation failure.", message.getMessageId()); - throw new IllegalArgumentException( - "Failed business validation. No point of retrying. Move me to DLQ." + message.getMessageId()); - } - - if (nextInt > 90) { - log.info("Randomly picked message with id {} as intermittent failure.", message.getMessageId()); - throw new RuntimeException( - "Failed due to intermittent issue. Will be sent back for retry." + message.getMessageId()); - } - - return "Success"; - } - } -} \ No newline at end of file diff --git a/examples/powertools-examples-sqs/template.yaml b/examples/powertools-examples-sqs/template.yaml deleted file mode 100644 index 50327de18..000000000 --- a/examples/powertools-examples-sqs/template.yaml +++ /dev/null @@ -1,148 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: > - sqs batch processing demo - -Globals: - Function: - Timeout: 20 - Runtime: java11 - MemorySize: 512 - Tracing: Active - Environment: - Variables: - # Powertools for AWS Lambda (Java) env vars: https://docs.powertools.aws.dev/lambda/java/#environment-variables - POWERTOOLS_LOG_LEVEL: INFO - POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 - POWERTOOLS_LOGGER_LOG_EVENT: true - -Resources: - CustomerKey: - Type: AWS::KMS::Key - Properties: - Description: KMS key for encrypted queues - Enabled: true - KeyPolicy: - Version: '2012-10-17' - Statement: - - Sid: Enable IAM User Permissions - Effect: Allow - Principal: - AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root' - Action: 'kms:*' - Resource: '*' - - Sid: Allow use of the key - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: - - kms:Decrypt - - kms:GenerateDataKey - Resource: '*' - - CustomerKeyAlias: - Type: AWS::KMS::Alias - Properties: - AliasName: alias/sqs-key - TargetKeyId: !Ref CustomerKey - - DemoDlqSqsQueue: - Type: AWS::SQS::Queue - Properties: - KmsMasterKeyId: !Ref CustomerKey - - DemoSqsQueue: - Type: AWS::SQS::Queue - Properties: - RedrivePolicy: - deadLetterTargetArn: - Fn::GetAtt: - - "DemoDlqSqsQueue" - - "Arn" - maxReceiveCount: 2 - KmsMasterKeyId: !Ref CustomerKey - - DemoSQSSenderFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: . - Handler: org.demo.sqs.SqsMessageSender::handleRequest - Environment: - Variables: - POWERTOOLS_SERVICE_NAME: sqs-demo - QUEUE_URL: !Ref DemoSqsQueue - Policies: - - Statement: - - Sid: SQSSendMessageBatch - Effect: Allow - Action: - - sqs:SendMessageBatch - - sqs:SendMessage - Resource: !GetAtt DemoSqsQueue.Arn - - Sid: SQSKMSKey - Effect: Allow - Action: - - kms:GenerateDataKey - - kms:Decrypt - Resource: !GetAtt CustomerKey.Arn - Events: - CWSchedule: - Type: Schedule - Properties: - Schedule: 'rate(5 minutes)' - Name: !Join ["-", ["message-producer-schedule", !Select [0, !Split [-, !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] - Description: Produce message to SQS via a Lambda function - Enabled: true - - DemoSQSConsumerFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: . - Handler: org.demo.sqs.SqsPoller::handleRequest - Environment: - Variables: - POWERTOOLS_SERVICE_NAME: sqs-demo - Policies: - - Statement: - - Sid: SQSDeleteGetAttribute - Effect: Allow - Action: - - sqs:DeleteMessageBatch - - sqs:GetQueueAttributes - Resource: !GetAtt DemoSqsQueue.Arn - - Sid: SQSSendMessageBatch - Effect: Allow - Action: - - sqs:SendMessageBatch - - sqs:SendMessage - Resource: !GetAtt DemoDlqSqsQueue.Arn - - Sid: SQSKMSKey - Effect: Allow - Action: - - kms:GenerateDataKey - - kms:Decrypt - Resource: !GetAtt CustomerKey.Arn - Events: - MySQSEvent: - Type: SQS - Properties: - Queue: !GetAtt DemoSqsQueue.Arn - BatchSize: 2 - MaximumBatchingWindowInSeconds: 300 - -Outputs: - DemoSqsQueue: - Description: "ARN for main SQS queue" - Value: !GetAtt DemoSqsQueue.Arn - DemoDlqSqsQueue: - Description: "ARN for DLQ" - Value: !GetAtt DemoDlqSqsQueue.Arn - DemoSQSSenderFunction: - Description: "Sender SQS Lambda Function ARN" - Value: !GetAtt DemoSQSSenderFunction.Arn - DemoSQSConsumerFunction: - Description: "Consumer SQS Lambda Function ARN" - Value: !GetAtt DemoSQSConsumerFunction.Arn - DemoSQSConsumerFunctionRole: - Description: "Implicit IAM Role created for SQS Lambda Function ARN" - Value: !GetAtt DemoSQSConsumerFunctionRole.Arn diff --git a/examples/powertools-examples-validation/README.md b/examples/powertools-examples-validation/README.md index 3f6790b0c..2f2028301 100644 --- a/examples/powertools-examples-validation/README.md +++ b/examples/powertools-examples-validation/README.md @@ -15,7 +15,7 @@ started with SAM in [the examples directory](../README.md) To test the validation, we can POST a JSON object shaped like our schema: ```bash - curl -X POST https://[REST-API-ID].execute-api.[REGION].amazonaws.com/Prod/hello/ -H "Content-Type: application/json" -d '{"address": "https://checkip.amazonaws.com"}' + curl -X POST https://[REST-API-ID].execute-api.[REGION].amazonaws.com/Prod/hello/ -H "Content-Type: application/json" -d '{"id": 123,"name":"The Hitchhikers Guide to the Galaxy","price":10.99}' ``` If we break the schema - for instance, by removing one of the compulsory fields, diff --git a/examples/powertools-examples-validation/pom.xml b/examples/powertools-examples-validation/pom.xml index 42d484ed1..8bed7cb5e 100644 --- a/examples/powertools-examples-validation/pom.xml +++ b/examples/powertools-examples-validation/pom.xml @@ -16,20 +16,21 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.lambda.examples - 1.19.0-SNAPSHOT + 2.0.0-RC1 powertools-examples-validation jar - Powertools for AWS Lambda (Java) library Examples - Validation + Powertools for AWS Lambda (Java) - Examples - Validation - 1.8 - 1.8 + 11 + 11 + 1.9.20.1 software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} @@ -42,6 +43,31 @@ aws-lambda-java-core 1.2.3 + + org.aspectj + aspectjrt + ${aspectj.version} + + + + + org.mockito + mockito-core + 4.11.0 + test + + + org.junit.jupiter + junit-jupiter-api + 5.11.1 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.3 + test + @@ -49,7 +75,7 @@ dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14 ${maven.compiler.source} ${maven.compiler.target} @@ -68,82 +94,23 @@ + + + org.aspectj + aspectjtools + ${aspectj.version} + + org.apache.maven.plugins maven-deploy-plugin + 3.1.2 true - - - - jdk8 - - (,11) - - - 1.9.7 - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - dev.aspectj - aspectj-maven-plugin - ${aspectj.plugin.version} - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-validation - - - - - - - compile - test-compile - - - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - - - - diff --git a/examples/spotbugs-exclude.xml b/examples/spotbugs-exclude.xml deleted file mode 100644 index e33e65478..000000000 --- a/examples/spotbugs-exclude.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index a271c1260..6f185584b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,11 +1,13 @@ -site_name: Powertools for AWS Lambda (Java) -site_description: Powertools for AWS Lambda (Java) +site_name: Powertools for AWS Lambda (Java) Preview +site_description: Powertools for AWS Lambda (Java) Preview site_author: Amazon Web Services -site_url: https://docs.powertools.aws.dev/lambda-java/ +site_url: https://docs.powertools.aws.dev/lambda/java/preview/ nav: - Homepage: index.md - Changelog: changelog.md + - Upgrade Guide: upgrade.md - FAQs: FAQs.md + - Roadmap: roadmap.md - Core utilities: - core/logging.md - core/tracing.md @@ -18,11 +20,12 @@ nav: - utilities/validation.md - utilities/custom_resources.md - utilities/serialization.md - - Deprecated: - - utilities/sqs_large_message_handling.md - - utilities/sqs_batch.md - Processes: - processes/maintainers.md + - "Versioning policy": processes/versioning.md + - Resources: + - "llms.txt": ./llms.txt + - "llms.txt (full version)": ./llms-full.txt theme: name: material @@ -60,8 +63,10 @@ markdown_extensions: alternate_style: true - pymdownx.details - pymdownx.snippets: - base_path: '.' + base_path: "." check_paths: true + - pymdownx.tasklist: + custom_checkbox: true - pymdownx.superfences: custom_fences: - name: mermaid @@ -79,6 +84,31 @@ plugins: - git-revision-date - search - macros + - privacy + - llmstxt: + markdown_description: Powertools for AWS Lambda (Java) is a developer toolkit to implement Serverless best practices and increase developer velocity. It provides a suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. + full_output: llms-full.txt + sections: + Project Overview: + - index.md + - changelog.md + - FAQs.md + - roadmap.md + Core Utilities: + - core/logging.md + - core/metrics.md + - core/tracing.md + Utilities: + - utilities/idempotency.md + - utilities/parameters.md + - utilities/large_messages.md + - utilities/batch.md + - utilities/validation.md + - utilities/custom_resources.md + - utilities/serialization.md + Processes: + - processes/maintainers.md + - processes/versioning.md extra_css: - stylesheets/extra.css @@ -88,7 +118,7 @@ extra_javascript: extra: powertools: - version: 1.18.0 # to update after each release (we do not want snapshot version here) + version: 2.0.0-RC1 repo_url: https://github.com/aws-powertools/powertools-lambda-java edit_uri: edit/main/docs diff --git a/pom.xml b/pom.xml index e3f5dad3b..af17d6b1b 100644 --- a/pom.xml +++ b/pom.xml @@ -14,19 +14,32 @@ --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 software.amazon.lambda powertools-parent - 1.19.0-SNAPSHOT + 2.0.0-RC1 pom - Powertools for AWS Lambda (Java) library Parent + Powertools for AWS Lambda (Java) - Parent A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. + + https://github.com/aws-powertools/powertools-lambda-java + scm:git:git://github.com/aws-powertools/powertools-lambda-java.git + scm:git:ssh://github.com:aws-powertools/powertools-lambda-java.git + + + + Powertools for AWS team + aws-powertools-maintainers@amazon.com + Amazon Web Services Inc. + https://aws.amazon.com + + https://aws.amazon.com/lambda/ GitHub Issues @@ -41,65 +54,76 @@ - powertools-core + powertools-common powertools-serialization powertools-logging + powertools-logging/powertools-logging-log4j + powertools-logging/powertools-logging-logback powertools-tracing - powertools-sqs powertools-metrics powertools-parameters powertools-validation - powertools-test-suite powertools-cloudformation powertools-idempotency powertools-large-messages powertools-e2e-tests powertools-batch + powertools-parameters/powertools-parameters-ssm + powertools-parameters/powertools-parameters-secrets + powertools-parameters/powertools-parameters-dynamodb + powertools-parameters/powertools-parameters-appconfig + powertools-parameters/powertools-parameters-tests examples - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - - Powertools for AWS Lambda (Java) team - Amazon Web Services - https://aws.amazon.com/ - - - - 1.8 - 1.8 - 2.22.1 - 2.15.3 - 2.25.6 - 2.15.1 - 2.1.3 + 11 + 11 + 3.1.2 + 2.24.3 + 2.0.7 + 2.17.2 + 2.31.40 + 2.18.2 + 2.2.0 UTF-8 1.2.3 - 3.11.4 + 3.15.0 1.1.5 - 3.11.0 + 3.13.0 1.9.7 1.13.1 3.2.5 0.8.11 1.6.13 - 3.6.0 - 3.3.0 - 3.1.0 + 3.11.2 + 3.3.1 + 3.2.1 5.10.0 - 1.0.6 + 1.14 + 3.1.2 + 0.8.12 + 1.7.0 + 3.11.2 + 3.3.0 + 3.2.7 + 5.10.2 + 4.1.2 0.6.0 + 1.12.781 + 2.18.0 + 1.6.0 + 5.17.0 + + + ${maven.multiModuleProjectDirectory} - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots + central + https://central.sonatype.com/repository/maven-snapshots/ @@ -107,7 +131,7 @@ software.amazon.lambda - powertools-core + powertools-common ${project.version} @@ -120,6 +144,16 @@ powertools-logging ${project.version} + + software.amazon.lambda + powertools-logging-log4j + ${project.version} + + + software.amazon.lambda + powertools-logging-logback + ${project.version} + software.amazon.lambda powertools-sqs @@ -197,6 +231,11 @@ log4j-core ${log4j.version} + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + org.apache.logging.log4j log4j-slf4j2-impl @@ -217,6 +256,16 @@ log4j-jcl ${log4j.version} + + co.elastic.logging + logback-ecs-encoder + ${elastic.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + com.amazonaws aws-xray-recorder-sdk-core @@ -242,6 +291,11 @@ aws-embedded-metrics ${aws-embedded-metrics.version} + + org.apache.commons + commons-lang3 + 3.15.0 + @@ -257,12 +311,6 @@ 1.9.1 test - - org.apache.commons - commons-lang3 - 3.13.0 - test - org.aspectj aspectjweaver @@ -272,7 +320,7 @@ org.assertj assertj-core - 3.24.2 + 3.26.3 test @@ -281,10 +329,16 @@ + + org.slf4j + slf4j-simple + ${slf4j.version} + test + org.skyscreamer jsonassert - 1.5.1 + 1.5.3 test @@ -292,6 +346,24 @@ aspectjtools ${aspectj.version} + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + org.mockito + mockito-subclass + ${mockito.version} + test + com.amazonaws aws-lambda-java-tests @@ -301,7 +373,7 @@ com.github.tomakehurst wiremock-jre8 - 2.35.1 + 2.35.2 test @@ -310,6 +382,14 @@ + + org.codehaus.mojo + versions-maven-plugin + ${versions-maven-plugin.version} + + false + + org.apache.maven.plugins maven-compiler-plugin @@ -365,7 +445,7 @@ org.apache.maven.plugins maven-artifact-plugin - 3.5.0 + 3.6.0 true @@ -383,7 +463,6 @@ - process-sources compile test-compile @@ -422,13 +501,12 @@ - org.sonatype.plugins - nexus-staging-maven-plugin + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 true - ossrh - https://aws.oss.sonatype.org - true + central @@ -499,7 +577,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.7.3.6 + 4.8.4.0 test @@ -510,13 +588,12 @@ true - ../spotbugs-exclude.xml + ${project.rootdir}/spotbugs-exclude.xml - jdk16 @@ -527,7 +604,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.5 + 3.1.2 @{argLine} @@ -539,101 +616,6 @@ - - olderThanJdk11 - - (,11) - - - - 4.11.0 - - - - org.mockito - mockito-core - ${mockito.version} - test - - - org.mockito - mockito-inline - ${mockito.version} - test - - - - - newerThanJdk11 - - [11,) - - - 5.6.0 - - - - - org.mockito - mockito-core - ${mockito.version} - test - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - - true - - - - - - - - newerThanJdk8 - - [9,) - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.3.0 - - checkstyle.xml - UTF-8 - true - true - false - - - - - - com.puppycrawl.tools - checkstyle - 10.12.3 - - - - - - check - - - - - - - diff --git a/powertools-batch/pom.xml b/powertools-batch/pom.xml index fabaeba30..20dbd179b 100644 --- a/powertools-batch/pom.xml +++ b/powertools-batch/pom.xml @@ -6,11 +6,11 @@ software.amazon.lambda powertools-parent - 1.19.0-SNAPSHOT + 2.0.0-RC1 A suite of utilities that makes batch message processing using AWS Lambda easier. - Powertools for AWS Lambda (Java) batch messages + Powertools for AWS Lambda (Java) - Batch messages @@ -21,10 +21,21 @@ true + + org.apache.maven.plugins + maven-surefire-plugin + + + + -Djava.util.concurrent.ForkJoinPool.common.parallelism=4 + + + powertools-batch + com.amazonaws @@ -46,6 +57,12 @@ junit-jupiter-api test + + org.slf4j + slf4j-simple + 2.0.7 + test + org.assertj assertj-core @@ -56,6 +73,16 @@ aws-lambda-java-tests test + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + \ No newline at end of file diff --git a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/BatchMessageHandler.java b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/BatchMessageHandler.java index 730211feb..18d74bb25 100644 --- a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/BatchMessageHandler.java +++ b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/BatchMessageHandler.java @@ -33,6 +33,21 @@ public interface BatchMessageHandler { * @param context The lambda context * @return A partial batch response */ - public abstract R processBatch(E event, Context context); + R processBatch(E event, Context context); + /** + * Processes the given batch in parallel returning a partial batch + * response indicating the success and failure of individual + * messages within the batch.
+ * Note that parallel processing is not always better than sequential processing, + * and you should benchmark your code to determine the best approach for your use case.
+ * Also note that to get more threads available (more vCPUs), + * you need to increase the amount of memory allocated to your Lambda function.
+ + * + * @param event The Lambda event containing the batch to process + * @param context The lambda context + * @return A partial batch response + */ + R processBatchInParallel(E event, Context context); } diff --git a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/DynamoDbBatchMessageHandler.java b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/DynamoDbBatchMessageHandler.java index aa6eba839..4b03d0947 100644 --- a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/DynamoDbBatchMessageHandler.java +++ b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/DynamoDbBatchMessageHandler.java @@ -17,12 +17,14 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; -import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.batch.internal.MultiThreadMDC; /** * A batch message processor for DynamoDB Streams batches. @@ -30,7 +32,7 @@ * @see DynamoDB Streams batch failure reporting */ public class DynamoDbBatchMessageHandler implements BatchMessageHandler { - private final static Logger LOGGER = LoggerFactory.getLogger(DynamoDbBatchMessageHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDbBatchMessageHandler.class); private final Consumer successHandler; private final BiConsumer failureHandler; @@ -46,35 +48,60 @@ public DynamoDbBatchMessageHandler(Consumer @Override public StreamsEventResponse processBatch(DynamodbEvent event, Context context) { - List batchFailures = new ArrayList<>(); + List batchItemFailures = event.getRecords() + .stream() + .map(eventRecord -> processBatchItem(eventRecord, context)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); - for (DynamodbEvent.DynamodbStreamRecord record : event.getRecords()) { - try { + return StreamsEventResponse.builder().withBatchItemFailures(batchItemFailures).build(); + } - rawMessageHandler.accept(record, context); - // Report success if we have a handler - if (this.successHandler != null) { - this.successHandler.accept(record); - } - } catch (Throwable t) { - String sequenceNumber = record.getDynamodb().getSequenceNumber(); - LOGGER.error("Error while processing record with id {}: {}, adding it to batch item failures", - sequenceNumber, t.getMessage()); - LOGGER.error("Error was", t); - batchFailures.add(new StreamsEventResponse.BatchItemFailure(sequenceNumber)); + @Override + public StreamsEventResponse processBatchInParallel(DynamodbEvent event, Context context) { + MultiThreadMDC multiThreadMDC = new MultiThreadMDC(); + + List batchItemFailures = event.getRecords() + .parallelStream() // Parallel processing + .map(eventRecord -> { + multiThreadMDC.copyMDCToThread(Thread.currentThread().getName()); + return processBatchItem(eventRecord, context); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + + return StreamsEventResponse.builder().withBatchItemFailures(batchItemFailures).build(); + } - // Report failure if we have a handler - if (this.failureHandler != null) { - // A failing failure handler is no reason to fail the batch - try { - this.failureHandler.accept(record, t); - } catch (Throwable t2) { - LOGGER.warn("failureHandler threw handling failure", t2); - } + private Optional processBatchItem(DynamodbEvent.DynamodbStreamRecord streamRecord, Context context) { + try { + LOGGER.debug("Processing item {}", streamRecord.getEventID()); + + rawMessageHandler.accept(streamRecord, context); + + // Report success if we have a handler + if (this.successHandler != null) { + this.successHandler.accept(streamRecord); + } + return Optional.empty(); + } catch (Throwable t) { + String sequenceNumber = streamRecord.getDynamodb().getSequenceNumber(); + LOGGER.error("Error while processing record with id {}: {}, adding it to batch item failures", + sequenceNumber, t.getMessage()); + LOGGER.error("Error was", t); + + // Report failure if we have a handler + if (this.failureHandler != null) { + // A failing failure handler is no reason to fail the batch + try { + this.failureHandler.accept(streamRecord, t); + } catch (Throwable t2) { + LOGGER.warn("failureHandler threw handling failure", t2); } } + return Optional.of(StreamsEventResponse.BatchItemFailure.builder().withItemIdentifier(sequenceNumber).build()); } - - return new StreamsEventResponse(batchFailures); } } diff --git a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/KinesisStreamsBatchMessageHandler.java b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/KinesisStreamsBatchMessageHandler.java index fe1aaf354..7b4179de7 100644 --- a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/KinesisStreamsBatchMessageHandler.java +++ b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/KinesisStreamsBatchMessageHandler.java @@ -18,12 +18,14 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.KinesisEvent; import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; -import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.batch.internal.MultiThreadMDC; import software.amazon.lambda.powertools.utilities.EventDeserializer; /** @@ -34,7 +36,7 @@ * @param The user-defined type of the Kinesis record payload */ public class KinesisStreamsBatchMessageHandler implements BatchMessageHandler { - private final static Logger LOGGER = LoggerFactory.getLogger(KinesisStreamsBatchMessageHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(KinesisStreamsBatchMessageHandler.class); private final BiConsumer rawMessageHandler; private final BiConsumer messageHandler; @@ -57,42 +59,67 @@ public KinesisStreamsBatchMessageHandler(BiConsumer batchFailures = new ArrayList<>(); - - for (KinesisEvent.KinesisEventRecord record : event.getRecords()) { - try { - if (this.rawMessageHandler != null) { - rawMessageHandler.accept(record, context); - } else { - M messageDeserialized = EventDeserializer.extractDataFrom(record).as(messageClass); - messageHandler.accept(messageDeserialized, context); - } + List batchItemFailures = event.getRecords() + .stream() + .map(eventRecord -> processBatchItem(eventRecord, context)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); - // Report success if we have a handler - if (this.successHandler != null) { - this.successHandler.accept(record); - } - } catch (Throwable t) { - String sequenceNumber = record.getEventID(); - LOGGER.error("Error while processing record with eventID {}: {}, adding it to batch item failures", - sequenceNumber, t.getMessage()); - LOGGER.error("Error was", t); - - batchFailures.add(new StreamsEventResponse.BatchItemFailure(record.getKinesis().getSequenceNumber())); - - // Report failure if we have a handler - if (this.failureHandler != null) { - // A failing failure handler is no reason to fail the batch - try { - this.failureHandler.accept(record, t); - } catch (Throwable t2) { - LOGGER.warn("failureHandler threw handling failure", t2); - } + return StreamsEventResponse.builder().withBatchItemFailures(batchItemFailures).build(); + } + + @Override + public StreamsEventResponse processBatchInParallel(KinesisEvent event, Context context) { + MultiThreadMDC multiThreadMDC = new MultiThreadMDC(); + + List batchItemFailures = event.getRecords() + .parallelStream() // Parallel processing + .map(eventRecord -> { + multiThreadMDC.copyMDCToThread(Thread.currentThread().getName()); + return processBatchItem(eventRecord, context); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + + return StreamsEventResponse.builder().withBatchItemFailures(batchItemFailures).build(); + } + + private Optional processBatchItem(KinesisEvent.KinesisEventRecord eventRecord, Context context) { + try { + LOGGER.debug("Processing item {}", eventRecord.getEventID()); + + if (this.rawMessageHandler != null) { + rawMessageHandler.accept(eventRecord, context); + } else { + M messageDeserialized = EventDeserializer.extractDataFrom(eventRecord).as(messageClass); + messageHandler.accept(messageDeserialized, context); + } + + // Report success if we have a handler + if (this.successHandler != null) { + this.successHandler.accept(eventRecord); + } + return Optional.empty(); + } catch (Throwable t) { + String sequenceNumber = eventRecord.getEventID(); + LOGGER.error("Error while processing record with eventID {}: {}, adding it to batch item failures", + sequenceNumber, t.getMessage()); + LOGGER.error("Error was", t); + + // Report failure if we have a handler + if (this.failureHandler != null) { + // A failing failure handler is no reason to fail the batch + try { + this.failureHandler.accept(eventRecord, t); + } catch (Throwable t2) { + LOGGER.warn("failureHandler threw handling failure", t2); } } - } - return new StreamsEventResponse(batchFailures); + return Optional.of(StreamsEventResponse.BatchItemFailure.builder().withItemIdentifier(eventRecord.getKinesis().getSequenceNumber()).build()); + } } } diff --git a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/SqsBatchMessageHandler.java b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/SqsBatchMessageHandler.java index b3c416a69..2dfb0a28e 100644 --- a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/SqsBatchMessageHandler.java +++ b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/SqsBatchMessageHandler.java @@ -18,10 +18,15 @@ import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; import com.amazonaws.services.lambda.runtime.events.SQSEvent; import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.batch.internal.MultiThreadMDC; import software.amazon.lambda.powertools.utilities.EventDeserializer; /** @@ -31,11 +36,11 @@ * @see SQS Batch failure reporting */ public class SqsBatchMessageHandler implements BatchMessageHandler { - private final static Logger LOGGER = LoggerFactory.getLogger(SqsBatchMessageHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(SqsBatchMessageHandler.class); // The attribute on an SQS-FIFO message used to record the message group ID // https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#sample-fifo-queues-message-event - private final static String MESSAGE_GROUP_ID_KEY = "MessageGroupId"; + private static final String MESSAGE_GROUP_ID_KEY = "MessageGroupId"; private final Class messageClass; private final BiConsumer messageHandler; @@ -61,57 +66,27 @@ public SQSBatchResponse processBatch(SQSEvent event, Context context) { // If we are working on a FIFO queue, when any message fails we should stop processing and return the // rest of the batch as failed too. We use this variable to track when that has happened. // https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting - boolean failWholeBatch = false; + final AtomicBoolean failWholeBatch = new AtomicBoolean(false); int messageCursor = 0; - for (; messageCursor < event.getRecords().size() && !failWholeBatch; messageCursor++) { + for (; messageCursor < event.getRecords().size() && !failWholeBatch.get(); messageCursor++) { SQSEvent.SQSMessage message = event.getRecords().get(messageCursor); String messageGroupId = message.getAttributes() != null ? message.getAttributes().get(MESSAGE_GROUP_ID_KEY) : null; - try { - if (this.rawMessageHandler != null) { - rawMessageHandler.accept(message, context); - } else { - M messageDeserialized = EventDeserializer.extractDataFrom(message).as(messageClass); - messageHandler.accept(messageDeserialized, context); - } - - // Report success if we have a handler - if (this.successHandler != null) { - this.successHandler.accept(message); - } - - } catch (Throwable t) { - LOGGER.error("Error while processing message with messageId {}: {}, adding it to batch item failures", - message.getMessageId(), t.getMessage()); - LOGGER.error("Error was", t); - - response.getBatchItemFailures() - .add(SQSBatchResponse.BatchItemFailure.builder().withItemIdentifier(message.getMessageId()) - .build()); + processBatchItem(message, context).ifPresent(batchItemFailure -> { + response.getBatchItemFailures().add(batchItemFailure); if (messageGroupId != null) { - failWholeBatch = true; + failWholeBatch.set(true); LOGGER.info( "A message in a batch with messageGroupId {} and messageId {} failed; failing the rest of the batch too" , messageGroupId, message.getMessageId()); } - - // Report failure if we have a handler - if (this.failureHandler != null) { - // A failing failure handler is no reason to fail the batch - try { - this.failureHandler.accept(message, t); - } catch (Throwable t2) { - LOGGER.warn("failureHandler threw handling failure", t2); - } - } - - } + }); } - if (failWholeBatch) { + if (failWholeBatch.get()) { // Add the remaining messages to the batch item failures event.getRecords() .subList(messageCursor, event.getRecords().size()) @@ -121,4 +96,60 @@ public SQSBatchResponse processBatch(SQSEvent event, Context context) { } return response; } + + @Override + public SQSBatchResponse processBatchInParallel(SQSEvent event, Context context) { + if (!event.getRecords().isEmpty() && event.getRecords().get(0).getAttributes().get(MESSAGE_GROUP_ID_KEY) != null) { + throw new UnsupportedOperationException("FIFO queues are not supported in parallel mode, use the processBatch method instead"); + } + + MultiThreadMDC multiThreadMDC = new MultiThreadMDC(); + List batchItemFailures = event.getRecords() + .parallelStream() // Parallel processing + .map(sqsMessage -> { + + multiThreadMDC.copyMDCToThread(Thread.currentThread().getName()); + return processBatchItem(sqsMessage, context); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + + return SQSBatchResponse.builder().withBatchItemFailures(batchItemFailures).build(); + } + + private Optional processBatchItem(SQSEvent.SQSMessage message, Context context) { + try { + LOGGER.debug("Processing message {}", message.getMessageId()); + + if (this.rawMessageHandler != null) { + rawMessageHandler.accept(message, context); + } else { + M messageDeserialized = EventDeserializer.extractDataFrom(message).as(messageClass); + messageHandler.accept(messageDeserialized, context); + } + + // Report success if we have a handler + if (this.successHandler != null) { + this.successHandler.accept(message); + } + return Optional.empty(); + } catch (Throwable t) { + LOGGER.error("Error while processing message with messageId {}: {}, adding it to batch item failures", + message.getMessageId(), t.getMessage()); + LOGGER.error("Error was", t); + + // Report failure if we have a handler + if (this.failureHandler != null) { + // A failing failure handler is no reason to fail the batch + try { + this.failureHandler.accept(message, t); + } catch (Throwable t2) { + LOGGER.warn("failureHandler threw handling failure", t2); + } + } + return Optional.of(SQSBatchResponse.BatchItemFailure.builder().withItemIdentifier(message.getMessageId()) + .build()); + } + } } diff --git a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/internal/MultiThreadMDC.java b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/internal/MultiThreadMDC.java new file mode 100644 index 000000000..df1c2e7a0 --- /dev/null +++ b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/internal/MultiThreadMDC.java @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.batch.internal; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +/** + * MDC (SLF4J) is not passed to other threads (ThreadLocal). + * This class permits to manually copy the MDC to a given thread. + */ +public class MultiThreadMDC { + + private static final Logger LOGGER = LoggerFactory.getLogger(MultiThreadMDC.class); + + private final List mdcAwareThreads = new ArrayList<>(); + private final Map contextMap; + + public MultiThreadMDC() { + mdcAwareThreads.add("main"); + contextMap = MDC.getCopyOfContextMap(); + } + + public void copyMDCToThread(String thread) { + if (!mdcAwareThreads.contains(thread)) { + LOGGER.debug("Copy MDC to thread {}", thread); + MDC.setContextMap(contextMap); + mdcAwareThreads.add(thread); + } + } +} diff --git a/powertools-batch/src/test/java/software/amazon/lambda/powertools/batch/DdbBatchProcessorTest.java b/powertools-batch/src/test/java/software/amazon/lambda/powertools/batch/DdbBatchProcessorTest.java index 9e2c211e2..6bb247323 100644 --- a/powertools-batch/src/test/java/software/amazon/lambda/powertools/batch/DdbBatchProcessorTest.java +++ b/powertools-batch/src/test/java/software/amazon/lambda/powertools/batch/DdbBatchProcessorTest.java @@ -20,16 +20,27 @@ import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.mockito.Mock; import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; -public class DdbBatchProcessorTest { +class DdbBatchProcessorTest { @Mock private Context context; + private final List threadList = Collections.synchronizedList(new ArrayList<>()); + + @AfterEach + public void clear() { + threadList.clear(); + } + private void processMessageSucceeds(DynamodbEvent.DynamodbStreamRecord record, Context context) { // Great success } @@ -40,9 +51,36 @@ private void processMessageFailsForFixedMessage(DynamodbEvent.DynamodbStreamReco } } + private void processMessageInParallelSucceeds(DynamodbEvent.DynamodbStreamRecord record, Context context) { + String thread = Thread.currentThread().getName(); + if (!threadList.contains(thread)) { + threadList.add(thread); + } + try { + Thread.sleep(500); // simulate some processing + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void processMessageInParallelFailsForFixedMessage(DynamodbEvent.DynamodbStreamRecord record, Context context) { + String thread = Thread.currentThread().getName(); + if (!threadList.contains(thread)) { + threadList.add(thread); + } + try { + Thread.sleep(500); // simulate some processing + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (record.getDynamodb().getSequenceNumber().equals("4421584500000000017450439091")) { + throw new RuntimeException("fake exception"); + } + } + @ParameterizedTest @Event(value = "dynamo_event.json", type = DynamodbEvent.class) - public void batchProcessingSucceedsAndReturns(DynamodbEvent event) { + void batchProcessingSucceedsAndReturns(DynamodbEvent event) { // Arrange BatchMessageHandler handler = new BatchMessageHandlerBuilder() .withDynamoDbBatchHandler() @@ -52,12 +90,28 @@ public void batchProcessingSucceedsAndReturns(DynamodbEvent event) { StreamsEventResponse dynamodbBatchResponse = handler.processBatch(event, context); // Assert - assertThat(dynamodbBatchResponse.getBatchItemFailures()).hasSize(0); + assertThat(dynamodbBatchResponse.getBatchItemFailures()).isEmpty(); + } + + @ParameterizedTest + @Event(value = "dynamo_event_big.json", type = DynamodbEvent.class) + void parallelBatchProcessingSucceedsAndReturns(DynamodbEvent event) { + // Arrange + BatchMessageHandler handler = new BatchMessageHandlerBuilder() + .withDynamoDbBatchHandler() + .buildWithRawMessageHandler(this::processMessageInParallelSucceeds); + + // Act + StreamsEventResponse dynamodbBatchResponse = handler.processBatchInParallel(event, context); + + // Assert + assertThat(dynamodbBatchResponse.getBatchItemFailures()).isEmpty(); + assertThat(threadList).hasSizeGreaterThan(1); } @ParameterizedTest @Event(value = "dynamo_event.json", type = DynamodbEvent.class) - public void shouldAddMessageToBatchFailure_whenException_withMessage(DynamodbEvent event) { + void shouldAddMessageToBatchFailure_whenException_withMessage(DynamodbEvent event) { // Arrange BatchMessageHandler handler = new BatchMessageHandlerBuilder() .withDynamoDbBatchHandler() @@ -72,9 +126,27 @@ public void shouldAddMessageToBatchFailure_whenException_withMessage(DynamodbEve assertThat(batchItemFailure.getItemIdentifier()).isEqualTo("4421584500000000017450439091"); } + @ParameterizedTest + @Event(value = "dynamo_event_big.json", type = DynamodbEvent.class) + void parallelBatchProcessing_shouldAddMessageToBatchFailure_whenException_withMessage(DynamodbEvent event) { + // Arrange + BatchMessageHandler handler = new BatchMessageHandlerBuilder() + .withDynamoDbBatchHandler() + .buildWithRawMessageHandler(this::processMessageInParallelFailsForFixedMessage); + + // Act + StreamsEventResponse dynamodbBatchResponse = handler.processBatchInParallel(event, context); + + // Assert + assertThat(dynamodbBatchResponse.getBatchItemFailures()).hasSize(1); + StreamsEventResponse.BatchItemFailure batchItemFailure = dynamodbBatchResponse.getBatchItemFailures().get(0); + assertThat(batchItemFailure.getItemIdentifier()).isEqualTo("4421584500000000017450439091"); + assertThat(threadList).hasSizeGreaterThan(1); + } + @ParameterizedTest @Event(value = "dynamo_event.json", type = DynamodbEvent.class) - public void failingFailureHandlerShouldntFailBatch(DynamodbEvent event) { + void failingFailureHandlerShouldntFailBatch(DynamodbEvent event) { // Arrange AtomicBoolean wasCalledAndFailed = new AtomicBoolean(false); BatchMessageHandler handler = new BatchMessageHandlerBuilder() @@ -92,7 +164,7 @@ public void failingFailureHandlerShouldntFailBatch(DynamodbEvent event) { // Assert assertThat(dynamodbBatchResponse).isNotNull(); - assertThat(dynamodbBatchResponse.getBatchItemFailures().size()).isEqualTo(1); + assertThat(dynamodbBatchResponse.getBatchItemFailures()).hasSize(1); assertThat(wasCalledAndFailed.get()).isTrue(); StreamsEventResponse.BatchItemFailure batchItemFailure = dynamodbBatchResponse.getBatchItemFailures().get(0); assertThat(batchItemFailure.getItemIdentifier()).isEqualTo("4421584500000000017450439091"); @@ -100,7 +172,7 @@ public void failingFailureHandlerShouldntFailBatch(DynamodbEvent event) { @ParameterizedTest @Event(value = "dynamo_event.json", type = DynamodbEvent.class) - public void failingSuccessHandlerShouldntFailBatchButShouldFailMessage(DynamodbEvent event) { + void failingSuccessHandlerShouldntFailBatchButShouldFailMessage(DynamodbEvent event) { // Arrange AtomicBoolean wasCalledAndFailed = new AtomicBoolean(false); BatchMessageHandler handler = new BatchMessageHandlerBuilder() @@ -118,7 +190,7 @@ public void failingSuccessHandlerShouldntFailBatchButShouldFailMessage(DynamodbE // Assert assertThat(dynamodbBatchResponse).isNotNull(); - assertThat(dynamodbBatchResponse.getBatchItemFailures().size()).isEqualTo(1); + assertThat(dynamodbBatchResponse.getBatchItemFailures()).hasSize(1); assertThat(wasCalledAndFailed.get()).isTrue(); StreamsEventResponse.BatchItemFailure batchItemFailure = dynamodbBatchResponse.getBatchItemFailures().get(0); assertThat(batchItemFailure.getItemIdentifier()).isEqualTo("4421584500000000017450439091"); diff --git a/powertools-batch/src/test/java/software/amazon/lambda/powertools/batch/KinesisBatchProcessorTest.java b/powertools-batch/src/test/java/software/amazon/lambda/powertools/batch/KinesisBatchProcessorTest.java index d78638e1d..059a4d2d0 100644 --- a/powertools-batch/src/test/java/software/amazon/lambda/powertools/batch/KinesisBatchProcessorTest.java +++ b/powertools-batch/src/test/java/software/amazon/lambda/powertools/batch/KinesisBatchProcessorTest.java @@ -20,17 +20,28 @@ import com.amazonaws.services.lambda.runtime.events.KinesisEvent; import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.mockito.Mock; import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; import software.amazon.lambda.powertools.batch.model.Product; -public class KinesisBatchProcessorTest { +class KinesisBatchProcessorTest { @Mock private Context context; + private final List threadList = Collections.synchronizedList(new ArrayList<>()); + + @AfterEach + public void clear() { + threadList.clear(); + } + private void processMessageSucceeds(KinesisEvent.KinesisEventRecord record, Context context) { // Great success } @@ -42,6 +53,34 @@ private void processMessageFailsForFixedMessage(KinesisEvent.KinesisEventRecord } } + private void processMessageInParallelSucceeds(KinesisEvent.KinesisEventRecord record, Context context) { + String thread = Thread.currentThread().getName(); + if (!threadList.contains(thread)) { + threadList.add(thread); + } + try { + Thread.sleep(500); // simulate some processing + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void processMessageInParallelFailsForFixedMessage(KinesisEvent.KinesisEventRecord record, Context context) { + String thread = Thread.currentThread().getName(); + if (!threadList.contains(thread)) { + threadList.add(thread); + } + try { + Thread.sleep(500); // simulate some processing + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (record.getKinesis().getSequenceNumber() + .equals("49545115243490985018280067714973144582180062593244200961")) { + throw new RuntimeException("fake exception"); + } + } + // A handler that throws an exception for _one_ of the deserialized products in the same messages public void processMessageFailsForFixedProduct(Product product, Context context) { if (product.getId() == 1234) { @@ -51,7 +90,7 @@ public void processMessageFailsForFixedProduct(Product product, Context context) @ParameterizedTest @Event(value = "kinesis_event.json", type = KinesisEvent.class) - public void batchProcessingSucceedsAndReturns(KinesisEvent event) { + void batchProcessingSucceedsAndReturns(KinesisEvent event) { // Arrange BatchMessageHandler handler = new BatchMessageHandlerBuilder() .withKinesisBatchHandler() @@ -61,12 +100,28 @@ public void batchProcessingSucceedsAndReturns(KinesisEvent event) { StreamsEventResponse kinesisBatchResponse = handler.processBatch(event, context); // Assert - assertThat(kinesisBatchResponse.getBatchItemFailures()).hasSize(0); + assertThat(kinesisBatchResponse.getBatchItemFailures()).isEmpty(); + } + + @ParameterizedTest + @Event(value = "kinesis_event_big.json", type = KinesisEvent.class) + void batchProcessingInParallelSucceedsAndReturns(KinesisEvent event) { + // Arrange + BatchMessageHandler handler = new BatchMessageHandlerBuilder() + .withKinesisBatchHandler() + .buildWithRawMessageHandler(this::processMessageInParallelSucceeds); + + // Act + StreamsEventResponse kinesisBatchResponse = handler.processBatchInParallel(event, context); + + // Assert + assertThat(kinesisBatchResponse.getBatchItemFailures()).isEmpty(); + assertThat(threadList).hasSizeGreaterThan(1); } @ParameterizedTest @Event(value = "kinesis_event.json", type = KinesisEvent.class) - public void shouldAddMessageToBatchFailure_whenException_withMessage(KinesisEvent event) { + void shouldAddMessageToBatchFailure_whenException_withMessage(KinesisEvent event) { // Arrange BatchMessageHandler handler = new BatchMessageHandlerBuilder() .withKinesisBatchHandler() @@ -82,9 +137,28 @@ public void shouldAddMessageToBatchFailure_whenException_withMessage(KinesisEven "49545115243490985018280067714973144582180062593244200961"); } + @ParameterizedTest + @Event(value = "kinesis_event_big.json", type = KinesisEvent.class) + void batchProcessingInParallel_shouldAddMessageToBatchFailure_whenException_withMessage(KinesisEvent event) { + // Arrange + BatchMessageHandler handler = new BatchMessageHandlerBuilder() + .withKinesisBatchHandler() + .buildWithRawMessageHandler(this::processMessageInParallelFailsForFixedMessage); + + // Act + StreamsEventResponse kinesisBatchResponse = handler.processBatchInParallel(event, context); + + // Assert + assertThat(kinesisBatchResponse.getBatchItemFailures()).hasSize(1); + StreamsEventResponse.BatchItemFailure batchItemFailure = kinesisBatchResponse.getBatchItemFailures().get(0); + assertThat(batchItemFailure.getItemIdentifier()).isEqualTo( + "49545115243490985018280067714973144582180062593244200961"); + assertThat(threadList).hasSizeGreaterThan(1); + } + @ParameterizedTest @Event(value = "kinesis_event.json", type = KinesisEvent.class) - public void shouldAddMessageToBatchFailure_whenException_withProduct(KinesisEvent event) { + void shouldAddMessageToBatchFailure_whenException_withProduct(KinesisEvent event) { // Arrange BatchMessageHandler handler = new BatchMessageHandlerBuilder() .withKinesisBatchHandler() @@ -102,7 +176,7 @@ public void shouldAddMessageToBatchFailure_whenException_withProduct(KinesisEven @ParameterizedTest @Event(value = "kinesis_event.json", type = KinesisEvent.class) - public void failingFailureHandlerShouldntFailBatch(KinesisEvent event) { + void failingFailureHandlerShouldntFailBatch(KinesisEvent event) { // Arrange AtomicBoolean wasCalled = new AtomicBoolean(false); BatchMessageHandler handler = new BatchMessageHandlerBuilder() @@ -118,7 +192,7 @@ public void failingFailureHandlerShouldntFailBatch(KinesisEvent event) { // Assert assertThat(kinesisBatchResponse).isNotNull(); - assertThat(kinesisBatchResponse.getBatchItemFailures().size()).isEqualTo(1); + assertThat(kinesisBatchResponse.getBatchItemFailures()).hasSize(1); assertThat(wasCalled.get()).isTrue(); StreamsEventResponse.BatchItemFailure batchItemFailure = kinesisBatchResponse.getBatchItemFailures().get(0); assertThat(batchItemFailure.getItemIdentifier()).isEqualTo( @@ -127,7 +201,7 @@ public void failingFailureHandlerShouldntFailBatch(KinesisEvent event) { @ParameterizedTest @Event(value = "kinesis_event.json", type = KinesisEvent.class) - public void failingSuccessHandlerShouldntFailBatchButShouldFailMessage(KinesisEvent event) { + void failingSuccessHandlerShouldntFailBatchButShouldFailMessage(KinesisEvent event) { // Arrange AtomicBoolean wasCalledAndFailed = new AtomicBoolean(false); BatchMessageHandler handler = new BatchMessageHandlerBuilder() @@ -146,7 +220,7 @@ public void failingSuccessHandlerShouldntFailBatchButShouldFailMessage(KinesisEv // Assert assertThat(kinesisBatchResponse).isNotNull(); - assertThat(kinesisBatchResponse.getBatchItemFailures().size()).isEqualTo(1); + assertThat(kinesisBatchResponse.getBatchItemFailures()).hasSize(1); assertThat(wasCalledAndFailed.get()).isTrue(); StreamsEventResponse.BatchItemFailure batchItemFailure = kinesisBatchResponse.getBatchItemFailures().get(0); assertThat(batchItemFailure.getItemIdentifier()).isEqualTo( diff --git a/powertools-batch/src/test/java/software/amazon/lambda/powertools/batch/SQSBatchProcessorTest.java b/powertools-batch/src/test/java/software/amazon/lambda/powertools/batch/SQSBatchProcessorTest.java index 2f9429fa3..7dd51374e 100644 --- a/powertools-batch/src/test/java/software/amazon/lambda/powertools/batch/SQSBatchProcessorTest.java +++ b/powertools-batch/src/test/java/software/amazon/lambda/powertools/batch/SQSBatchProcessorTest.java @@ -20,20 +20,43 @@ import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; import com.amazonaws.services.lambda.runtime.events.SQSEvent; import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.mockito.Mock; import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; import software.amazon.lambda.powertools.batch.model.Product; -public class SQSBatchProcessorTest { +class SQSBatchProcessorTest { @Mock private Context context; + private final List threadList = Collections.synchronizedList(new ArrayList<>()); + + @AfterEach + public void clear() { + threadList.clear(); + } + // A handler that works private void processMessageSucceeds(SQSEvent.SQSMessage sqsMessage) { } + private void processMessageInParallelSucceeds(SQSEvent.SQSMessage sqsMessage) { + String thread = Thread.currentThread().getName(); + if (!threadList.contains(thread)) { + threadList.add(thread); + } + try { + Thread.sleep(500); // simulate some processing + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + // A handler that throws an exception for _one_ of the sample messages private void processMessageFailsForFixedMessage(SQSEvent.SQSMessage message, Context context) { if (message.getMessageId().equals("e9144555-9a4f-4ec3-99a0-34ce359b4b54")) { @@ -41,8 +64,23 @@ private void processMessageFailsForFixedMessage(SQSEvent.SQSMessage message, Con } } + private void processMessageInParallelFailsForFixedMessage(SQSEvent.SQSMessage message, Context context) { + String thread = Thread.currentThread().getName(); + if (!threadList.contains(thread)) { + threadList.add(thread); + } + try { + Thread.sleep(500); // simulate some processing + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (message.getMessageId().equals("e9144555-9a4f-4ec3-99a0-34ce359b4b54")) { + throw new RuntimeException("fake exception"); + } + } + // A handler that throws an exception for _one_ of the deserialized products in the same messages - public void processMessageFailsForFixedProduct(Product product, Context context) { + private void processMessageFailsForFixedProduct(Product product, Context context) { if (product.getId() == 12345) { throw new RuntimeException("fake exception"); } @@ -50,7 +88,7 @@ public void processMessageFailsForFixedProduct(Product product, Context context) @ParameterizedTest @Event(value = "sqs_event.json", type = SQSEvent.class) - public void batchProcessingSucceedsAndReturns(SQSEvent event) { + void batchProcessingSucceedsAndReturns(SQSEvent event) { // Arrange BatchMessageHandler handler = new BatchMessageHandlerBuilder() .withSqsBatchHandler() @@ -60,13 +98,28 @@ public void batchProcessingSucceedsAndReturns(SQSEvent event) { SQSBatchResponse sqsBatchResponse = handler.processBatch(event, context); // Assert - assertThat(sqsBatchResponse.getBatchItemFailures()).hasSize(0); + assertThat(sqsBatchResponse.getBatchItemFailures()).isEmpty(); } + @ParameterizedTest + @Event(value = "sqs_event_big.json", type = SQSEvent.class) + void parallelBatchProcessingSucceedsAndReturns(SQSEvent event) { + // Arrange + BatchMessageHandler handler = new BatchMessageHandlerBuilder() + .withSqsBatchHandler() + .buildWithRawMessageHandler(this::processMessageInParallelSucceeds); + + // Act + SQSBatchResponse sqsBatchResponse = handler.processBatchInParallel(event, context); + + // Assert + assertThat(sqsBatchResponse.getBatchItemFailures()).isEmpty(); + assertThat(threadList).hasSizeGreaterThan(1); + } @ParameterizedTest @Event(value = "sqs_event.json", type = SQSEvent.class) - public void shouldAddMessageToBatchFailure_whenException_withMessage(SQSEvent event) { + void shouldAddMessageToBatchFailure_whenException_withMessage(SQSEvent event) { // Arrange BatchMessageHandler handler = new BatchMessageHandlerBuilder() .withSqsBatchHandler() @@ -81,9 +134,27 @@ public void shouldAddMessageToBatchFailure_whenException_withMessage(SQSEvent ev assertThat(batchItemFailure.getItemIdentifier()).isEqualTo("e9144555-9a4f-4ec3-99a0-34ce359b4b54"); } + @ParameterizedTest + @Event(value = "sqs_event_big.json", type = SQSEvent.class) + void parallelBatchProcessing_shouldAddMessageToBatchFailure_whenException_withMessage(SQSEvent event) { + // Arrange + BatchMessageHandler handler = new BatchMessageHandlerBuilder() + .withSqsBatchHandler() + .buildWithRawMessageHandler(this::processMessageInParallelFailsForFixedMessage); + + // Act + SQSBatchResponse sqsBatchResponse = handler.processBatchInParallel(event, context); + + // Assert + assertThat(sqsBatchResponse.getBatchItemFailures()).hasSize(1); + SQSBatchResponse.BatchItemFailure batchItemFailure = sqsBatchResponse.getBatchItemFailures().get(0); + assertThat(batchItemFailure.getItemIdentifier()).isEqualTo("e9144555-9a4f-4ec3-99a0-34ce359b4b54"); + assertThat(threadList).hasSizeGreaterThan(1); + } + @ParameterizedTest @Event(value = "sqs_fifo_event.json", type = SQSEvent.class) - public void shouldAddMessageToBatchFailure_whenException_withSQSFIFO(SQSEvent event) { + void shouldAddMessageToBatchFailure_whenException_withSQSFIFO(SQSEvent event) { // Arrange BatchMessageHandler handler = new BatchMessageHandlerBuilder() .withSqsBatchHandler() @@ -103,7 +174,7 @@ public void shouldAddMessageToBatchFailure_whenException_withSQSFIFO(SQSEvent ev @ParameterizedTest @Event(value = "sqs_event.json", type = SQSEvent.class) - public void shouldAddMessageToBatchFailure_whenException_withProduct(SQSEvent event) { + void shouldAddMessageToBatchFailure_whenException_withProduct(SQSEvent event) { // Arrange BatchMessageHandler handler = new BatchMessageHandlerBuilder() @@ -121,7 +192,7 @@ public void shouldAddMessageToBatchFailure_whenException_withProduct(SQSEvent ev @ParameterizedTest @Event(value = "sqs_event.json", type = SQSEvent.class) - public void failingFailureHandlerShouldntFailBatch(SQSEvent event) { + void failingFailureHandlerShouldntFailBatch(SQSEvent event) { // Arrange AtomicBoolean wasCalled = new AtomicBoolean(false); BatchMessageHandler handler = new BatchMessageHandlerBuilder() @@ -144,7 +215,7 @@ public void failingFailureHandlerShouldntFailBatch(SQSEvent event) { @ParameterizedTest @Event(value = "sqs_event.json", type = SQSEvent.class) - public void failingSuccessHandlerShouldntFailBatchButShouldFailMessage(SQSEvent event) { + void failingSuccessHandlerShouldntFailBatchButShouldFailMessage(SQSEvent event) { // Arrange AtomicBoolean wasCalledAndFailed = new AtomicBoolean(false); BatchMessageHandler handler = new BatchMessageHandlerBuilder() diff --git a/powertools-batch/src/test/resources/dynamo_event_big.json b/powertools-batch/src/test/resources/dynamo_event_big.json new file mode 100644 index 000000000..fa0a75c24 --- /dev/null +++ b/powertools-batch/src/test/resources/dynamo_event_big.json @@ -0,0 +1,376 @@ +{ + "Records": [ + { + "eventID": "c4ca4238a0b923820dcc509a6f75849b", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439001", + "SizeBytes": 26, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "userIdentity": { + "principalId": "dynamodb.amazonaws.com", + "type": "Service" + } + }, + { + "eventID": "c81e728d9d4c2f636f067f89cc14862c", + "eventName": "MODIFY", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439092", + "SizeBytes": 59, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + }, + { + "eventID": "eccbc87e4b5ce2fe28308fd9f2a7baf3", + "eventName": "REMOVE", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439093", + "SizeBytes": 38, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + }, + { + "eventID": "c4ca4238a0b923820dcc509a6f75849b", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439031", + "SizeBytes": 26, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "userIdentity": { + "principalId": "dynamodb.amazonaws.com", + "type": "Service" + } + }, + { + "eventID": "c81e728d9d4c2f636f067f89cc14862c", + "eventName": "MODIFY", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439092", + "SizeBytes": 59, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + }, + { + "eventID": "eccbc87e4b5ce2fe28308fd9f2a7baf3", + "eventName": "REMOVE", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439093", + "SizeBytes": 38, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + }, + { + "eventID": "c4ca4238a0b923820dcc509a6f75849b", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439001", + "SizeBytes": 26, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "userIdentity": { + "principalId": "dynamodb.amazonaws.com", + "type": "Service" + } + }, + { + "eventID": "c81e728d9d4c2f636f067f89cc14862c", + "eventName": "MODIFY", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439092", + "SizeBytes": 59, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + }, + { + "eventID": "eccbc87e4b5ce2fe28308fd9f2a7baf3", + "eventName": "REMOVE", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439093", + "SizeBytes": 38, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + }, + { + "eventID": "c4ca4238a0b923820dcc509a6f75849b", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439031", + "SizeBytes": 26, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "userIdentity": { + "principalId": "dynamodb.amazonaws.com", + "type": "Service" + } + }, + { + "eventID": "c81e728d9d4c2f636f067f89cc14862c", + "eventName": "MODIFY", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439091", + "SizeBytes": 59, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + }, + { + "eventID": "eccbc87e4b5ce2fe28308fd9f2a7baf3", + "eventName": "REMOVE", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439093", + "SizeBytes": 38, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + } + ] +} \ No newline at end of file diff --git a/powertools-batch/src/test/resources/kinesis_event_big.json b/powertools-batch/src/test/resources/kinesis_event_big.json new file mode 100644 index 000000000..57f702d27 --- /dev/null +++ b/powertools-batch/src/test/resources/kinesis_event_big.json @@ -0,0 +1,224 @@ +{ + "Records": [ + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200962", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200962", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNDUsICJuYW1lIjoicHJvZHVjdDUiLCAicHJpY2UiOjQ1fQ==", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200963", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200963", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200964", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200964", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNDUsICJuYW1lIjoicHJvZHVjdDUiLCAicHJpY2UiOjQ1fQ==", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200965", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200965", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + },{ + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200966", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200966", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNDUsICJuYW1lIjoicHJvZHVjdDUiLCAicHJpY2UiOjQ1fQ==", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200967", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200967", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200968", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200968", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNDUsICJuYW1lIjoicHJvZHVjdDUiLCAicHJpY2UiOjQ1fQ==", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200969", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200969", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200971", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200971", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200981", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200981", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNDUsICJuYW1lIjoicHJvZHVjdDUiLCAicHJpY2UiOjQ1fQ==", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200992", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200992", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "eyJpZCI6MTIzNCwgIm5hbWUiOiJwcm9kdWN0IiwgInByaWNlIjo0Mn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244210961", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244210961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file diff --git a/powertools-batch/src/test/resources/sqs_event_big.json b/powertools-batch/src/test/resources/sqs_event_big.json new file mode 100644 index 000000000..f5c83f442 --- /dev/null +++ b/powertools-batch/src/test/resources/sqs_event_big.json @@ -0,0 +1,429 @@ +{ + "Records": [ + { + "messageId": "d9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1234,\n \"name\": \"product\",\n \"price\": 42\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "f9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "14e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1235,\n \"name\": \"product\",\n \"price\": 43\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "14e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "g9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "15e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1236,\n \"name\": \"product\",\n \"price\": 44\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "15e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "c9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "16e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1237,\n \"name\": \"product\",\n \"price\": 45\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "16e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "b4144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1238,\n \"name\": \"product\",\n \"price\": 486\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "a2144552-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "14e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1239,\n \"name\": \"product\",\n \"price\": 430\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "14e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "32144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "15e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1240,\n \"name\": \"product\",\n \"price\": 445\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "15e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "a9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "16e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1241,\n \"name\": \"product\",\n \"price\": 45\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "16e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "d9144555-9a4f-4ec3-99a0-34ce359b354", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1242,\n \"name\": \"product\",\n \"price\": 42\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "e9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "14e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1243,\n \"name\": \"product\",\n \"price\": 43\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "14e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "19144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "15e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1244,\n \"name\": \"product\",\n \"price\": 44\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "15e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "c9144555-9a4f-4ec3-99a0-34ce3a9b4b54", + "receiptHandle": "16e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1245,\n \"name\": \"product\",\n \"price\": 45\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "16e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "b4144555-9a4f-4ec3-99a5-34ce359b4b54", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1247,\n \"name\": \"product\",\n \"price\": 486\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "a2144555-9a4f-4ec3-97a0-34ce359b4b54", + "receiptHandle": "14e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1248,\n \"name\": \"product\",\n \"price\": 430\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "14e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "3k144555-9a4f-4ec2-99a0-34ce359b4b54", + "receiptHandle": "15e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1249,\n \"name\": \"product\",\n \"price\": 445\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "15e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "h9144555-9aaf-4ec3-99a0-34ce359b4b54", + "receiptHandle": "16e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1250,\n \"name\": \"product\",\n \"price\": 45\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "16e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "d9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1234,\n \"name\": \"product\",\n \"price\": 42\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "f9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "14e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1235,\n \"name\": \"product\",\n \"price\": 43\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "14e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "g9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "15e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1236,\n \"name\": \"product\",\n \"price\": 44\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "15e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "c9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "16e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1237,\n \"name\": \"product\",\n \"price\": 45\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "16e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "b4144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1238,\n \"name\": \"product\",\n \"price\": 486\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "a2144552-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "14e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1239,\n \"name\": \"product\",\n \"price\": 430\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "14e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "32144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "15e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1240,\n \"name\": \"product\",\n \"price\": 445\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "15e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "a9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "16e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1241,\n \"name\": \"product\",\n \"price\": 45\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "16e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "d9144555-9a4f-4ec3-99a0-34ce359b354", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1242,\n \"name\": \"product\",\n \"price\": 42\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file diff --git a/powertools-cloudformation/pom.xml b/powertools-cloudformation/pom.xml index 54c104468..e3ab012ae 100644 --- a/powertools-cloudformation/pom.xml +++ b/powertools-cloudformation/pom.xml @@ -24,36 +24,14 @@ powertools-parent software.amazon.lambda - 1.19.0-SNAPSHOT + 2.0.0-RC1 - Powertools for AWS Lambda (Java)library Cloudformation + Powertools for AWS Lambda (Java) - Cloudformation A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda (Java) Team - Amazon Web Services - https://aws.amazon.com/ - - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - @@ -97,6 +75,16 @@ junit-jupiter-params test + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + org.assertj assertj-core @@ -108,13 +96,4 @@ test - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - \ No newline at end of file + diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java index 2f020aa25..404137802 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java @@ -36,6 +36,7 @@ import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.utils.StringInputStream; +import software.amazon.awssdk.utils.StringUtils; /** * Client for sending responses to AWS CloudFormation custom resources by way of a response URL, which is an Amazon S3 @@ -148,7 +149,9 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, ObjectNode node = body.toObjectNode(null); return new StringInputStream(node.toString()); } else { - + if (!StringUtils.isBlank(resp.getReason())) { + reason = resp.getReason(); + } String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() : event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() : context.getLogStreamName(); diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java index fe18000d4..8c782d957 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; +import software.amazon.awssdk.utils.StringUtils; /** * Models the arbitrary data to be sent to the custom resource in response to a CloudFormation event. This object @@ -30,12 +31,22 @@ public class Response { private final Status status; private final String physicalResourceId; private final boolean noEcho; + private final String reason; private Response(JsonNode jsonNode, Status status, String physicalResourceId, boolean noEcho) { this.jsonNode = jsonNode; this.status = status; this.physicalResourceId = physicalResourceId; this.noEcho = noEcho; + this.reason = null; + } + + private Response(JsonNode jsonNode, Status status, String physicalResourceId, boolean noEcho, String reason) { + this.jsonNode = jsonNode; + this.status = status; + this.physicalResourceId = physicalResourceId; + this.noEcho = noEcho; + this.reason = reason; } /** @@ -47,23 +58,6 @@ public static Builder builder() { return new Builder(); } - /** - * Creates a failed Response with no physicalResourceId set. Powertools for AWS Lambda (Java) will set the physicalResourceId to the - * Lambda LogStreamName - *

- * The value returned for a PhysicalResourceId can change custom resource update operations. If the value returned - * is the same, it is considered a normal update. If the value returned is different, AWS CloudFormation recognizes - * the update as a replacement and sends a delete request to the old resource. For more information, - * see AWS::CloudFormation::CustomResource. - * - * @return a failed Response with no value. - * @deprecated this method is not safe. Provide a physicalResourceId. - */ - @Deprecated - public static Response failed() { - return new Response(null, Status.FAILED, null, false); - } - /** * Creates a failed Response with a given physicalResourceId. * @@ -80,23 +74,6 @@ public static Response failed(String physicalResourceId) { return new Response(null, Status.FAILED, physicalResourceId, false); } - /** - * Creates a successful Response with no physicalResourceId set. Powertools for AWS Lambda (Java) will set the physicalResourceId to the - * Lambda LogStreamName - *

- * The value returned for a PhysicalResourceId can change custom resource update operations. If the value returned - * is the same, it is considered a normal update. If the value returned is different, AWS CloudFormation recognizes - * the update as a replacement and sends a delete request to the old resource. For more information, - * see AWS::CloudFormation::CustomResource. - * - * @return a success Response with no physicalResourceId value. - * @deprecated this method is not safe. Provide a physicalResourceId. - */ - @Deprecated - public static Response success() { - return new Response(null, Status.SUCCESS, null, false); - } - /** * Creates a successful Response with a given physicalResourceId. * @@ -149,6 +126,15 @@ public boolean isNoEcho() { return noEcho; } + /** + * The reason for the failure. + * + * @return a potentially null reason + */ + public String getReason() { + return reason; + } + /** * Includes all Response attributes, including its value in JSON format * @@ -161,6 +147,7 @@ public String toString() { attributes.put("Status", status); attributes.put("PhysicalResourceId", physicalResourceId); attributes.put("NoEcho", noEcho); + attributes.put("Reason", reason); return attributes.entrySet().stream() .map(entry -> entry.getKey() + " = " + entry.getValue()) .collect(Collectors.joining(",", "[", "]")); @@ -182,6 +169,7 @@ public static class Builder { private Status status; private String physicalResourceId; private boolean noEcho; + private String reason; private Builder() { } @@ -263,6 +251,20 @@ public Builder noEcho(boolean noEcho) { return this; } + /** + * Reason for the response. + * Reason is optional for Success responses, but required for Failed responses. + * If not provided it will be replaced with cloudwatch log stream name. + * + * @param reason if null, the default reason will be used + * @return a reference to this builder + */ + + public Builder reason(String reason) { + this.reason = reason; + return this; + } + /** * Builds a Response object for the value. * @@ -277,6 +279,9 @@ public Response build() { node = mapper.valueToTree(value); } Status responseStatus = this.status != null ? this.status : Status.SUCCESS; + if (StringUtils.isNotBlank(this.reason)) { + return new Response(node, responseStatus, physicalResourceId, noEcho, reason); + } return new Response(node, responseStatus, physicalResourceId, noEcho); } } diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java index 51f0e95f9..9da18790c 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java @@ -38,7 +38,7 @@ import software.amazon.awssdk.utils.StringInputStream; import software.amazon.lambda.powertools.cloudformation.CloudFormationResponse.ResponseBody; -public class CloudFormationResponseTest { +class CloudFormationResponseTest { /** * Creates a mock CloudFormationCustomResourceEvent with a non-null response URL. @@ -215,7 +215,7 @@ void reasonIncludesLogStreamName() { } @Test - public void sendWithNoResponseData() throws Exception { + void sendWithNoResponseData() throws Exception { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); Context context = mock(Context.class); CloudFormationResponse cfnResponse = testableCloudFormationResponse(); @@ -237,7 +237,7 @@ public void sendWithNoResponseData() throws Exception { } @Test - public void sendWithNonNullResponseData() throws Exception { + void sendWithNonNullResponseData() throws Exception { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); Context context = mock(Context.class); CloudFormationResponse cfnResponse = testableCloudFormationResponse(); @@ -289,7 +289,7 @@ void responseBodyStreamSuccessResponse() throws Exception { Context context = mock(Context.class); CloudFormationResponse cfnResponse = testableCloudFormationResponse(); - StringInputStream stream = cfnResponse.responseBodyStream(event, context, Response.success()); + StringInputStream stream = cfnResponse.responseBodyStream(event, context, Response.success(null)); String expectedJson = "{" + "\"Status\":\"SUCCESS\"," + @@ -310,7 +310,7 @@ void responseBodyStreamFailedResponse() throws Exception { Context context = mock(Context.class); CloudFormationResponse cfnResponse = testableCloudFormationResponse(); - StringInputStream stream = cfnResponse.responseBodyStream(event, context, Response.failed()); + StringInputStream stream = cfnResponse.responseBodyStream(event, context, Response.failed(null)); String expectedJson = "{" + "\"Status\":\"FAILED\"," + @@ -324,4 +324,27 @@ void responseBodyStreamFailedResponse() throws Exception { "}"; assertThat(stream.getString()).isEqualTo(expectedJson); } + + @Test + void responseBodyStreamFailedResponseWithReason() throws Exception { + CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); + Context context = mock(Context.class); + CloudFormationResponse cfnResponse = testableCloudFormationResponse(); + String failureReason = "Failed test reason"; + Response failedResponseWithReason = Response.builder(). + status(Response.Status.FAILED).reason(failureReason).build(); + StringInputStream stream = cfnResponse.responseBodyStream(event, context, failedResponseWithReason); + + String expectedJson = "{" + + "\"Status\":\"FAILED\"," + + "\"Reason\":\"" + failureReason + "\"," + + "\"PhysicalResourceId\":null," + + "\"StackId\":null," + + "\"RequestId\":null," + + "\"LogicalResourceId\":null," + + "\"NoEcho\":false," + + "\"Data\":null" + + "}"; + assertThat(stream.getString()).isEqualTo(expectedJson); + } } diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java index 37fe73d0f..3e2930541 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java @@ -22,7 +22,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; -public class ResponseTest { +class ResponseTest { @Test void defaultValues() { @@ -33,11 +33,13 @@ void defaultValues() { assertThat(response.getStatus()).isEqualTo(Response.Status.SUCCESS); assertThat(response.getPhysicalResourceId()).isNull(); assertThat(response.isNoEcho()).isFalse(); + assertThat(response.getReason()).isNull(); assertThat(response.toString()).contains("JSON = null"); assertThat(response.toString()).contains("Status = SUCCESS"); assertThat(response.toString()).contains("PhysicalResourceId = null"); assertThat(response.toString()).contains("NoEcho = false"); + assertThat(response.toString()).contains("Reason = null"); } @Test @@ -61,6 +63,27 @@ void explicitNullValues() { assertThat(response.toString()).contains("NoEcho = false"); } + @Test + void explicitReasonWithDefaultValues() { + String reason = "test"; + Response response = Response.builder() + .reason(reason) + .build(); + assertThat(response).isNotNull(); + assertThat(response.getJsonNode()).isNull(); + assertThat(response.getStatus()).isEqualTo(Response.Status.SUCCESS); + assertThat(response.getPhysicalResourceId()).isNull(); + assertThat(response.isNoEcho()).isFalse(); + assertThat(response.getReason()).isNotNull(); + assertThat(response.getReason()).isEqualTo(reason); + + assertThat(response.toString()).contains("JSON = null"); + assertThat(response.toString()).contains("Status = SUCCESS"); + assertThat(response.toString()).contains("PhysicalResourceId = null"); + assertThat(response.toString()).contains("NoEcho = false"); + assertThat(response.toString()).contains("Reason = "+reason); + } + @Test void customNonJsonRelatedValues() { Response response = Response.builder() @@ -92,7 +115,7 @@ void jsonMapValueWithDefaultObjectMapper() { String expected = "{\"foo\":\"bar\"}"; assertThat(response.getJsonNode()).isNotNull(); - assertThat(response.getJsonNode().toString()).isEqualTo(expected); + assertThat(response.getJsonNode()).hasToString(expected); assertThat(response.toString()).contains("JSON = " + expected); } @@ -105,7 +128,7 @@ void jsonObjectValueWithDefaultObjectMapper() { .build(); String expected = "{\"PropertyWithLongName\":\"test\"}"; - assertThat(response.getJsonNode().toString()).isEqualTo(expected); + assertThat(response.getJsonNode()).hasToString(expected); assertThat(response.toString()).contains("JSON = " + expected); } @@ -119,7 +142,7 @@ void jsonObjectValueWithNullObjectMapper() { .build(); String expected = "{\"PropertyWithLongName\":\"test\"}"; - assertThat(response.getJsonNode().toString()).isEqualTo(expected); + assertThat(response.getJsonNode()).hasToString(expected); assertThat(response.toString()).contains("JSON = " + expected); } @@ -135,7 +158,7 @@ void jsonObjectValueWithCustomObjectMapper() { .build(); String expected = "{\"property-with-long-name\":10}"; - assertThat(response.getJsonNode().toString()).isEqualTo(expected); + assertThat(response.getJsonNode()).hasToString(expected); assertThat(response.toString()).contains("JSON = " + expected); } @@ -154,13 +177,13 @@ void jsonObjectValueWithPostConfiguredObjectMapper() { customMapper.setPropertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE); String expected = "{\"property-with-long-name\":10}"; - assertThat(response.getJsonNode().toString()).isEqualTo(expected); + assertThat(response.getJsonNode()).hasToString(expected); assertThat(response.toString()).contains("JSON = " + expected); } @Test void successFactoryMethod() { - Response response = Response.success(); + Response response = Response.success(null); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(Response.Status.SUCCESS); @@ -168,7 +191,7 @@ void successFactoryMethod() { @Test void failedFactoryMethod() { - Response response = Response.failed(); + Response response = Response.failed(null); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(Response.Status.FAILED); diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java index 2bbda309f..e55abca03 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java @@ -23,16 +23,16 @@ public class NoPhysicalResourceIdSetHandler extends AbstractCustomResourceHandle @Override protected Response create(CloudFormationCustomResourceEvent event, Context context) { - return Response.success(); + return Response.success(null); } @Override protected Response update(CloudFormationCustomResourceEvent event, Context context) { - return Response.success(); + return Response.success(null); } @Override protected Response delete(CloudFormationCustomResourceEvent event, Context context) { - return Response.success(); + return Response.success(null); } } diff --git a/powertools-common/pom.xml b/powertools-common/pom.xml new file mode 100644 index 000000000..b9a8572ea --- /dev/null +++ b/powertools-common/pom.xml @@ -0,0 +1,196 @@ + + + + + 4.0.0 + + powertools-common + jar + + + powertools-parent + software.amazon.lambda + 2.0.0-RC1 + + + Powertools for AWS Lambda (Java) - Common Internal Utilities + Internal utilities shared by the Powertools for AWS Lambda (Java) modules. Do not use directly in your project. + + + + org.slf4j + slf4j-api + + + org.aspectj + aspectjrt + provided + + + com.amazonaws + aws-lambda-java-core + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit-pioneer + junit-pioneer + test + + + org.apache.commons + commons-lang3 + test + + + org.aspectj + aspectjweaver + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + + + + + generate-graalvm-files + + + org.mockito + mockito-subclass + 5.17.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-common,experimental-class-define-support + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + 5.17.0 + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.2 + true + + + test-native + + test + + test + + + + powertools-common + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + --native-image-info + -H:+UnlockExperimentalVMOptions + -H:Log=registerResource:5 + + + + + + + + + + + + src/main/resources-filtered + true + + + + src/main/resources + + + + diff --git a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaConstants.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaConstants.java similarity index 72% rename from powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaConstants.java rename to powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaConstants.java index e64e334c5..d27ac1aa2 100644 --- a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaConstants.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaConstants.java @@ -12,17 +12,11 @@ * */ -package software.amazon.lambda.powertools.core.internal; +package software.amazon.lambda.powertools.common.internal; public class LambdaConstants { public static final String LAMBDA_FUNCTION_NAME_ENV = "AWS_LAMBDA_FUNCTION_NAME"; public static final String AWS_REGION_ENV = "AWS_REGION"; - // Also you can use AWS_LAMBDA_INITIALIZATION_TYPE to distinguish between on-demand and SnapStart initialization - // it's not recommended to use this env variable to initialize SDK clients or other resources. - @Deprecated - public static final String AWS_LAMBDA_INITIALIZATION_TYPE = "AWS_LAMBDA_INITIALIZATION_TYPE"; - @Deprecated - public static final String ON_DEMAND = "on-demand"; public static final String X_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID"; public static final String XRAY_TRACE_HEADER = "com.amazonaws.xray.traceHeader"; public static final String AWS_SAM_LOCAL = "AWS_SAM_LOCAL"; diff --git a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessor.java similarity index 94% rename from powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java rename to powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessor.java index d4e18dddc..bfacd5204 100644 --- a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessor.java @@ -12,12 +12,12 @@ * */ -package software.amazon.lambda.powertools.core.internal; +package software.amazon.lambda.powertools.common.internal; import static java.util.Optional.empty; import static java.util.Optional.of; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getProperty; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; +import static software.amazon.lambda.powertools.common.internal.SystemWrapper.getProperty; +import static software.amazon.lambda.powertools.common.internal.SystemWrapper.getenv; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; @@ -69,7 +69,6 @@ public static boolean placedOnStreamHandler(final ProceedingJoinPoint pjp) { } public static Context extractContext(final ProceedingJoinPoint pjp) { - if (placedOnRequestHandler(pjp)) { return (Context) pjp.getArgs()[1]; } else if (placedOnStreamHandler(pjp)) { diff --git a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/SystemWrapper.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/SystemWrapper.java similarity index 93% rename from powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/SystemWrapper.java rename to powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/SystemWrapper.java index 99046ffec..6dc4e9d9f 100644 --- a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/SystemWrapper.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/SystemWrapper.java @@ -12,7 +12,7 @@ * */ -package software.amazon.lambda.powertools.core.internal; +package software.amazon.lambda.powertools.common.internal; public class SystemWrapper { private SystemWrapper() { diff --git a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/UserAgentConfigurator.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/UserAgentConfigurator.java similarity index 84% rename from powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/UserAgentConfigurator.java rename to powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/UserAgentConfigurator.java index 354305d33..d2e592902 100644 --- a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/UserAgentConfigurator.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/UserAgentConfigurator.java @@ -12,13 +12,12 @@ * */ -package software.amazon.lambda.powertools.core.internal; +package software.amazon.lambda.powertools.common.internal; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; +import static software.amazon.lambda.powertools.common.internal.SystemWrapper.getenv; -import java.io.FileInputStream; import java.io.IOException; -import java.net.URL; +import java.io.InputStream; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,8 +36,8 @@ public class UserAgentConfigurator { public static final String AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV"; private static final Logger LOG = LoggerFactory.getLogger(UserAgentConfigurator.class); private static final String NO_OP = "no-op"; - private static String ptVersion = getProjectVersion(); - private static String userAgentPattern = "PT/" + PT_FEATURE_VARIABLE + "/" + ptVersion + " PTEnv/" + private static final String POWERTOOLS_VERSION = getProjectVersion(); + private static final String USER_AGENT_PATTERN = "PT/" + PT_FEATURE_VARIABLE + "/" + POWERTOOLS_VERSION + " PTEnv/" + PT_EXEC_ENV_VARIABLE; private UserAgentConfigurator() { @@ -66,11 +65,12 @@ static String getProjectVersion() { */ static String getVersionFromProperties(String propertyFileName, String versionKey) { - URL propertiesFileURI = Thread.currentThread().getContextClassLoader().getResource(propertyFileName); - if (propertiesFileURI != null) { - try (FileInputStream fis = new FileInputStream(propertiesFileURI.getPath())) { + InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(propertyFileName); + + if (is != null) { + try { Properties properties = new Properties(); - properties.load(fis); + properties.load(is); String version = properties.getProperty(versionKey); if (version != null && !version.isEmpty()) { return version; @@ -99,7 +99,7 @@ public static String getUserAgent(String ptFeature) { String awsExecutionEnv = getenv(AWS_EXECUTION_ENV); String ptExecEnv = awsExecutionEnv != null ? awsExecutionEnv : NA; - String userAgent = userAgentPattern.replace(PT_EXEC_ENV_VARIABLE, ptExecEnv); + String userAgent = USER_AGENT_PATTERN.replace(PT_EXEC_ENV_VARIABLE, ptExecEnv); if (ptFeature == null || ptFeature.isEmpty()) { ptFeature = NO_OP; diff --git a/powertools-core/src/main/resources-filtered/version.properties b/powertools-common/src/main/resources-filtered/version.properties similarity index 100% rename from powertools-core/src/main/resources-filtered/version.properties rename to powertools-common/src/main/resources-filtered/version.properties diff --git a/powertools-common/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-common/jni-config.json b/powertools-common/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-common/jni-config.json new file mode 100644 index 000000000..8ea90d67f --- /dev/null +++ b/powertools-common/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-common/jni-config.json @@ -0,0 +1,22 @@ +[ +{ + "name":"java.lang.String", + "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"org.apache.maven.surefire.booter.ForkedBooter", + "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"sun.instrument.InstrumentationImpl", + "methods":[{"name":"","parameterTypes":["long","boolean","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] +}, +{ + "name":"sun.management.VMManagementImpl", + "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] +} +] diff --git a/powertools-common/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-common/reflect-config.json b/powertools-common/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-common/reflect-config.json new file mode 100644 index 000000000..54afdb74e --- /dev/null +++ b/powertools-common/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-common/reflect-config.json @@ -0,0 +1,189 @@ +[ +{ + "name":"com.amazonaws.services.lambda.runtime.Context", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getAwsRequestId","parameterTypes":[] }, {"name":"getClientContext","parameterTypes":[] }, {"name":"getFunctionName","parameterTypes":[] }, {"name":"getFunctionVersion","parameterTypes":[] }, {"name":"getIdentity","parameterTypes":[] }, {"name":"getInvokedFunctionArn","parameterTypes":[] }, {"name":"getLogGroupName","parameterTypes":[] }, {"name":"getLogStreamName","parameterTypes":[] }, {"name":"getLogger","parameterTypes":[] }, {"name":"getMemoryLimitInMB","parameterTypes":[] }, {"name":"getRemainingTimeInMillis","parameterTypes":[] }] +}, +{ + "name":"com.sun.tools.attach.VirtualMachine" +}, +{ + "name":"java.io.Closeable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.io.Flushable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.io.InputStream", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"available","parameterTypes":[] }, {"name":"close","parameterTypes":[] }, {"name":"mark","parameterTypes":["int"] }, {"name":"markSupported","parameterTypes":[] }, {"name":"read","parameterTypes":[] }, {"name":"read","parameterTypes":["byte[]"] }, {"name":"read","parameterTypes":["byte[]","int","int"] }, {"name":"readAllBytes","parameterTypes":[] }, {"name":"readNBytes","parameterTypes":["int"] }, {"name":"readNBytes","parameterTypes":["byte[]","int","int"] }, {"name":"reset","parameterTypes":[] }, {"name":"skip","parameterTypes":["long"] }, {"name":"skipNBytes","parameterTypes":["long"] }, {"name":"transferTo","parameterTypes":["java.io.OutputStream"] }] +}, +{ + "name":"java.io.OutputStream", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"close","parameterTypes":[] }, {"name":"flush","parameterTypes":[] }, {"name":"write","parameterTypes":["int"] }, {"name":"write","parameterTypes":["byte[]"] }, {"name":"write","parameterTypes":["byte[]","int","int"] }] +}, +{ + "name":"java.io.Serializable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.AutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.Class", + "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] +}, +{ + "name":"java.lang.ClassLoader", + "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] +}, +{ + "name":"java.lang.Module", + "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] +}, +{ + "name":"java.lang.Object", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] +}, +{ + "name":"java.lang.ProcessEnvironment", + "fields":[{"name":"theCaseInsensitiveEnvironment"}, {"name":"theEnvironment"}] +}, +{ + "name":"java.lang.ProcessHandle", + "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime", + "methods":[{"name":"version","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime$Version", + "methods":[{"name":"feature","parameterTypes":[] }] +}, +{ + "name":"java.lang.StackWalker" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.annotation.Retention", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.annotation.Target", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.invoke.MethodHandle", + "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles", + "methods":[{"name":"lookup","parameterTypes":[] }] +}, +{ + "name":"java.lang.invoke.MethodHandles$Lookup", + "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] +}, +{ + "name":"java.lang.invoke.MethodType", + "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] +}, +{ + "name":"java.lang.reflect.AccessibleObject", + "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] +}, +{ + "name":"java.lang.reflect.AnnotatedArrayType", + "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedType", + "methods":[{"name":"getType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Executable", + "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Parameter", + "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.util.Collections$UnmodifiableMap", + "fields":[{"name":"m"}] +}, +{ + "name":"java.util.concurrent.ForkJoinTask", + "fields":[{"name":"aux"}, {"name":"status"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"jdk.internal.misc.Unsafe" +}, +{ + "name":"kotlin.jvm.JvmInline" +}, +{ + "name":"org.apiguardian.api.API", + "queryAllPublicMethods":true +}, +{ + "name":"org.aspectj.lang.JoinPoint", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getArgs","parameterTypes":[] }, {"name":"getKind","parameterTypes":[] }, {"name":"getSignature","parameterTypes":[] }, {"name":"getSourceLocation","parameterTypes":[] }, {"name":"getStaticPart","parameterTypes":[] }, {"name":"getTarget","parameterTypes":[] }, {"name":"getThis","parameterTypes":[] }, {"name":"toLongString","parameterTypes":[] }, {"name":"toShortString","parameterTypes":[] }] +}, +{ + "name":"org.aspectj.lang.ProceedingJoinPoint", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"proceed","parameterTypes":[] }, {"name":"proceed","parameterTypes":["java.lang.Object[]"] }, {"name":"set$AroundClosure","parameterTypes":["org.aspectj.runtime.internal.AroundClosure"] }, {"name":"stack$AroundClosure","parameterTypes":["org.aspectj.runtime.internal.AroundClosure"] }] +}, +{ + "name":"org.aspectj.lang.Signature", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getDeclaringType","parameterTypes":[] }, {"name":"getDeclaringTypeName","parameterTypes":[] }, {"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"toLongString","parameterTypes":[] }, {"name":"toShortString","parameterTypes":[] }] +}, +{ + "name":"sun.reflect.ReflectionFactory", + "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] +} +] diff --git a/powertools-common/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-common/resource-config.json b/powertools-common/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-common/resource-config.json new file mode 100644 index 000000000..6fb5eb95e --- /dev/null +++ b/powertools-common/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-common/resource-config.json @@ -0,0 +1,21 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.configuration.Configuration\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.presentation.Representation\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" + }, { + "pattern":"\\Qversion.properties\\E" + }]}, + "bundles":[] +} diff --git a/powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessorTest.java b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessorTest.java similarity index 77% rename from powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessorTest.java rename to powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessorTest.java index dc8f49580..15d7bccdb 100644 --- a/powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessorTest.java +++ b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessorTest.java @@ -12,25 +12,26 @@ * */ -package software.amazon.lambda.powertools.core.internal; +package software.amazon.lambda.powertools.common.internal; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.RequestStreamHandler; import java.io.InputStream; import java.io.OutputStream; import java.util.Optional; + import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; +import org.junitpioneer.jupiter.ClearEnvironmentVariable; +import org.junitpioneer.jupiter.SetEnvironmentVariable; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; class LambdaHandlerProcessorTest { @@ -139,27 +140,23 @@ void placedOnStreamHandler_shouldInvalidateOnTypeOfArgs_invalidContextArg() { } @Test + @SetEnvironmentVariable(key = LambdaConstants.X_AMZN_TRACE_ID, value = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\"") void getXrayTraceId_present() { String traceID = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\""; - try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) { - mockedSystemWrapper.when(() -> getenv(LambdaConstants.X_AMZN_TRACE_ID)).thenReturn(traceID); - Optional xRayTraceId = LambdaHandlerProcessor.getXrayTraceId(); + Optional xRayTraceId = LambdaHandlerProcessor.getXrayTraceId(); - assertThat(xRayTraceId.isPresent()).isTrue(); - assertThat(traceID.split(";")[0].replace(LambdaConstants.ROOT_EQUALS, "")).isEqualTo(xRayTraceId.get()); - } + assertThat(xRayTraceId.isPresent()).isTrue(); + assertThat(traceID.split(";")[0].replace(LambdaConstants.ROOT_EQUALS, "")).isEqualTo(xRayTraceId.get()); } @Test + @ClearEnvironmentVariable(key = LambdaConstants.X_AMZN_TRACE_ID) void getXrayTraceId_notPresent() { - try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) { - mockedSystemWrapper.when(() -> getenv(LambdaConstants.X_AMZN_TRACE_ID)).thenReturn(null); - boolean isXRayTraceIdPresent = LambdaHandlerProcessor.getXrayTraceId().isPresent(); + boolean isXRayTraceIdPresent = LambdaHandlerProcessor.getXrayTraceId().isPresent(); - assertThat(isXRayTraceIdPresent).isFalse(); - } + assertThat(isXRayTraceIdPresent).isFalse(); } @Test @@ -209,37 +206,28 @@ void isColdStart_coldStartDone() { } @Test + @SetEnvironmentVariable(key = LambdaConstants.AWS_SAM_LOCAL, value = "true") void isSamLocal() { - try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) { - mockedSystemWrapper.when(() -> getenv(LambdaConstants.AWS_SAM_LOCAL)).thenReturn("true"); - boolean isSamLocal = LambdaHandlerProcessor.isSamLocal(); + boolean isSamLocal = LambdaHandlerProcessor.isSamLocal(); - assertThat(isSamLocal).isTrue(); - } + assertThat(isSamLocal).isTrue(); } @Test + @SetEnvironmentVariable(key = LambdaConstants.POWERTOOLS_SERVICE_NAME, value = "MyService") void serviceName() { - try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) { - String expectedServiceName = "MyService"; - mockedSystemWrapper.when(() -> getenv(LambdaConstants.POWERTOOLS_SERVICE_NAME)) - .thenReturn(expectedServiceName); + String expectedServiceName = "MyService"; + String actualServiceName = LambdaHandlerProcessor.serviceName(); - String actualServiceName = LambdaHandlerProcessor.serviceName(); - - assertThat(actualServiceName).isEqualTo(expectedServiceName); - } + assertThat(actualServiceName).isEqualTo(expectedServiceName); } @Test + @ClearEnvironmentVariable(key = LambdaConstants.POWERTOOLS_SERVICE_NAME) void serviceName_Undefined() { LambdaHandlerProcessor.resetServiceName(); - try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) { - mockedSystemWrapper.when(() -> getenv(LambdaConstants.POWERTOOLS_SERVICE_NAME)).thenReturn(null); - - assertThat(LambdaHandlerProcessor.serviceName()).isEqualTo(LambdaConstants.SERVICE_UNDEFINED); - } + assertThat(LambdaHandlerProcessor.serviceName()).isEqualTo(LambdaConstants.SERVICE_UNDEFINED); } private ProceedingJoinPoint mockRequestHandlerPjp(Class handlerClass, Object[] handlerArgs) { @@ -248,4 +236,4 @@ private ProceedingJoinPoint mockRequestHandlerPjp(Class handlerClass, Object[] h when(pjpMock.getSignature()).thenReturn(signature); return pjpMock; } -} \ No newline at end of file +} diff --git a/powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/UserAgentConfiguratorTest.java b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/UserAgentConfiguratorTest.java similarity index 67% rename from powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/UserAgentConfiguratorTest.java rename to powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/UserAgentConfiguratorTest.java index 2d75bdb3a..0c7935a55 100644 --- a/powertools-core/src/test/java/software/amazon/lambda/powertools/core/internal/UserAgentConfiguratorTest.java +++ b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/UserAgentConfiguratorTest.java @@ -12,21 +12,22 @@ * */ -package software.amazon.lambda.powertools.core.internal; +package software.amazon.lambda.powertools.common.internal; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mockStatic; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; -import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.AWS_EXECUTION_ENV; -import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.VERSION_KEY; -import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.VERSION_PROPERTIES_FILENAME; -import static software.amazon.lambda.powertools.core.internal.UserAgentConfigurator.getVersionFromProperties; +import static software.amazon.lambda.powertools.common.internal.UserAgentConfigurator.AWS_EXECUTION_ENV; +import static software.amazon.lambda.powertools.common.internal.UserAgentConfigurator.VERSION_KEY; +import static software.amazon.lambda.powertools.common.internal.UserAgentConfigurator.VERSION_PROPERTIES_FILENAME; +import static software.amazon.lambda.powertools.common.internal.UserAgentConfigurator.getVersionFromProperties; import java.io.File; -import java.util.Objects; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.regex.Pattern; + import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; +import org.junitpioneer.jupiter.SetEnvironmentVariable; class UserAgentConfiguratorTest { @@ -63,14 +64,18 @@ void testGetVersionFromProperties_FileNotExist() { } @Test - void testGetVersionFromProperties_InvalidFile() { - File f = new File(Objects.requireNonNull(Thread.currentThread().getContextClassLoader() - .getResource("unreadable.properties")).getPath()); + void testGetVersionFromProperties_InvalidFile() throws IOException { + Path tempFile = Files.createTempFile("unreadable", ".properties"); + File f = tempFile.toFile(); f.setReadable(false); - String version = getVersionFromProperties("unreadable.properties", VERSION_KEY); + String version = getVersionFromProperties(f.getName(), VERSION_KEY); assertThat(version).isEqualTo("NA"); + + // Cleanup + f.setReadable(true); + Files.deleteIfExists(tempFile); } @Test @@ -109,15 +114,13 @@ void testGetUserAgent_NullFeature() { } @Test + @SetEnvironmentVariable(key = AWS_EXECUTION_ENV, value = "AWS_Lambda_java8") void testGetUserAgent_SetAWSExecutionEnv() { - try (MockedStatic mockedSystemWrapper = mockStatic(SystemWrapper.class)) { - mockedSystemWrapper.when(() -> getenv(AWS_EXECUTION_ENV)).thenReturn("AWS_Lambda_java8"); - String userAgent = UserAgentConfigurator.getUserAgent("test-feature"); - - assertThat(userAgent) - .isNotNull() - .isEqualTo("PT/test-feature/" + VERSION + " PTEnv/AWS_Lambda_java8"); - } + String userAgent = UserAgentConfigurator.getUserAgent("test-feature"); + + assertThat(userAgent) + .isNotNull() + .isEqualTo("PT/test-feature/" + VERSION + " PTEnv/AWS_Lambda_java8"); } } diff --git a/powertools-core/src/test/resources/test.properties b/powertools-common/src/test/resources/test.properties similarity index 100% rename from powertools-core/src/test/resources/test.properties rename to powertools-common/src/test/resources/test.properties diff --git a/powertools-core/pom.xml b/powertools-core/pom.xml deleted file mode 100644 index 041950cf5..000000000 --- a/powertools-core/pom.xml +++ /dev/null @@ -1,115 +0,0 @@ - - - - - 4.0.0 - - powertools-core - jar - - - powertools-parent - software.amazon.lambda - 1.19.0-SNAPSHOT - - - Powertools for AWS Lambda (Java) library Core - - A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. - - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - - - - - com.amazonaws - aws-lambda-java-core - - - org.aspectj - aspectjrt - - - org.apache.logging.log4j - log4j-slf4j2-impl - ${log4j.version} - - - - - org.junit.jupiter - junit-jupiter-api - test - - - org.junit.jupiter - junit-jupiter-engine - test - - - org.apache.commons - commons-lang3 - test - - - org.aspectj - aspectjweaver - test - - - org.assertj - assertj-core - test - - - - - - - src/main/resources-filtered - true - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - \ No newline at end of file diff --git a/powertools-core/src/test/resources/unreadable.properties b/powertools-core/src/test/resources/unreadable.properties deleted file mode 100644 index 42ff4693f..000000000 --- a/powertools-core/src/test/resources/unreadable.properties +++ /dev/null @@ -1,2 +0,0 @@ -# This is intentionally left empty -# It used during testing and is set to un-readable to fulfil the test purposes. \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/batch/pom.xml b/powertools-e2e-tests/handlers/batch/pom.xml index 995121e2a..a36d464ea 100644 --- a/powertools-e2e-tests/handlers/batch/pom.xml +++ b/powertools-e2e-tests/handlers/batch/pom.xml @@ -5,7 +5,7 @@ software.amazon.lambda e2e-test-handlers-parent - 1.0.0 + 2.0.0-SNAPSHOT e2e-test-handler-batch @@ -19,7 +19,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j com.amazonaws @@ -29,14 +29,14 @@ com.amazonaws aws-lambda-java-serialization - - org.apache.logging.log4j - log4j-slf4j2-impl - software.amazon.awssdk dynamodb + + org.aspectj + aspectjrt + diff --git a/powertools-e2e-tests/handlers/batch/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/batch/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index 64f5a02c2..bfde65bc8 100644 --- a/powertools-e2e-tests/handlers/batch/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/batch/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -14,7 +14,6 @@ package software.amazon.lambda.powertools.e2e; -import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ObjectMapper; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; @@ -24,41 +23,26 @@ import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; -import com.amazonaws.services.lambda.runtime.serialization.factories.JacksonFactory; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonNode; - import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; import software.amazon.lambda.powertools.batch.BatchMessageHandlerBuilder; import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; import software.amazon.lambda.powertools.e2e.model.Product; -import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.utilities.JsonConfig; - -import javax.management.Attribute; public class Function implements RequestHandler { - private final static Logger LOGGER = LogManager.getLogger(Function.class); + private static final Logger LOGGER = LoggerFactory.getLogger(Function.class); private final BatchMessageHandler sqsHandler; private final BatchMessageHandler kinesisHandler; @@ -139,7 +123,7 @@ public Object createResult(String input, Context context) { SQSEvent event = serializer.fromJson(input); if (event.getRecords().get(0).getEventSource().equals("aws:sqs")) { LOGGER.info("Running for SQS"); - LOGGER.info(event); + LOGGER.info(event.toString()); return sqsHandler.processBatch(event, context); } diff --git a/powertools-e2e-tests/handlers/idempotency/pom.xml b/powertools-e2e-tests/handlers/idempotency/pom.xml index 25dfbfabf..e3a67a5b5 100644 --- a/powertools-e2e-tests/handlers/idempotency/pom.xml +++ b/powertools-e2e-tests/handlers/idempotency/pom.xml @@ -5,7 +5,7 @@ software.amazon.lambda e2e-test-handlers-parent - 1.0.0 + 2.0.0-SNAPSHOT e2e-test-handler-idempotency @@ -15,21 +15,20 @@ software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb software.amazon.lambda - powertools-logging - - - org.apache.logging.log4j - log4j-slf4j2-impl + powertools-logging-log4j com.amazonaws aws-lambda-java-events - + + org.aspectj + aspectjrt + @@ -44,7 +43,7 @@ software.amazon.lambda - powertools-idempotency + powertools-idempotency-core software.amazon.lambda diff --git a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index e4c2f2b9a..16109778d 100644 --- a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -27,7 +27,7 @@ import software.amazon.lambda.powertools.idempotency.Idempotency; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; import software.amazon.lambda.powertools.idempotency.Idempotent; -import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore; +import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.DynamoDBPersistenceStore; import software.amazon.lambda.powertools.logging.Logging; diff --git a/powertools-e2e-tests/handlers/largemessage/pom.xml b/powertools-e2e-tests/handlers/largemessage/pom.xml index c626f5f64..0728404bf 100644 --- a/powertools-e2e-tests/handlers/largemessage/pom.xml +++ b/powertools-e2e-tests/handlers/largemessage/pom.xml @@ -5,7 +5,7 @@ software.amazon.lambda e2e-test-handlers-parent - 1.0.0 + 2.0.0-SNAPSHOT e2e-test-handler-largemessage @@ -23,15 +23,15 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j com.amazonaws aws-lambda-java-events - org.apache.logging.log4j - log4j-slf4j2-impl + org.aspectj + aspectjrt diff --git a/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml b/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml index 9635efd87..b57063346 100644 --- a/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml +++ b/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml @@ -5,7 +5,7 @@ software.amazon.lambda e2e-test-handlers-parent - 1.0.0 + 2.0.0-SNAPSHOT e2e-test-handler-large-msg-idempotent @@ -15,7 +15,7 @@ software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb software.amazon.lambda @@ -23,17 +23,16 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j com.amazonaws aws-lambda-java-events - org.apache.logging.log4j - log4j-slf4j2-impl + org.aspectj + aspectjrt - @@ -48,7 +47,7 @@ software.amazon.lambda - powertools-idempotency + powertools-idempotency-core software.amazon.lambda diff --git a/powertools-e2e-tests/handlers/largemessage_idempotent/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/largemessage_idempotent/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index 5269b37c9..6c9d4e8cf 100644 --- a/powertools-e2e-tests/handlers/largemessage_idempotent/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/largemessage_idempotent/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -37,7 +37,7 @@ import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; import software.amazon.lambda.powertools.idempotency.IdempotencyKey; import software.amazon.lambda.powertools.idempotency.Idempotent; -import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore; +import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.DynamoDBPersistenceStore; import software.amazon.lambda.powertools.largemessages.LargeMessage; import software.amazon.lambda.powertools.logging.Logging; diff --git a/powertools-e2e-tests/handlers/logging/pom.xml b/powertools-e2e-tests/handlers/logging/pom.xml index 61ec6b414..88feda09b 100644 --- a/powertools-e2e-tests/handlers/logging/pom.xml +++ b/powertools-e2e-tests/handlers/logging/pom.xml @@ -5,7 +5,7 @@ software.amazon.lambda e2e-test-handlers-parent - 1.0.0 + 2.0.0-SNAPSHOT e2e-test-handler-logging @@ -15,7 +15,12 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j + + + org.apache.logging.log4j + log4j-layout-template-json + 2.20.0 org.aspectj @@ -25,7 +30,6 @@ com.amazonaws aws-lambda-java-events - diff --git a/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index 62ebabc6e..58492653a 100644 --- a/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/logging/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -16,19 +16,17 @@ 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; public class Function implements RequestHandler { - - private static final Logger LOG = LogManager.getLogger(Function.class); + private static final Logger LOG = LoggerFactory.getLogger(Function.class); @Logging public String handleRequest(Input input, Context context) { - - LoggingUtils.appendKeys(input.getKeys()); + input.getKeys().forEach(MDC::put); LOG.info(input.getMessage()); return "OK"; diff --git a/powertools-e2e-tests/handlers/metrics/pom.xml b/powertools-e2e-tests/handlers/metrics/pom.xml index 35db53899..68059e67e 100644 --- a/powertools-e2e-tests/handlers/metrics/pom.xml +++ b/powertools-e2e-tests/handlers/metrics/pom.xml @@ -5,12 +5,12 @@ software.amazon.lambda e2e-test-handlers-parent - 1.0.0 + 2.0.0-SNAPSHOT e2e-test-handler-metrics jar - A Lambda function using Powertools for AWS Lambda (Java) Parameters + A Lambda function using Powertools for AWS Lambda (Java) Metrics @@ -21,7 +21,10 @@ com.amazonaws aws-lambda-java-events - + + org.aspectj + aspectjrt + diff --git a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index d9cf575c3..7244b3212 100644 --- a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -16,25 +16,36 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; -import software.amazon.lambda.powertools.metrics.MetricsUtils; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.time.Instant; public class Function implements RequestHandler { - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); + Metrics metrics = MetricsFactory.getMetricsInstance(); - @Metrics(captureColdStart = true) + @FlushMetrics(captureColdStart = true) public String handleRequest(Input input, Context context) { + Instant currentTimeTruncatedPlusThirty = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES) + .toInstant(ZoneOffset.UTC).plusSeconds(30); + metrics.setTimestamp(currentTimeTruncatedPlusThirty); + DimensionSet dimensionSet = new DimensionSet(); input.getDimensions().forEach((key, value) -> dimensionSet.addDimension(key, value)); - metricsLogger.putDimensions(dimensionSet); + metrics.addDimension(dimensionSet); - input.getMetrics().forEach((key, value) -> metricsLogger.putMetric(key, value, Unit.COUNT)); + input.getMetrics().forEach((key, value) -> metrics.addMetric(key, value, MetricUnit.COUNT, + input.getHighResolution().equalsIgnoreCase("true") ? MetricResolution.HIGH + : MetricResolution.STANDARD)); return "OK"; } -} \ No newline at end of file +} diff --git a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java index 18c4eb747..1328ded77 100644 --- a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java +++ b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Input.java @@ -21,6 +21,8 @@ public class Input { private Map dimensions; + private String highResolution; + public Input() { } @@ -32,6 +34,14 @@ public void setMetrics(Map metrics) { this.metrics = metrics; } + public String getHighResolution() { + return highResolution; + } + + public void setHighResolution(String highResolution) { + this.highResolution = highResolution; + } + public Map getDimensions() { return dimensions; } diff --git a/powertools-e2e-tests/handlers/parameters/pom.xml b/powertools-e2e-tests/handlers/parameters/pom.xml index 410cdfb5e..2d6a9a06a 100644 --- a/powertools-e2e-tests/handlers/parameters/pom.xml +++ b/powertools-e2e-tests/handlers/parameters/pom.xml @@ -5,27 +5,30 @@ software.amazon.lambda e2e-test-handlers-parent - 1.0.0 + 2.0.0-SNAPSHOT e2e-test-handler-parameters jar - A Lambda function using powertools logging + A Lambda function using powertools parameters software.amazon.lambda - powertools-logging + powertools-logging-log4j software.amazon.lambda - powertools-parameters + powertools-parameters-appconfig com.amazonaws aws-lambda-java-events - + + org.aspectj + aspectjrt + diff --git a/powertools-e2e-tests/handlers/parameters/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/parameters/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index 3a83a1b05..1e151825e 100644 --- a/powertools-e2e-tests/handlers/parameters/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/parameters/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -17,14 +17,18 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.parameters.AppConfigProvider; -import software.amazon.lambda.powertools.parameters.ParamManager; +import software.amazon.lambda.powertools.parameters.appconfig.AppConfigProvider; public class Function implements RequestHandler { @Logging public String handleRequest(Input input, Context context) { - AppConfigProvider provider = ParamManager.getAppConfigProvider(input.getEnvironment(), input.getApp()); + AppConfigProvider provider = AppConfigProvider.builder() + .withApplication(input.getApp()) + .withEnvironment(input.getEnvironment()) + .build(); + + //(input.getEnvironment(), input.getApp()); return provider.get(input.getKey()); } diff --git a/powertools-e2e-tests/handlers/pom.xml b/powertools-e2e-tests/handlers/pom.xml index 799ed465e..b55cf436a 100644 --- a/powertools-e2e-tests/handlers/pom.xml +++ b/powertools-e2e-tests/handlers/pom.xml @@ -4,16 +4,16 @@ software.amazon.lambda e2e-test-handlers-parent - 1.0.0 + 2.0.0-SNAPSHOT pom Handlers for End-to-End tests Fake handlers that use Powertools for AWS Lambda (Java). - 1.19.0-SNAPSHOT + 2.0.0-SNAPSHOT UTF-8 - 1.8 - 1.8 + 11 + 11 1.2.2 1.1.2 @@ -22,15 +22,20 @@ 1.13.1 3.11.0 2.20.108 - 2.20.0 + 1.9.20.1 + batch + largemessage + largemessage_idempotent logging tracing metrics idempotency parameters + validation-alb-event + validation-apigw-event @@ -49,7 +54,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${lambda.powertools.version} @@ -69,7 +74,12 @@ software.amazon.lambda - powertools-parameters + powertools-idempotency-dynamodb + ${lambda.powertools.version} + + + software.amazon.lambda + powertools-parameters-appconfig ${lambda.powertools.version} @@ -82,6 +92,11 @@ powertools-batch ${lambda.powertools.version} + + software.amazon.lambda + powertools-validation + ${lambda.powertools.version} + com.amazonaws aws-lambda-java-core @@ -97,11 +112,6 @@ aws-lambda-java-serialization ${lambda.java.serialization} - - org.apache.logging.log4j - log4j-slf4j2-impl - ${log4j.version} - @@ -182,9 +192,9 @@ - jdk8to16 + jdk11to16 - [1.8,16] + [11,16] 1.9.7 diff --git a/powertools-e2e-tests/handlers/tracing/pom.xml b/powertools-e2e-tests/handlers/tracing/pom.xml index 252009aa9..b96fcef0a 100644 --- a/powertools-e2e-tests/handlers/tracing/pom.xml +++ b/powertools-e2e-tests/handlers/tracing/pom.xml @@ -5,7 +5,7 @@ software.amazon.lambda e2e-test-handlers-parent - 1.0.0 + 2.0.0-SNAPSHOT e2e-test-handler-tracing @@ -21,7 +21,10 @@ com.amazonaws aws-lambda-java-events - + + org.aspectj + aspectjrt + diff --git a/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index 397e34a85..0f140a20d 100644 --- a/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -52,4 +52,4 @@ private String buildMessage(String message, String funcName) { } return String.format("%s (%s)", message, funcName); } -} \ No newline at end of file +} diff --git a/powertools-e2e-tests/handlers/validation-alb-event/pom.xml b/powertools-e2e-tests/handlers/validation-alb-event/pom.xml new file mode 100644 index 000000000..be50094c1 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-alb-event/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + + + software.amazon.lambda + e2e-test-handlers-parent + 2.0.0-SNAPSHOT + + + e2e-test-handler-validation-alb-event + jar + A Lambda function using Powertools for AWS Lambda (Java) validation + + + + software.amazon.lambda + powertools-validation + + + com.amazonaws + aws-lambda-java-events + + + org.aspectj + aspectjrt + + + + + + + dev.aspectj + aspectj-maven-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-validation + + + + + + + compile + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + diff --git a/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java new file mode 100644 index 000000000..d221ee153 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.e2e; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent; +import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; + +import software.amazon.lambda.powertools.validation.Validation; + +public class Function + implements RequestHandler { + @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") + public ApplicationLoadBalancerResponseEvent handleRequest(ApplicationLoadBalancerRequestEvent input, + Context context) { + ApplicationLoadBalancerResponseEvent response = new ApplicationLoadBalancerResponseEvent(); + response.setBody(input.getBody()); + response.setStatusCode(200); + response.setIsBase64Encoded(false); + return response; + } +} \ No newline at end of file diff --git a/examples/powertools-examples-sqs/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/log4j2.xml similarity index 94% rename from examples/powertools-examples-sqs/src/main/resources/log4j2.xml rename to powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/log4j2.xml index e1fd14cea..8925f70b9 100644 --- a/examples/powertools-examples-sqs/src/main/resources/log4j2.xml +++ b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/log4j2.xml @@ -6,11 +6,11 @@ + + + - - - \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/inbound_schema.json b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/inbound_schema.json new file mode 100644 index 000000000..3665879eb --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/inbound_schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "price" + ], + "properties": { + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMinimum": 0, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/outbound_schema.json b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/outbound_schema.json new file mode 100644 index 000000000..b1f14d025 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/outbound_schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "price" + ], + "properties": { + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMaximum": 1000, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/pom.xml b/powertools-e2e-tests/handlers/validation-apigw-event/pom.xml new file mode 100644 index 000000000..f204a8a9f --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + + + software.amazon.lambda + e2e-test-handlers-parent + 2.0.0-SNAPSHOT + + + e2e-test-handler-validation-apigw-event + jar + A Lambda function using Powertools for AWS Lambda (Java) validation + + + + software.amazon.lambda + powertools-validation + + + com.amazonaws + aws-lambda-java-events + + + org.aspectj + aspectjrt + + + + + + + dev.aspectj + aspectj-maven-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-validation + + + + + + + compile + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java new file mode 100644 index 000000000..5ac8951d8 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.e2e; + +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 software.amazon.lambda.powertools.validation.Validation; + +public class Function implements RequestHandler { + @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent(); + response.setBody(input.getBody()); + response.setStatusCode(200); + response.setIsBase64Encoded(false); + return response; + } +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml new file mode 100644 index 000000000..8925f70b9 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json new file mode 100644 index 000000000..3665879eb --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "price" + ], + "properties": { + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMinimum": 0, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json new file mode 100644 index 000000000..b1f14d025 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "price" + ], + "properties": { + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMaximum": 1000, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 450d1f5c1..ce9e12860 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -14,24 +14,24 @@ --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 powertools-parent software.amazon.lambda - 1.19.0-SNAPSHOT + 2.0.0-RC1 powertools-e2e-tests - Powertools for AWS Lambda (Java)library End-to-end tests + Powertools for AWS Lambda (Java) - End-to-end tests Powertools for AWS Lambda (Java)End-To-End Tests - 1.8 - 1.8 - 10.3.0 - 2.130.0 + 11 + 11 + 10.4.2 + 2.186.0 @@ -84,7 +84,7 @@ com.amazonaws amazon-sqs-java-extended-client-lib - 2.0.4 + 2.1.0 test @@ -103,7 +103,7 @@ commons-io commons-io - 2.15.1 + 2.16.1 @@ -159,7 +159,7 @@ org.yaml snakeyaml - 2.2 + 2.4 test @@ -174,34 +174,36 @@ - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - ${maven.compiler.source} - ${maven.compiler.target} - UTF-8 - - - - - + + default + + true + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.2 + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${maven.compiler.source} + ${maven.compiler.target} + UTF-8 + + + + + e2e @@ -209,7 +211,7 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.1.2 + 3.5.3 @@ -219,10 +221,14 @@ - 1 + 1 **/*E2ET.java + + **/TracingE2ET.java + @@ -230,4 +236,4 @@ - \ No newline at end of file + diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java index 548a710b8..d9c3ef749 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java @@ -3,8 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; -import com.amazon.sqs.javamessaging.AmazonSQSExtendedClient; -import com.amazon.sqs.javamessaging.ExtendedClientConfiguration; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -14,6 +12,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; + import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -22,6 +21,10 @@ import org.junit.jupiter.api.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import com.amazon.sqs.javamessaging.AmazonSQSExtendedClient; +import com.amazon.sqs.javamessaging.ExtendedClientConfiguration; + import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; @@ -101,21 +104,20 @@ public void reset() { @Test public void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException, InterruptedException { // given - final ExtendedClientConfiguration extendedClientConfig = - new ExtendedClientConfiguration() - .withPayloadSupportEnabled(s3Client, bucketName); - AmazonSQSExtendedClient client = - new AmazonSQSExtendedClient(SqsClient.builder().httpClient(httpClient).build(), extendedClientConfig); - InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt"); - String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // when - client.sendMessage(SendMessageRequest - .builder() - .queueUrl(queueUrl) - .messageBody(bigMessage) - .build()); - + final ExtendedClientConfiguration extendedClientConfig = new ExtendedClientConfiguration() + .withPayloadSupportEnabled(s3Client, bucketName); + try (AmazonSQSExtendedClient client = new AmazonSQSExtendedClient( + SqsClient.builder().region(region).httpClient(httpClient).build(), extendedClientConfig)) { + InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt"); + String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // when + client.sendMessage(SendMessageRequest + .builder() + .queueUrl(queueUrl) + .messageBody(bigMessage) + .build()); + } Thread.sleep(30000); // wait for function to be executed // then @@ -137,36 +139,37 @@ public void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException, In @Test public void smallSQSMessage_shouldNotReadFromS3() throws IOException, InterruptedException { // given - final ExtendedClientConfiguration extendedClientConfig = - new ExtendedClientConfiguration() - .withPayloadSupportEnabled(s3Client, bucketName); - AmazonSQSExtendedClient client = - new AmazonSQSExtendedClient(SqsClient.builder().httpClient(httpClient).build(), extendedClientConfig); - String message = "Hello World"; - - // when - client.sendMessage(SendMessageRequest - .builder() - .queueUrl(queueUrl) - .messageBody(message) - .build()); - - Thread.sleep(30000); // wait for function to be executed - - // then - QueryRequest request = QueryRequest - .builder() - .tableName(tableName) - .keyConditionExpression("functionName = :func") - .expressionAttributeValues( - Collections.singletonMap(":func", AttributeValue.builder().s(functionName).build())) - .build(); - QueryResponse response = dynamoDbClient.query(request); - List> items = response.items(); - assertThat(items).hasSize(1); - messageId = items.get(0).get("id").s(); - assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo( - message.getBytes(StandardCharsets.UTF_8).length); - assertThat(items.get(0).get("bodyMD5").s()).isEqualTo("b10a8db164e0754105b7a99be72e3fe5"); + final ExtendedClientConfiguration extendedClientConfig = new ExtendedClientConfiguration() + .withPayloadSupportEnabled(s3Client, bucketName); + try (AmazonSQSExtendedClient client = new AmazonSQSExtendedClient( + SqsClient.builder().region(region).httpClient(httpClient).build(), + extendedClientConfig)) { + String message = "Hello World"; + + // when + client.sendMessage(SendMessageRequest + .builder() + .queueUrl(queueUrl) + .messageBody(message) + .build()); + + Thread.sleep(30000); // wait for function to be executed + + // then + QueryRequest request = QueryRequest + .builder() + .tableName(tableName) + .keyConditionExpression("functionName = :func") + .expressionAttributeValues( + Collections.singletonMap(":func", AttributeValue.builder().s(functionName).build())) + .build(); + QueryResponse response = dynamoDbClient.query(request); + List> items = response.items(); + assertThat(items).hasSize(1); + messageId = items.get(0).get("id").s(); + assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo( + message.getBytes(StandardCharsets.UTF_8).length); + assertThat(items.get(0).get("bodyMD5").s()).isEqualTo("b10a8db164e0754105b7a99be72e3fe5"); + } } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java index b060879d3..f78500c65 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java @@ -83,7 +83,7 @@ public void test_logInfoWithAdditionalKeys() throws JsonProcessingException { JsonNode jsonNode = objectMapper.readTree(functionLogs[0]); assertThat(jsonNode.get("message").asText()).isEqualTo("New Order"); assertThat(jsonNode.get("orderId").asText()).isEqualTo(orderId); - assertThat(jsonNode.get("coldStart").asBoolean()).isTrue(); + assertThat(jsonNode.get("cold_start").asBoolean()).isTrue(); assertThat(jsonNode.get("xray_trace_id").asText()).isNotBlank(); assertThat(jsonNode.get("function_request_id").asText()).isEqualTo(invocationResult1.getRequestId()); @@ -91,6 +91,6 @@ public void test_logInfoWithAdditionalKeys() throws JsonProcessingException { functionLogs = invocationResult2.getLogs().getFunctionLogs(INFO); assertThat(functionLogs).hasSize(1); jsonNode = objectMapper.readTree(functionLogs[0]); - assertThat(jsonNode.get("coldStart").asBoolean()).isFalse(); + assertThat(jsonNode.get("cold_start").asBoolean()).isFalse(); } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java index 80673b995..2765e0e70 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java @@ -18,6 +18,9 @@ import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; +import java.time.Clock; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.List; import java.util.Map; @@ -25,10 +28,12 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; + import software.amazon.lambda.powertools.testutils.Infrastructure; import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; import software.amazon.lambda.powertools.testutils.metrics.MetricsFetcher; @@ -47,9 +52,9 @@ public static void setup() { .pathToFunction("metrics") .environmentVariables( Stream.of(new String[][] { - {"POWERTOOLS_METRICS_NAMESPACE", namespace}, - {"POWERTOOLS_SERVICE_NAME", service} - }) + { "POWERTOOLS_METRICS_NAMESPACE", namespace }, + { "POWERTOOLS_SERVICE_NAME", service } + }) .collect(Collectors.toMap(data -> data[0], data -> data[1]))) .build(); Map outputs = infrastructure.deploy(); @@ -66,36 +71,55 @@ public static void tearDown() { @Test public void test_recordMetrics() { // GIVEN - String event1 = - "{ \"metrics\": {\"orders\": 1, \"products\": 4}, \"dimensions\": { \"Environment\": \"test\"} }"; + Instant currentTimeTruncatedToMinutes = Instant.now(Clock.systemUTC()).truncatedTo(ChronoUnit.MINUTES); + String event1 = "{ \"metrics\": {\"orders\": 1, \"products\": 4}, \"dimensions\": { \"Environment\": \"test\"}, \"highResolution\": \"false\"}"; + + String event2 = "{ \"metrics\": {\"orders\": 1, \"products\": 8}, \"dimensions\": { \"Environment\": \"test\"}, \"highResolution\": \"true\"}"; // WHEN InvocationResult invocationResult = invokeFunction(functionName, event1); + invokeFunction(functionName, event2); + // THEN MetricsFetcher metricsFetcher = new MetricsFetcher(); - List coldStart = - metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, - "ColdStart", Stream.of(new String[][] { - {"FunctionName", functionName}, - {"Service", service}} - ).collect(Collectors.toMap(data -> data[0], data -> data[1]))); + List coldStart = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, + namespace, + "ColdStart", Stream.of(new String[][] { + { "FunctionName", functionName }, + { "Service", service } }).collect(Collectors.toMap(data -> data[0], data -> data[1]))); assertThat(coldStart.get(0)).isEqualTo(1); - List orderMetrics = - metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, - "orders", Collections.singletonMap("Environment", "test")); - assertThat(orderMetrics.get(0)).isEqualTo(1); - List productMetrics = - metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, - "products", Collections.singletonMap("Environment", "test")); - assertThat(productMetrics.get(0)).isEqualTo(4); - orderMetrics = - metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, - "orders", Collections.singletonMap("Service", service)); - assertThat(orderMetrics.get(0)).isEqualTo(1); - productMetrics = - metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, - "products", Collections.singletonMap("Service", service)); - assertThat(productMetrics.get(0)).isEqualTo(4); + List orderMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), + 60, namespace, + "orders", Collections.singletonMap("Environment", "test")); + assertThat(orderMetrics.get(0)).isEqualTo(2); + List productMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), + invocationResult.getEnd(), 60, namespace, + "products", Collections.singletonMap("Environment", "test")); + + // When searching across a 1 minute time period with a period of 60 we find both metrics and the sum is 12 + + assertThat(productMetrics.get(0)).isEqualTo(12); + + orderMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, + namespace, + "orders", Collections.singletonMap("Service", service)); + assertThat(orderMetrics.get(0)).isEqualTo(2); + productMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, + namespace, + "products", Collections.singletonMap("Service", service)); + assertThat(productMetrics.get(0)).isEqualTo(12); + + Instant searchStartTime = currentTimeTruncatedToMinutes.plusSeconds(15); + Instant searchEndTime = currentTimeTruncatedToMinutes.plusSeconds(45); + + List productMetricDataResult = metricsFetcher.fetchMetrics(searchStartTime, searchEndTime, 1, namespace, + "products", Collections.singletonMap("Environment", "test")); + + // We are searching across the time period the metric was created but with a period of 1 second. Only the high + // resolution metric will be available at this point + + assertThat(productMetricDataResult.get(0)).isEqualTo(8); + } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java index 0827d91ae..d2a5ceed1 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java @@ -18,14 +18,15 @@ import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; -import java.util.Collections; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; + import software.amazon.lambda.powertools.testutils.Infrastructure; import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; import software.amazon.lambda.powertools.testutils.tracing.SegmentDocument.SubSegment; @@ -45,7 +46,9 @@ public static void setup() { .testName(TracingE2ET.class.getSimpleName()) .pathToFunction("tracing") .tracing(true) - .environmentVariables(Collections.singletonMap("POWERTOOLS_SERVICE_NAME", service)) + .environmentVariables( + Map.of("POWERTOOLS_SERVICE_NAME", service, + "POWERTOOLS_TRACER_CAPTURE_RESPONSE", "true")) .build(); Map outputs = infrastructure.deploy(); functionName = outputs.get(FUNCTION_NAME_OUTPUT); @@ -61,45 +64,58 @@ public static void tearDown() { @Test public void test_tracing() { // GIVEN - String message = "Hello World"; - String event = String.format("{\"message\":\"%s\"}", message); - String result = String.format("%s (%s)", message, functionName); + final String message = "Hello World"; + final String event = String.format("{\"message\":\"%s\"}", message); + final String result = String.format("%s (%s)", message, functionName); // WHEN - InvocationResult invocationResult = invokeFunction(functionName, event); + final InvocationResult invocationResult = invokeFunction(functionName, event); // THEN - Trace trace = TraceFetcher.builder() + final Trace trace = TraceFetcher.builder() .start(invocationResult.getStart()) .end(invocationResult.getEnd()) .functionName(functionName) .build() .fetchTrace(); - assertThat(trace.getSubsegments()).hasSize(1); - SubSegment handleRequest = trace.getSubsegments().get(0); - assertThat(handleRequest.getName()).isEqualTo("## handleRequest"); - assertThat(handleRequest.getAnnotations()).hasSize(2); - assertThat(handleRequest.getAnnotations().get("ColdStart")).isEqualTo(true); - assertThat(handleRequest.getAnnotations().get("Service")).isEqualTo(service); - assertThat(handleRequest.getMetadata()).hasSize(1); - Map metadata = (Map) handleRequest.getMetadata().get(service); - assertThat(metadata.get("handleRequest response")).isEqualTo(result); - assertThat(handleRequest.getSubsegments()).hasSize(2); - - SubSegment sub = handleRequest.getSubsegments().get(0); + assertThat(trace.getSubsegments()).hasSize(2); + + // We need to filter segments based on name because they are not returned in-order from the X-Ray API + // The Init segment is created by default for Lambda functions in X-Ray + final SubSegment initSegment = trace.getSubsegments().stream() + .filter(subSegment -> subSegment.getName().equals("Init")) + .findFirst().orElse(null); + assertThat(initSegment.getName()).isEqualTo("Init"); + assertThat(initSegment.getAnnotations()).isNull(); + + final SubSegment handleRequestSegment = trace.getSubsegments().stream() + .filter(subSegment -> subSegment.getName().equals("## handleRequest")) + .findFirst().orElse(null); + assertThat(handleRequestSegment.getName()).isEqualTo("## handleRequest"); + assertThat(handleRequestSegment.getAnnotations()).hasSize(2); + assertThat(handleRequestSegment.getAnnotations()).containsEntry("ColdStart", true); + assertThat(handleRequestSegment.getAnnotations()).containsEntry("Service", service); + assertThat(handleRequestSegment.getMetadata()).hasSize(1); + final Map metadata = (Map) handleRequestSegment.getMetadata().get(service); + assertThat(metadata).containsEntry("handleRequest response", result); + assertThat(handleRequestSegment.getSubsegments()).hasSize(2); + + SubSegment sub = handleRequestSegment.getSubsegments().get(0); assertThat(sub.getName()).isIn("## internal_stuff", "## buildMessage"); - sub = handleRequest.getSubsegments().get(1); + sub = handleRequestSegment.getSubsegments().get(1); assertThat(sub.getName()).isIn("## internal_stuff", "## buildMessage"); - SubSegment buildMessage = handleRequest.getSubsegments().stream() - .filter(subSegment -> subSegment.getName().equals("## buildMessage")).findFirst().orElse(null); + SubSegment buildMessage = handleRequestSegment.getSubsegments().stream() + .filter(subSegment -> subSegment.getName().equals("## buildMessage")) + .findFirst().orElse(null); assertThat(buildMessage).isNotNull(); assertThat(buildMessage.getAnnotations()).hasSize(1); - assertThat(buildMessage.getAnnotations().get("message")).isEqualTo(message); + assertThat(buildMessage.getAnnotations()).containsEntry("message", message); assertThat(buildMessage.getMetadata()).hasSize(1); - metadata = (Map) buildMessage.getMetadata().get(service); - assertThat(metadata.get("buildMessage response")).isEqualTo(result); + final Map buildMessageSegmentMetadata = (Map) buildMessage.getMetadata() + .get(service); + assertThat(buildMessageSegmentMetadata).containsEntry("buildMessage response", result); } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java new file mode 100644 index 000000000..41696943a --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java @@ -0,0 +1,104 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; +import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; + +class ValidationALBE2ET { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static Infrastructure infrastructure; + private static String functionName; + + @BeforeAll + @Timeout(value = 5, unit = TimeUnit.MINUTES) + public static void setup() { + infrastructure = Infrastructure.builder().testName(ValidationALBE2ET.class.getSimpleName()) + .pathToFunction("validation-alb-event").build(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); + } + + @AfterAll + public static void tearDown() { + if (infrastructure != null) { + infrastructure.destroy(); + } + } + + @Test + void test_validInboundSQSEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_alb_in_out_event.json"); + String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, validEvent); + + // THEN + // invocation should pass validation and return 200 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); + assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + } + + @Test + void test_invalidInboundSQSEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_in_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail inbound validation and return an error message + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("errorMessage").asText()).contains(": required property 'price' not found"); + } + + @Test + void test_invalidOutboundSQSEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_out_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail outbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("errorMessage").asText()).contains("/price: must have an exclusive maximum value of 1000"); + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java new file mode 100644 index 000000000..425399c95 --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java @@ -0,0 +1,107 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; +import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; + +class ValidationApiGWE2ET { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static Infrastructure infrastructure; + private static String functionName; + + @BeforeAll + @Timeout(value = 5, unit = TimeUnit.MINUTES) + public static void setup() { + infrastructure = Infrastructure.builder().testName(ValidationApiGWE2ET.class.getSimpleName()) + .pathToFunction("validation-apigw-event").build(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); + } + + @AfterAll + public static void tearDown() { + if (infrastructure != null) { + infrastructure.destroy(); + } + } + + @Test + void test_validInboundApiGWEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_api_gw_in_out_event.json"); + String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, validEvent); + + // THEN + // invocation should pass validation and return 200 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); + assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + } + + @Test + void test_invalidInboundApiGWEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_in_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail inbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); + assertThat(validJsonNode.get("body").asText()).contains(": required property 'price' not found"); + } + + @Test + void test_invalidOutboundApiGWEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_out_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail outbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); + assertThat(validJsonNode.get("body").asText()) + .contains("/price: must have an exclusive maximum value of 1000"); + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java index 28a0f2bb4..143409989 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java @@ -16,7 +16,6 @@ import static java.util.Collections.singletonList; -import com.fasterxml.jackson.databind.JsonNode; import java.io.File; import java.io.IOException; import java.nio.file.Paths; @@ -27,13 +26,18 @@ import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; + +import com.fasterxml.jackson.databind.JsonNode; + import software.amazon.awscdk.App; import software.amazon.awscdk.BundlingOptions; import software.amazon.awscdk.BundlingOutput; import software.amazon.awscdk.CfnOutput; +import software.amazon.awscdk.DefaultStackSynthesizer; import software.amazon.awscdk.DockerVolume; import software.amazon.awscdk.Duration; import software.amazon.awscdk.RemovalPolicy; @@ -45,7 +49,11 @@ import software.amazon.awscdk.services.appconfig.CfnDeploymentStrategy; import software.amazon.awscdk.services.appconfig.CfnEnvironment; import software.amazon.awscdk.services.appconfig.CfnHostedConfigurationVersion; -import software.amazon.awscdk.services.dynamodb.*; +import software.amazon.awscdk.services.dynamodb.Attribute; +import software.amazon.awscdk.services.dynamodb.AttributeType; +import software.amazon.awscdk.services.dynamodb.BillingMode; +import software.amazon.awscdk.services.dynamodb.StreamViewType; +import software.amazon.awscdk.services.dynamodb.Table; import software.amazon.awscdk.services.iam.PolicyStatement; import software.amazon.awscdk.services.kinesis.Stream; import software.amazon.awscdk.services.kinesis.StreamMode; @@ -89,7 +97,8 @@ * CloudWatch log groups, ... *
* It uses the Cloud Development Kit (CDK) to define required resources. The CDK stack is then synthesized to retrieve - * the CloudFormation templates and the assets (function jars). Assets are uploaded to S3 (with the SDK `PutObjectRequest`) + * the CloudFormation templates and the assets (function jars). Assets are uploaded to S3 (with the SDK + * `PutObjectRequest`) * and the CloudFormation stack is created (with the SDK `createStack`) */ public class Infrastructure { @@ -174,11 +183,11 @@ public Map deploy() { .onFailure(OnFailure.ROLLBACK) .capabilities(Capability.CAPABILITY_IAM) .build()); - WaiterResponse waiterResponse = - cfn.waiter().waitUntilStackCreateComplete(DescribeStacksRequest.builder().stackName(stackName).build()); + WaiterResponse waiterResponse = cfn.waiter() + .waitUntilStackCreateComplete(DescribeStacksRequest.builder().stackName(stackName).build()); if (waiterResponse.matched().response().isPresent()) { - software.amazon.awssdk.services.cloudformation.model.Stack deployedStack = - waiterResponse.matched().response().get().stacks().get(0); + software.amazon.awssdk.services.cloudformation.model.Stack deployedStack = waiterResponse.matched() + .response().get().stacks().get(0); LOG.info("Stack " + deployedStack.stackName() + " successfully deployed"); Map outputs = new HashMap<>(); deployedStack.outputs().forEach(output -> outputs.put(output.outputKey(), output.outputValue())); @@ -203,7 +212,11 @@ public void destroy() { */ private Stack createStackWithLambda() { boolean createTableForAsyncTests = false; - Stack stack = new Stack(app, stackName); + final Stack e2eStack = Stack.Builder.create(app, stackName) + .synthesizer(DefaultStackSynthesizer.Builder.create() + .generateBootstrapVersionRule(false) // Disable bootstrap version check + .build()) + .build(); List packagingInstruction = Arrays.asList( "/bin/sh", @@ -213,8 +226,7 @@ private Stack createStackWithLambda() { " -Dmaven.test.skip=true " + " -Dmaven.compiler.source=" + runtime.getMvnProperty() + " -Dmaven.compiler.target=" + runtime.getMvnProperty() + - " && cp /asset-input/" + pathToFunction + "/target/function.jar /asset-output/" - ); + " && cp /asset-input/" + pathToFunction + "/target/function.jar /asset-output/"); BundlingOptions.Builder builderOptions = BundlingOptions.builder() .command(packagingInstruction) @@ -224,20 +236,19 @@ private Stack createStackWithLambda() { DockerVolume.builder() .hostPath(System.getProperty("user.home") + "/.m2/") .containerPath("/root/.m2/") - .build() - )) + .build())) .user("root") .outputType(BundlingOutput.ARCHIVED); functionName = stackName + "-function"; - CfnOutput.Builder.create(stack, FUNCTION_NAME_OUTPUT) + CfnOutput.Builder.create(e2eStack, FUNCTION_NAME_OUTPUT) .value(functionName) .build(); LOG.debug("Building Lambda function with command " + packagingInstruction.stream().collect(Collectors.joining(" ", "[", "]"))); Function function = Function.Builder - .create(stack, functionName) + .create(e2eStack, functionName) .code(Code.fromAsset("handlers/", AssetOptions.builder() .bundling(builderOptions .command(packagingInstruction) @@ -253,7 +264,7 @@ private Stack createStackWithLambda() { .build(); LogGroup.Builder - .create(stack, functionName + "-logs") + .create(e2eStack, functionName + "-logs") .logGroupName("/aws/lambda/" + functionName) .retention(RetentionDays.ONE_DAY) .removalPolicy(RemovalPolicy.DESTROY) @@ -261,7 +272,7 @@ private Stack createStackWithLambda() { if (!StringUtils.isEmpty(idempotencyTable)) { Table table = Table.Builder - .create(stack, "IdempotencyTable") + .create(e2eStack, "IdempotencyTable") .billingMode(BillingMode.PAY_PER_REQUEST) .removalPolicy(RemovalPolicy.DESTROY) .partitionKey(Attribute.builder().name("id").type(AttributeType.STRING).build()) @@ -275,7 +286,7 @@ private Stack createStackWithLambda() { if (!StringUtils.isEmpty(queue)) { Queue sqsQueue = Queue.Builder - .create(stack, "SQSQueue") + .create(e2eStack, "SQSQueue") .queueName(queue) .visibilityTimeout(Duration.seconds(timeout * 6)) .retentionPeriod(Duration.seconds(timeout * 6)) @@ -293,14 +304,14 @@ private Stack createStackWithLambda() { .build(); function.addEventSource(sqsEventSource); CfnOutput.Builder - .create(stack, "QueueURL") + .create(e2eStack, "QueueURL") .value(sqsQueue.getQueueUrl()) .build(); createTableForAsyncTests = true; } if (!StringUtils.isEmpty(kinesisStream)) { Stream stream = Stream.Builder - .create(stack, "KinesisStream") + .create(e2eStack, "KinesisStream") .streamMode(StreamMode.ON_DEMAND) .streamName(kinesisStream) .build(); @@ -316,13 +327,13 @@ private Stack createStackWithLambda() { .build(); function.addEventSource(kinesisEventSource); CfnOutput.Builder - .create(stack, "KinesisStreamName") + .create(e2eStack, "KinesisStreamName") .value(stream.getStreamName()) .build(); } if (!StringUtils.isEmpty(ddbStreamsTableName)) { - Table ddbStreamsTable = Table.Builder.create(stack, "DDBStreamsTable") + Table ddbStreamsTable = Table.Builder.create(e2eStack, "DDBStreamsTable") .tableName(ddbStreamsTableName) .stream(StreamViewType.KEYS_ONLY) .removalPolicy(RemovalPolicy.DESTROY) @@ -336,12 +347,12 @@ private Stack createStackWithLambda() { .reportBatchItemFailures(true) .build(); function.addEventSource(ddbEventSource); - CfnOutput.Builder.create(stack, "DdbStreamsTestTable").value(ddbStreamsTable.getTableName()).build(); + CfnOutput.Builder.create(e2eStack, "DdbStreamsTestTable").value(ddbStreamsTable.getTableName()).build(); } if (!StringUtils.isEmpty(largeMessagesBucket)) { Bucket offloadBucket = Bucket.Builder - .create(stack, "LargeMessagesOffloadBucket") + .create(e2eStack, "LargeMessagesOffloadBucket") .removalPolicy(RemovalPolicy.RETAIN) // autodelete does not work without cdk deploy .bucketName(largeMessagesBucket) .build(); @@ -352,19 +363,19 @@ private Stack createStackWithLambda() { if (appConfig != null) { CfnApplication app = CfnApplication.Builder - .create(stack, "AppConfigApp") + .create(e2eStack, "AppConfigApp") .name(appConfig.getApplication()) .build(); CfnEnvironment environment = CfnEnvironment.Builder - .create(stack, "AppConfigEnvironment") + .create(e2eStack, "AppConfigEnvironment") .applicationId(app.getRef()) .name(appConfig.getEnvironment()) .build(); // Create a fast deployment strategy, so we don't have to wait ages CfnDeploymentStrategy fastDeployment = CfnDeploymentStrategy.Builder - .create(stack, "AppConfigDeployment") + .create(e2eStack, "AppConfigDeployment") .name("FastDeploymentStrategy") .deploymentDurationInMinutes(0) .finalBakeTimeInMinutes(0) @@ -377,20 +388,19 @@ private Stack createStackWithLambda() { .create() .actions(singletonList("appconfig:*")) .resources(singletonList("*")) - .build() - ); + .build()); CfnDeployment previousDeployment = null; for (Map.Entry entry : appConfig.getConfigurationValues().entrySet()) { CfnConfigurationProfile configProfile = CfnConfigurationProfile.Builder - .create(stack, "AppConfigProfileFor" + entry.getKey()) + .create(e2eStack, "AppConfigProfileFor" + entry.getKey()) .applicationId(app.getRef()) .locationUri("hosted") .name(entry.getKey()) .build(); CfnHostedConfigurationVersion configVersion = CfnHostedConfigurationVersion.Builder - .create(stack, "AppConfigHostedVersionFor" + entry.getKey()) + .create(e2eStack, "AppConfigHostedVersionFor" + entry.getKey()) .applicationId(app.getRef()) .contentType("text/plain") .configurationProfileId(configProfile.getRef()) @@ -398,7 +408,7 @@ private Stack createStackWithLambda() { .build(); CfnDeployment deployment = CfnDeployment.Builder - .create(stack, "AppConfigDepoymentFor" + entry.getKey()) + .create(e2eStack, "AppConfigDepoymentFor" + entry.getKey()) .applicationId(app.getRef()) .environmentId(environment.getRef()) .deploymentStrategyId(fastDeployment.getRef()) @@ -415,7 +425,7 @@ private Stack createStackWithLambda() { } if (createTableForAsyncTests) { Table table = Table.Builder - .create(stack, "TableForAsyncTests") + .create(e2eStack, "TableForAsyncTests") .billingMode(BillingMode.PAY_PER_REQUEST) .removalPolicy(RemovalPolicy.DESTROY) .partitionKey(Attribute.builder().name("functionName").type(AttributeType.STRING).build()) @@ -424,10 +434,10 @@ private Stack createStackWithLambda() { table.grantReadWriteData(function); function.addEnvironment("TABLE_FOR_ASYNC_TESTS", table.getTableName()); - CfnOutput.Builder.create(stack, "TableNameForAsyncTests").value(table.getTableName()).build(); + CfnOutput.Builder.create(e2eStack, "TableNameForAsyncTests").value(table.getTableName()).build(); } - return stack; + return e2eStack; } /** @@ -444,13 +454,13 @@ private void synthesize() { */ private void uploadAssets() { Map assets = findAssets(); - assets.forEach((objectKey, asset) -> - { + assets.forEach((objectKey, asset) -> { if (!asset.assetPath.endsWith(".jar")) { return; } - ListObjectsV2Response objects = - s3.listObjectsV2(ListObjectsV2Request.builder().bucket(asset.bucketName).build()); + + ListObjectsV2Response objects = s3 + .listObjectsV2(ListObjectsV2Request.builder().bucket(asset.bucketName).build()); if (objects.contents().stream().anyMatch(o -> o.key().equals(objectKey))) { LOG.debug("Asset already exists, skipping"); return; @@ -472,14 +482,13 @@ private Map findAssets() { JsonNode jsonNode = JsonConfig.get().getObjectMapper() .readTree(new File(cfnAssetDirectory, stackName + ".assets.json")); JsonNode files = jsonNode.get("files"); - files.iterator().forEachRemaining(file -> - { + files.iterator().forEachRemaining(file -> { String assetPath = file.get("source").get("path").asText(); String assetPackaging = file.get("source").get("packaging").asText(); - String bucketName = - file.get("destinations").get("current_account-current_region").get("bucketName").asText(); - String objectKey = - file.get("destinations").get("current_account-current_region").get("objectKey").asText(); + String bucketName = file.get("destinations").get("current_account-current_region").get("bucketName") + .asText(); + String objectKey = file.get("destinations").get("current_account-current_region").get("objectKey") + .asText(); Asset asset = new Asset(assetPath, assetPackaging, bucketName.replace("${AWS::AccountId}", account) .replace("${AWS::Region}", region.toString())); assets.put(objectKey, asset); @@ -509,17 +518,13 @@ private Builder() { runtime = mapRuntimeVersion("JAVA_VERSION"); } - - private JavaRuntime mapRuntimeVersion(String environmentVariableName) { String javaVersion = System.getenv(environmentVariableName); // must be set in GitHub actions JavaRuntime ret = null; if (javaVersion == null) { throw new IllegalArgumentException(environmentVariableName + " is not set"); } - if (javaVersion.startsWith("8")) { - ret = JavaRuntime.JAVA8AL2; - } else if (javaVersion.startsWith("11")) { + if (javaVersion.startsWith("11")) { ret = JavaRuntime.JAVA11; } else if (javaVersion.startsWith("17")) { ret = JavaRuntime.JAVA17; diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/JavaRuntime.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/JavaRuntime.java index c75682949..53d35e86d 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/JavaRuntime.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/JavaRuntime.java @@ -17,8 +17,6 @@ import software.amazon.awscdk.services.lambda.Runtime; public enum JavaRuntime { - JAVA8("java8", Runtime.JAVA_8, "1.8"), - JAVA8AL2("java8.al2", Runtime.JAVA_8_CORRETTO, "1.8"), JAVA11("java11", Runtime.JAVA_11, "11"), JAVA17("java17", Runtime.JAVA_17, "17"), JAVA21("java21", Runtime.JAVA_21, "21"); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java index 00728f451..186e72d13 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java @@ -54,7 +54,8 @@ public class MetricsFetcher { .build(); /** - * Retrieve the metric values from start to end. Different parameters are required (see {@link CloudWatchClient#getMetricData} for more info). + * Retrieve the metric values from start to end. Different parameters are required (see + * {@link CloudWatchClient#getMetricData} for more info). * Use a retry mechanism as metrics may not be available instantaneously after a function runs. * * @param start @@ -66,14 +67,13 @@ public class MetricsFetcher { * @return */ public List fetchMetrics(Instant start, Instant end, int period, String namespace, String metricName, - Map dimensions) { + Map dimensions) { List dimensionsList = new ArrayList<>(); if (dimensions != null) { dimensions.forEach((key, value) -> dimensionsList.add(Dimension.builder().name(key).value(value).build())); } - Callable> callable = () -> - { + Callable> callable = () -> { LOG.debug("Get Metrics for namespace {}, start {}, end {}, metric {}, dimensions {}", namespace, start, end, metricName, dimensionsList); GetMetricDataResponse metricData = cloudwatch.getMetricData(GetMetricDataRequest.builder() @@ -109,9 +109,8 @@ public List fetchMetrics(Instant start, Instant end, int period, String .build(); CallExecutor> callExecutor = new CallExecutorBuilder>() .config(retryConfig) - .afterFailedTryListener(s -> - { - LOG.warn(s.getLastExceptionThatCausedRetry().getMessage() + ", attempts: " + s.getTotalTries()); + .afterFailedTryListener(s -> { + LOG.warn("{}, attempts: {}", s.getLastExceptionThatCausedRetry().getMessage(), s.getTotalTries()); }) .build(); Status> status = callExecutor.execute(callable); diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json new file mode 100644 index 000000000..ebad834d8 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "POST", + "path": "/path/to/resource", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "{\"message\": \"Lambda rocks\"}", + "isBase64Encoded": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json new file mode 100644 index 000000000..1b1961063 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "POST", + "path": "/path/to/resource", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "{\"price\": 50000}", + "isBase64Encoded": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json new file mode 100644 index 000000000..014ef9f05 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"message\": \"Lambda rocks\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json new file mode 100644 index 000000000..b7ef1780c --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"price\": 50000}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json b/powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json new file mode 100644 index 000000000..35560f109 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "POST", + "path": "/path/to/resource", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "{\"price\": 150}", + "isBase64Encoded": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json b/powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json new file mode 100644 index 000000000..8cb8ea27a --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"price\": 150}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} \ No newline at end of file diff --git a/powertools-idempotency/pom.xml b/powertools-idempotency/pom.xml index 94aceebfc..286ea3f97 100644 --- a/powertools-idempotency/pom.xml +++ b/powertools-idempotency/pom.xml @@ -21,76 +21,31 @@ software.amazon.lambda powertools-parent - 1.19.0-SNAPSHOT + 2.0.0-RC1 powertools-idempotency - jar + pom Powertools for AWS Lambda (Java) library Idempotency - + Utility to implement idempotency of Lambda functions - - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - + + powertools-idempotency-core + powertools-idempotency-dynamodb + - software.amazon.lambda - powertools-core + org.aspectj + aspectjrt + provided software.amazon.lambda - powertools-serialization - - - com.amazonaws - aws-lambda-java-core - - - software.amazon.awssdk - dynamodb - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - + powertools-common - - software.amazon.awssdk - url-connection-client - ${aws.sdk.version} - - - org.aspectj - aspectjrt - - org.junit.jupiter @@ -127,20 +82,6 @@ aws-lambda-java-tests test - - com.amazonaws - DynamoDBLocal - [1.12,2.0) - test - - - - io.github.ganadist.sqlite4java - libsqlite4java-osx-aarch64 - 1.0.392 - test - dylib - @@ -171,22 +112,6 @@ - - org.apache.maven.plugins - maven-jar-plugin - 3.3.0 - - - - software.amazon.awssdk.enhanced.dynamodb - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - \ No newline at end of file + diff --git a/powertools-idempotency/powertools-idempotency-core/pom.xml b/powertools-idempotency/powertools-idempotency-core/pom.xml new file mode 100644 index 000000000..3012406af --- /dev/null +++ b/powertools-idempotency/powertools-idempotency-core/pom.xml @@ -0,0 +1,52 @@ + + + + + 4.0.0 + + + software.amazon.lambda + powertools-idempotency + 2.0.0-RC1 + + + powertools-idempotency-core + jar + + Powertools for AWS Lambda (Java) library Idempotency - Core + + Idempotency module common implementation + + + + + software.amazon.lambda + powertools-serialization + + + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + + + \ No newline at end of file diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Constants.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Constants.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Constants.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Constants.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java similarity index 98% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java index 6da826c45..bd564caf8 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java +++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java @@ -82,7 +82,7 @@ private void setPersistenceStore(BasePersistenceStore persistenceStore) { } private static class Holder { - private final static Idempotency instance = new Idempotency(); + private static final Idempotency instance = new Idempotency(); } public static class Config { diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java similarity index 72% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java index 58d0a7f5b..9d5c66cac 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java +++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java @@ -14,9 +14,13 @@ package software.amazon.lambda.powertools.idempotency; -import com.amazonaws.services.lambda.runtime.Context; import java.time.Duration; +import java.util.function.BiFunction; + +import com.amazonaws.services.lambda.runtime.Context; + import software.amazon.lambda.powertools.idempotency.internal.cache.LRUCache; +import software.amazon.lambda.powertools.idempotency.persistence.DataRecord; /** * Configuration of the idempotency feature. Use the {@link Builder} to create an instance. @@ -29,11 +33,12 @@ public class IdempotencyConfig { private final String payloadValidationJMESPath; private final boolean throwOnNoIdempotencyKey; private final String hashFunction; + private final BiFunction responseHook; private Context lambdaContext; private IdempotencyConfig(String eventKeyJMESPath, String payloadValidationJMESPath, - boolean throwOnNoIdempotencyKey, boolean useLocalCache, int localCacheMaxItems, - long expirationInSeconds, String hashFunction) { + boolean throwOnNoIdempotencyKey, boolean useLocalCache, int localCacheMaxItems, + long expirationInSeconds, String hashFunction, BiFunction responseHook) { this.localCacheMaxItems = localCacheMaxItems; this.useLocalCache = useLocalCache; this.expirationInSeconds = expirationInSeconds; @@ -41,6 +46,7 @@ private IdempotencyConfig(String eventKeyJMESPath, String payloadValidationJMESP this.payloadValidationJMESPath = payloadValidationJMESPath; this.throwOnNoIdempotencyKey = throwOnNoIdempotencyKey; this.hashFunction = hashFunction; + this.responseHook = responseHook; } /** @@ -88,23 +94,31 @@ public void setLambdaContext(Context lambdaContext) { this.lambdaContext = lambdaContext; } + public BiFunction getResponseHook() { + return responseHook; + } + public static class Builder { private int localCacheMaxItems = 256; private boolean useLocalCache = false; - private long expirationInSeconds = 60 * 60; // 1 hour + private long expirationInSeconds = 60 * 60L; // 1 hour private String eventKeyJMESPath; private String payloadValidationJMESPath; private boolean throwOnNoIdempotencyKey = false; private String hashFunction = "MD5"; + private BiFunction responseHook; /** * Initialize and return an instance of {@link IdempotencyConfig}.
* Example:
+ * *

          * IdempotencyConfig.builder().withUseLocalCache().build();
          * 
+ * * This instance must then be passed to the {@link Idempotency.Config}: + * *
          * Idempotency.config().withConfig(config).configure();
          * 
@@ -119,13 +133,15 @@ public IdempotencyConfig build() { useLocalCache, localCacheMaxItems, expirationInSeconds, - hashFunction); + hashFunction, + responseHook); } /** * A JMESPath expression to extract the idempotency key from the event record.
* See https://jmespath.org/ for more details.
- * Common paths are:
    + * Common paths are: + *
      *
    • powertools_json(body) for APIGatewayProxyRequestEvent and APIGatewayV2HTTPEvent
    • *
    • Records[*].powertools_json(body) for SQSEvent
    • *
    • Records[0].Sns.Message | powertools_json(@) for SNSEvent
    • @@ -135,7 +151,8 @@ public IdempotencyConfig build() { *
    • ...
    • *
    * - * @param eventKeyJMESPath path of the key in the Lambda event + * @param eventKeyJMESPath + * path of the key in the Lambda event * @return the instance of the builder (to chain operations) */ public Builder withEventKeyJMESPath(String eventKeyJMESPath) { @@ -146,7 +163,8 @@ public Builder withEventKeyJMESPath(String eventKeyJMESPath) { /** * Set the maximum number of items to store in local cache, by default 256 * - * @param localCacheMaxItems maximum number of items to store in local cache + * @param localCacheMaxItems + * maximum number of items to store in local cache * @return the instance of the builder (to chain operations) */ public Builder withLocalCacheMaxItems(int localCacheMaxItems) { @@ -157,8 +175,9 @@ public Builder withLocalCacheMaxItems(int localCacheMaxItems) { /** * Whether to locally cache idempotency results, by default false * - * @param useLocalCache boolean that indicate if a local cache must be used in addition to the persistence store. - * If set to true, will use the {@link LRUCache} + * @param useLocalCache + * boolean that indicate if a local cache must be used in addition to the persistence store. + * If set to true, will use the {@link LRUCache} * @return the instance of the builder (to chain operations) */ public Builder withUseLocalCache(boolean useLocalCache) { @@ -169,7 +188,8 @@ public Builder withUseLocalCache(boolean useLocalCache) { /** * The number of seconds to wait before a record is expired * - * @param expiration expiration of the record in the store + * @param expiration + * expiration of the record in the store * @return the instance of the builder (to chain operations) */ public Builder withExpiration(Duration expiration) { @@ -181,7 +201,8 @@ public Builder withExpiration(Duration expiration) { * A JMESPath expression to extract the payload to be validated from the event record.
    * See https://jmespath.org/ for more details. * - * @param payloadValidationJMESPath JMES Path of a part of the payload to be used for validation + * @param payloadValidationJMESPath + * JMES Path of a part of the payload to be used for validation * @return the instance of the builder (to chain operations) */ public Builder withPayloadValidationJMESPath(String payloadValidationJMESPath) { @@ -192,8 +213,9 @@ public Builder withPayloadValidationJMESPath(String payloadValidationJMESPath) { /** * Whether to throw an exception if no idempotency key was found in the request, by default false * - * @param throwOnNoIdempotencyKey boolean to indicate if we must throw an Exception when - * idempotency key could not be found in the payload. + * @param throwOnNoIdempotencyKey + * boolean to indicate if we must throw an Exception when idempotency key could not be found in the + * payload. * @return the instance of the builder (to chain operations) */ public Builder withThrowOnNoIdempotencyKey(boolean throwOnNoIdempotencyKey) { @@ -214,15 +236,43 @@ public Builder withThrowOnNoIdempotencyKey() { /** * Function to use for calculating hashes, by default MD5. * - * @param hashFunction Can be any algorithm supported by {@link java.security.MessageDigest}, most commons are
      - *
    • MD5
    • - *
    • SHA-1
    • - *
    • SHA-256
    + * @param hashFunction + * Can be any algorithm supported by {@link java.security.MessageDigest}, most commons are + *
      + *
    • MD5
    • + *
    • SHA-1
    • + *
    • SHA-256
    • + *
    * @return the instance of the builder (to chain operations) */ public Builder withHashFunction(String hashFunction) { this.hashFunction = hashFunction; return this; } + + /** + * Response hook that will be called for each idempotent response. This hook will receive the de-serialized + * response data from the persistence store as first argument and the original DataRecord from the persistence + * store as second argument. + * + * Usage: + * + *
    +         * IdempotencyConfig.builder().withResponseHook((responseData, dataRecord) -> {
    +         *     // do something with the response data, for example:
    +         *     if(responseData instanceof APIGatewayProxyRequestEvent) {
    +         *         ((APIGatewayProxyRequestEvent) responseData).setHeaders(Map.of("x-idempotency-response", "true")
    +         *     }
    +         *     return responseData;
    +         * })
    +         * 
    + * + * @param responseHook + * @return + */ + public Builder withResponseHook(BiFunction responseHook) { + this.responseHook = responseHook; + return this; + } } } diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyKey.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyKey.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyKey.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyKey.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java similarity index 99% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java index 6ca40a0e1..d08874492 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java +++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java @@ -15,6 +15,7 @@ package software.amazon.lambda.powertools.idempotency; import com.amazonaws.services.lambda.runtime.Context; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyAlreadyInProgressException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyAlreadyInProgressException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyAlreadyInProgressException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyAlreadyInProgressException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyConfigurationException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyConfigurationException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyConfigurationException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyConfigurationException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java similarity index 68% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java index ba7da69bf..aed2e5ae0 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java +++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java @@ -14,11 +14,17 @@ package software.amazon.lambda.powertools.idempotency.exceptions; +import java.util.Optional; + +import software.amazon.lambda.powertools.idempotency.persistence.DataRecord; + /** * Exception thrown when trying to store an item which already exists. */ public class IdempotencyItemAlreadyExistsException extends RuntimeException { private static final long serialVersionUID = 9027152772149436500L; + // transient because we don't want to accidentally dump any payloads in logs / stack traces + private transient Optional dr = Optional.empty(); public IdempotencyItemAlreadyExistsException() { super(); @@ -27,4 +33,13 @@ public IdempotencyItemAlreadyExistsException() { public IdempotencyItemAlreadyExistsException(String msg, Throwable e) { super(msg, e); } + + public IdempotencyItemAlreadyExistsException(String msg, Throwable e, DataRecord dr) { + super(msg, e); + this.dr = Optional.ofNullable(dr); + } + + public Optional getDataRecord() { + return dr; + } } diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemNotFoundException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemNotFoundException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemNotFoundException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemNotFoundException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyKeyException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyKeyException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyKeyException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyKeyException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyPersistenceLayerException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyPersistenceLayerException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyPersistenceLayerException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyPersistenceLayerException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyValidationException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyValidationException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyValidationException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyValidationException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java similarity index 86% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java index 2875ab3d1..0466f244f 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java +++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java @@ -17,14 +17,18 @@ import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.EXPIRED; import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.INPROGRESS; -import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.databind.JsonNode; import java.time.Instant; import java.util.OptionalInt; +import java.util.function.BiFunction; + import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import com.amazonaws.services.lambda.runtime.Context; +import com.fasterxml.jackson.databind.JsonNode; + import software.amazon.lambda.powertools.idempotency.Idempotency; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyAlreadyInProgressException; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyInconsistentStateException; @@ -38,7 +42,8 @@ import software.amazon.lambda.powertools.utilities.JsonConfig; /** - * Internal class that will handle the Idempotency, and use the {@link software.amazon.lambda.powertools.idempotency.persistence.PersistenceStore} + * Internal class that will handle the Idempotency, and use the + * {@link software.amazon.lambda.powertools.idempotency.persistence.PersistenceStore} * to store the result of previous calls. */ public class IdempotencyHandler { @@ -89,9 +94,11 @@ private Object processIdempotency() throws Throwable { // already exists. If it succeeds, there's no need to call getRecord. persistenceStore.saveInProgress(data, Instant.now(), getRemainingTimeInMillis()); } catch (IdempotencyItemAlreadyExistsException iaee) { - DataRecord record = getIdempotencyRecord(); - if (record != null) { - return handleForStatus(record); + // If a DataRecord is already present on the Exception we can immediately take that one instead of trying + // to fetch it first. + DataRecord dr = iaee.getDataRecord().orElseGet(this::getIdempotencyRecord); + if (dr != null) { + return handleForStatus(dr); } } catch (IdempotencyKeyException ike) { throw ike; @@ -105,7 +112,8 @@ private Object processIdempotency() throws Throwable { /** * Tries to determine the remaining time available for the current lambda invocation. - * Currently, it only works if the idempotent handler decorator is used or using {@link Idempotency#registerLambdaContext(Context)} + * Currently, it only works if the idempotent handler decorator is used or using + * {@link Idempotency#registerLambdaContext(Context)} * * @return the remaining time in milliseconds or empty if the context was not provided/found */ @@ -143,7 +151,8 @@ private DataRecord getIdempotencyRecord() { /** * Take appropriate action based on data_record's status * - * @param record DataRecord + * @param record + * DataRecord * @return Function's response previously used for this idempotency key, if it has successfully executed already. */ private Object handleForStatus(DataRecord record) { @@ -166,10 +175,25 @@ private Object handleForStatus(DataRecord record) { try { LOG.debug("Response for key '{}' retrieved from idempotency store, skipping the function", record.getIdempotencyKey()); + + final BiFunction responseHook = Idempotency.getInstance().getConfig() + .getResponseHook(); + final Object responseData; + if (returnType.equals(String.class)) { - return record.getResponseData(); + // Primitive String data will be returned raw and not de-serialized from JSON. + responseData = record.getResponseData(); + } else { + responseData = JsonConfig.get().getObjectMapper().reader().readValue(record.getResponseData(), + returnType); } - return JsonConfig.get().getObjectMapper().reader().readValue(record.getResponseData(), returnType); + + if (responseHook != null) { + LOG.debug("Applying user-defined response hook to idempotency data before returning."); + return responseHook.apply(responseData, record); + } + + return responseData; } catch (Exception e) { throw new IdempotencyPersistenceLayerException( "Unable to get function response as " + returnType.getSimpleName(), e); diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java similarity index 97% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java index 989e88eb7..ea6d743f0 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java +++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java @@ -14,12 +14,8 @@ package software.amazon.lambda.powertools.idempotency.internal; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler; - import com.amazonaws.services.lambda.runtime.Context; import com.fasterxml.jackson.databind.JsonNode; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -33,6 +29,11 @@ import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyConfigurationException; import software.amazon.lambda.powertools.utilities.JsonConfig; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.placedOnRequestHandler; + /** * Aspect that handles the {@link Idempotent} annotation. * It uses the {@link IdempotencyHandler} to actually do the job. diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCache.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCache.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCache.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCache.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java similarity index 95% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java index a3b1b0ff4..8b08434ba 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java +++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java @@ -14,7 +14,7 @@ package software.amazon.lambda.powertools.idempotency.persistence; -import static software.amazon.lambda.powertools.core.internal.LambdaConstants.LAMBDA_FUNCTION_NAME_ENV; +import static software.amazon.lambda.powertools.common.internal.LambdaConstants.LAMBDA_FUNCTION_NAME_ENV; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -35,7 +35,6 @@ import java.util.stream.StreamSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import software.amazon.awssdk.utils.StringUtils; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException; @@ -55,7 +54,7 @@ public abstract class BasePersistenceStore implements PersistenceStore { protected boolean payloadValidationEnabled = false; private String functionName = ""; private boolean configured = false; - private long expirationInSeconds = 60 * 60; // 1 hour default + private long expirationInSeconds = 60 * 60L; // 1 hour default private boolean useLocalCache = false; private LRUCache cache; private String eventKeyJMESPath; @@ -73,7 +72,7 @@ public abstract class BasePersistenceStore implements PersistenceStore { public void configure(IdempotencyConfig config, String functionName) { String funcEnv = System.getenv(LAMBDA_FUNCTION_NAME_ENV); this.functionName = funcEnv != null ? funcEnv : "testFunction"; - if (!StringUtils.isEmpty(functionName)) { + if (functionName != null && !functionName.isEmpty()) { this.functionName += "." + functionName; } @@ -315,6 +314,7 @@ String generateHash(JsonNode data) { return String.format("%032x", new BigInteger(1, digest)); } + @SuppressWarnings("java:S4790") // Usage of MessageDigest is OK private MessageDigest getHashAlgorithm() { MessageDigest hashAlgorithm; try { @@ -339,7 +339,7 @@ private MessageDigest getHashAlgorithm() { private void validatePayload(JsonNode data, DataRecord dataRecord) throws IdempotencyValidationException { if (payloadValidationEnabled) { String dataHash = getHashedPayload(data); - if (!StringUtils.equals(dataHash, dataRecord.getPayloadHash())) { + if (!isEqual(dataRecord.getPayloadHash(), dataHash)) { throw new IdempotencyValidationException("Payload does not match stored record for this event key"); } } @@ -402,4 +402,12 @@ void configure(IdempotencyConfig config, String functionName, LRUCache - *
  • INPROGRESS: record initialized when function starts
  • - *
  • COMPLETED: record updated with the result of the function when it ends
  • - *
  • EXPIRED: record expired, idempotency will not happen
  • + *
  • INPROGRESS: record initialized when function starts
  • + *
  • COMPLETED: record updated with the result of the function when it ends
  • + *
  • EXPIRED: record expired, idempotency will not happen
  • *
*/ public enum Status { diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java similarity index 99% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java index c058b592e..b4f105720 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java +++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java @@ -14,10 +14,11 @@ package software.amazon.lambda.powertools.idempotency.persistence; -import java.time.Instant; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException; +import java.time.Instant; + /** * Persistence layer that will store the idempotency result. * In order to provide another implementation, extends {@link BasePersistenceStore}. diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyEnabledFunction.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyEnabledFunction.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyEnabledFunction.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyEnabledFunction.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunction.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunction.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunction.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunction.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInternalKey.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInternalKey.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInternalKey.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInternalKey.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInvalid.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInvalid.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInvalid.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInvalid.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionVoid.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionVoid.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionVoid.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionVoid.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyStringFunction.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyStringFunction.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyStringFunction.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyStringFunction.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyWithErrorFunction.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyWithErrorFunction.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyWithErrorFunction.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyWithErrorFunction.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyAspectTest.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyAspectTest.java similarity index 70% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyAspectTest.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyAspectTest.java index c72593b66..12113fc9e 100644 --- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyAspectTest.java +++ b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyAspectTest.java @@ -20,23 +20,27 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import java.time.Instant; import java.util.OptionalInt; import java.util.OptionalLong; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.SetEnvironmentVariable; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; + +import com.amazonaws.services.lambda.runtime.Context; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; + import software.amazon.lambda.powertools.idempotency.Constants; import software.amazon.lambda.powertools.idempotency.Idempotency; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; @@ -76,8 +80,8 @@ public void firstCall_shouldPutInStore() { .withPersistenceStore(store) .withConfig(IdempotencyConfig.builder() .withEventKeyJMESPath("id") - .build() - ).configure(); + .build()) + .configure(); IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); @@ -102,6 +106,45 @@ public void firstCall_shouldPutInStore() { assertThat(resultCaptor.getValue()).isEqualTo(basket); } + @Test + public void firstCall_shouldPutInStoreAndNotApplyResponseHook() { + Idempotency.config() + .withPersistenceStore(store) + .withConfig(IdempotencyConfig.builder() + .withEventKeyJMESPath("id") + // This hook will add a second product to the basket. It should not run here. Only for + // idempotent responses. + .withResponseHook((responseData, dataRecord) -> { + final Basket basket = (Basket) responseData; + basket.add(new Product(42, "fake product 2", 12)); + return basket; + }) + .build()) + .configure(); + + IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); + + when(context.getRemainingTimeInMillis()).thenReturn(30000); + + Product p = new Product(42, "fake product", 12); + Basket basket = function.handleRequest(p, context); + assertThat(basket.getProducts()).hasSize(1); // Size should be 1 because response hook should not run + assertThat(function.handlerCalled()).isTrue(); + + ArgumentCaptor nodeCaptor = ArgumentCaptor.forClass(JsonNode.class); + ArgumentCaptor expiryCaptor = ArgumentCaptor.forClass(OptionalInt.class); + verify(store).saveInProgress(nodeCaptor.capture(), any(), expiryCaptor.capture()); + assertThat(nodeCaptor.getValue().get("id").asLong()).isEqualTo(p.getId()); + assertThat(nodeCaptor.getValue().get("name").asText()).isEqualTo(p.getName()); + assertThat(nodeCaptor.getValue().get("price").asDouble()).isEqualTo(p.getPrice()); + + assertThat(expiryCaptor.getValue().orElse(-1)).isEqualTo(30000); + + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(Basket.class); + verify(store).saveSuccess(any(), resultCaptor.capture(), any()); + assertThat(resultCaptor.getValue()).isEqualTo(basket); + } + @Test public void secondCall_notExpired_shouldGetFromStore() throws JsonProcessingException { // GIVEN @@ -109,20 +152,56 @@ public void secondCall_notExpired_shouldGetFromStore() throws JsonProcessingExce .withPersistenceStore(store) .withConfig(IdempotencyConfig.builder() .withEventKeyJMESPath("id") - .build() - ).configure(); + .build()) + .configure(); + + doThrow(new IdempotencyItemAlreadyExistsException()).when(store).saveInProgress(any(), any(), any()); + + Product p = new Product(42, "fake product", 12); + Basket b = new Basket(p); + DataRecord dr = new DataRecord( + "42", + DataRecord.Status.COMPLETED, + Instant.now().plus(356, SECONDS).getEpochSecond(), + JsonConfig.get().getObjectMapper().writer().writeValueAsString(b), + null); + doReturn(dr).when(store).getRecord(any(), any()); + + // WHEN + IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); + Basket basket = function.handleRequest(p, context); + + // THEN + assertThat(basket).isEqualTo(b); + assertThat(function.handlerCalled()).isFalse(); + } - doThrow(IdempotencyItemAlreadyExistsException.class).when(store).saveInProgress(any(), any(), any()); + @Test + public void secondCall_notExpired_shouldNotGetFromStoreIfPresentOnIdempotencyException() + throws JsonProcessingException { + // GIVEN + Idempotency.config() + .withPersistenceStore(store) + .withConfig(IdempotencyConfig.builder() + .withEventKeyJMESPath("id") + .build()) + .configure(); Product p = new Product(42, "fake product", 12); Basket b = new Basket(p); - DataRecord record = new DataRecord( + DataRecord dr = new DataRecord( "42", DataRecord.Status.COMPLETED, Instant.now().plus(356, SECONDS).getEpochSecond(), JsonConfig.get().getObjectMapper().writer().writeValueAsString(b), null); - doReturn(record).when(store).getRecord(any(), any()); + + // A data record on this exception should take precedence over fetching a record from the store / cache + doThrow(new IdempotencyItemAlreadyExistsException( + "Test message", + new RuntimeException("Test Cause"), + dr)) + .when(store).saveInProgress(any(), any(), any()); // WHEN IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); @@ -131,6 +210,8 @@ public void secondCall_notExpired_shouldGetFromStore() throws JsonProcessingExce // THEN assertThat(basket).isEqualTo(b); assertThat(function.handlerCalled()).isFalse(); + // Should never call the store because item is already present on IdempotencyItemAlreadyExistsException + verify(store, never()).getRecord(any(), any()); } @Test @@ -140,19 +221,19 @@ public void secondCall_notExpired_shouldGetStringFromStore() { .withPersistenceStore(store) .withConfig(IdempotencyConfig.builder() .withEventKeyJMESPath("id") - .build() - ).configure(); + .build()) + .configure(); - doThrow(IdempotencyItemAlreadyExistsException.class).when(store).saveInProgress(any(), any(), any()); + doThrow(new IdempotencyItemAlreadyExistsException()).when(store).saveInProgress(any(), any(), any()); Product p = new Product(42, "fake product", 12); - DataRecord record = new DataRecord( + DataRecord dr = new DataRecord( "42", DataRecord.Status.COMPLETED, Instant.now().plus(356, SECONDS).getEpochSecond(), p.getName(), null); - doReturn(record).when(store).getRecord(any(), any()); + doReturn(dr).when(store).getRecord(any(), any()); // WHEN IdempotencyStringFunction function = new IdempotencyStringFunction(); @@ -163,6 +244,41 @@ public void secondCall_notExpired_shouldGetStringFromStore() { assertThat(function.handlerCalled()).isFalse(); } + @Test + public void secondCall_notExpired_shouldGetStringFromStoreWithResponseHook() { + // GIVEN + final String RESPONSE_HOOK_SUFFIX = " ResponseHook"; + Idempotency.config() + .withPersistenceStore(store) + .withConfig(IdempotencyConfig.builder() + .withEventKeyJMESPath("id") + .withResponseHook((responseData, dataRecord) -> { + responseData += RESPONSE_HOOK_SUFFIX; + return responseData; + }) + .build()) + .configure(); + + doThrow(new IdempotencyItemAlreadyExistsException()).when(store).saveInProgress(any(), any(), any()); + + Product p = new Product(42, "fake product", 12); + DataRecord dr = new DataRecord( + "42", + DataRecord.Status.COMPLETED, + Instant.now().plus(356, SECONDS).getEpochSecond(), + p.getName(), + null); + doReturn(dr).when(store).getRecord(any(), any()); + + // WHEN + IdempotencyStringFunction function = new IdempotencyStringFunction(); + String name = function.handleRequest(p, context); + + // THEN + assertThat(name).isEqualTo(p.getName() + RESPONSE_HOOK_SUFFIX); + assertThat(function.handlerCalled()).isFalse(); + } + @Test public void secondCall_inProgress_shouldThrowIdempotencyAlreadyInProgressException() throws JsonProcessingException { @@ -171,23 +287,23 @@ public void secondCall_inProgress_shouldThrowIdempotencyAlreadyInProgressExcepti .withPersistenceStore(store) .withConfig(IdempotencyConfig.builder() .withEventKeyJMESPath("id") - .build() - ).configure(); + .build()) + .configure(); - doThrow(IdempotencyItemAlreadyExistsException.class).when(store).saveInProgress(any(), any(), any()); + doThrow(new IdempotencyItemAlreadyExistsException()).when(store).saveInProgress(any(), any(), any()); Product p = new Product(42, "fake product", 12); Basket b = new Basket(p); - OptionalLong timestampInFuture = - OptionalLong.of(Instant.now().toEpochMilli() + 1000); // timeout not expired (in 1sec) - DataRecord record = new DataRecord( + OptionalLong timestampInFuture = OptionalLong.of(Instant.now().toEpochMilli() + 1000); // timeout not expired + // (in 1sec) + DataRecord dr = new DataRecord( "42", DataRecord.Status.INPROGRESS, Instant.now().plus(356, SECONDS).getEpochSecond(), JsonConfig.get().getObjectMapper().writer().writeValueAsString(b), null, timestampInFuture); - doReturn(record).when(store).getRecord(any(), any()); + doReturn(dr).when(store).getRecord(any(), any()); // THEN IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); @@ -203,23 +319,23 @@ public void secondCall_inProgress_lambdaTimeout_timeoutExpired_shouldThrowIncons .withPersistenceStore(store) .withConfig(IdempotencyConfig.builder() .withEventKeyJMESPath("id") - .build() - ).configure(); + .build()) + .configure(); - doThrow(IdempotencyItemAlreadyExistsException.class).when(store).saveInProgress(any(), any(), any()); + doThrow(new IdempotencyItemAlreadyExistsException()).when(store).saveInProgress(any(), any(), any()); Product p = new Product(42, "fake product", 12); Basket b = new Basket(p); - OptionalLong timestampInThePast = - OptionalLong.of(Instant.now().toEpochMilli() - 100); // timeout expired 100ms ago - DataRecord record = new DataRecord( + OptionalLong timestampInThePast = OptionalLong.of(Instant.now().toEpochMilli() - 100); // timeout expired 100ms + // ago + DataRecord dr = new DataRecord( "42", DataRecord.Status.INPROGRESS, Instant.now().plus(356, SECONDS).getEpochSecond(), JsonConfig.get().getObjectMapper().writer().writeValueAsString(b), null, timestampInThePast); - doReturn(record).when(store).getRecord(any(), any()); + doReturn(dr).when(store).getRecord(any(), any()); // THEN IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); @@ -234,8 +350,8 @@ public void functionThrowException_shouldDeleteRecord_andThrowFunctionException( .withPersistenceStore(store) .withConfig(IdempotencyConfig.builder() .withEventKeyJMESPath("id") - .build() - ).configure(); + .build()) + .configure(); // WHEN / THEN IdempotencyWithErrorFunction function = new IdempotencyWithErrorFunction(); @@ -255,8 +371,8 @@ public void testIdempotencyDisabled_shouldJustRunTheFunction() { .withPersistenceStore(store) .withConfig(IdempotencyConfig.builder() .withEventKeyJMESPath("id") - .build() - ).configure(); + .build()) + .configure(); // WHEN IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); @@ -335,17 +451,17 @@ public void idempotencyOnSubMethodAnnotated_secondCall_notExpired_shouldGetFromS .withPersistenceStore(store) .configure(); - doThrow(IdempotencyItemAlreadyExistsException.class).when(store).saveInProgress(any(), any(), any()); + doThrow(new IdempotencyItemAlreadyExistsException()).when(store).saveInProgress(any(), any(), any()); Product p = new Product(42, "fake product", 12); Basket b = new Basket(p); - DataRecord record = new DataRecord( + DataRecord dr = new DataRecord( "fake", DataRecord.Status.COMPLETED, Instant.now().plus(356, SECONDS).getEpochSecond(), JsonConfig.get().getObjectMapper().writer().writeValueAsString(b), null); - doReturn(record).when(store).getRecord(any(), any()); + doReturn(dr).when(store).getRecord(any(), any()); // WHEN IdempotencyInternalFunction function = new IdempotencyInternalFunction(false); @@ -404,8 +520,7 @@ public void idempotencyOnSubMethodAnnotated_keyJMESPathArray_shouldPutInStoreWit public void idempotencyOnSubMethodNotAnnotated_shouldThrowException() { Idempotency.config() .withPersistenceStore(store) - .withConfig(IdempotencyConfig.builder().build() - ).configure(); + .withConfig(IdempotencyConfig.builder().build()).configure(); // WHEN IdempotencyInternalFunctionInvalid function = new IdempotencyInternalFunctionInvalid(); @@ -420,8 +535,7 @@ public void idempotencyOnSubMethodNotAnnotated_shouldThrowException() { public void idempotencyOnSubMethodVoid_shouldThrowException() { Idempotency.config() .withPersistenceStore(store) - .withConfig(IdempotencyConfig.builder().build() - ).configure(); + .withConfig(IdempotencyConfig.builder().build()).configure(); // WHEN IdempotencyInternalFunctionVoid function = new IdempotencyInternalFunctionVoid(); diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java index 8854be1f2..053b56430 100644 --- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java +++ b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java @@ -14,10 +14,10 @@ package software.amazon.lambda.powertools.idempotency.internal.cache; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + public class LRUCacheTest { @Test diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Basket.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/model/Basket.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Basket.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/model/Basket.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Product.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/model/Product.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Product.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/model/Product.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java similarity index 95% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java index 67bc5aa22..f770679ee 100644 --- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java +++ b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java @@ -14,18 +14,11 @@ package software.amazon.lambda.powertools.idempotency.persistence; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.tests.EventLoader; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.DoubleNode; import com.fasterxml.jackson.databind.node.TextNode; -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.OptionalInt; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; @@ -37,6 +30,14 @@ import software.amazon.lambda.powertools.idempotency.model.Product; import software.amazon.lambda.powertools.utilities.JsonConfig; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.OptionalInt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + public class BasePersistenceStoreTest { private DataRecord dr; @@ -90,7 +91,7 @@ public void saveInProgress_defaultConfig() { assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.INPROGRESS); assertThat(dr.getExpiryTimestamp()).isEqualTo(now.plus(3600, ChronoUnit.SECONDS).getEpochSecond()); assertThat(dr.getResponseData()).isNull(); - assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#7b40f56c086de5aa91dc467456329ed2"); + assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#8d6a8f173b46479eff55e0997864a514"); assertThat(dr.getPayloadHash()).isEqualTo(""); assertThat(dr.getInProgressExpiryTimestamp()).isEmpty(); assertThat(status).isEqualTo(1); @@ -108,7 +109,7 @@ public void saveInProgress_withRemainingTime() { assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.INPROGRESS); assertThat(dr.getExpiryTimestamp()).isEqualTo(now.plus(3600, ChronoUnit.SECONDS).getEpochSecond()); assertThat(dr.getResponseData()).isNull(); - assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#7b40f56c086de5aa91dc467456329ed2"); + assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#8d6a8f173b46479eff55e0997864a514"); assertThat(dr.getPayloadHash()).isEqualTo(""); assertThat(dr.getInProgressExpiryTimestamp().orElse(-1)).isEqualTo( now.plus(lambdaTimeoutMs, ChronoUnit.MILLIS).toEpochMilli()); @@ -227,7 +228,7 @@ public void saveSuccess_shouldUpdateRecord() throws JsonProcessingException { assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.COMPLETED); assertThat(dr.getExpiryTimestamp()).isEqualTo(now.plus(3600, ChronoUnit.SECONDS).getEpochSecond()); assertThat(dr.getResponseData()).isEqualTo(JsonConfig.get().getObjectMapper().writeValueAsString(product)); - assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#7b40f56c086de5aa91dc467456329ed2"); + assertThat(dr.getIdempotencyKey()).isEqualTo("testFunction#8d6a8f173b46479eff55e0997864a514"); assertThat(dr.getPayloadHash()).isEqualTo(""); assertThat(status).isEqualTo(2); assertThat(cache).isEmpty(); @@ -246,11 +247,11 @@ public void saveSuccess_withCacheEnabled_shouldSaveInCache() throws JsonProcessi assertThat(status).isEqualTo(2); assertThat(cache).hasSize(1); - DataRecord record = cache.get("testFunction#7b40f56c086de5aa91dc467456329ed2"); + DataRecord record = cache.get("testFunction#8d6a8f173b46479eff55e0997864a514"); assertThat(record.getStatus()).isEqualTo(DataRecord.Status.COMPLETED); assertThat(record.getExpiryTimestamp()).isEqualTo(now.plus(3600, ChronoUnit.SECONDS).getEpochSecond()); assertThat(record.getResponseData()).isEqualTo(JsonConfig.get().getObjectMapper().writeValueAsString(product)); - assertThat(record.getIdempotencyKey()).isEqualTo("testFunction#7b40f56c086de5aa91dc467456329ed2"); + assertThat(record.getIdempotencyKey()).isEqualTo("testFunction#8d6a8f173b46479eff55e0997864a514"); assertThat(record.getPayloadHash()).isEqualTo(""); } @@ -269,7 +270,7 @@ public void getRecord_shouldReturnRecordFromPersistence() Instant now = Instant.now(); DataRecord record = persistenceStore.getRecord(JsonConfig.get().getObjectMapper().valueToTree(event), now); - assertThat(record.getIdempotencyKey()).isEqualTo("testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2"); + assertThat(record.getIdempotencyKey()).isEqualTo("testFunction.myfunc#8d6a8f173b46479eff55e0997864a514"); assertThat(record.getStatus()).isEqualTo(DataRecord.Status.INPROGRESS); assertThat(record.getResponseData()).isEqualTo("Response"); assertThat(status).isEqualTo(0); @@ -285,15 +286,15 @@ public void getRecord_cacheEnabledNotExpired_shouldReturnRecordFromCache() Instant now = Instant.now(); DataRecord dr = new DataRecord( - "testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2", + "testFunction.myfunc#8d6a8f173b46479eff55e0997864a514", DataRecord.Status.COMPLETED, now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(), "result of the function", null); - cache.put("testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2", dr); + cache.put("testFunction.myfunc#8d6a8f173b46479eff55e0997864a514", dr); DataRecord record = persistenceStore.getRecord(JsonConfig.get().getObjectMapper().valueToTree(event), now); - assertThat(record.getIdempotencyKey()).isEqualTo("testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2"); + assertThat(record.getIdempotencyKey()).isEqualTo("testFunction.myfunc#8d6a8f173b46479eff55e0997864a514"); assertThat(record.getStatus()).isEqualTo(DataRecord.Status.COMPLETED); assertThat(record.getResponseData()).isEqualTo("result of the function"); assertThat(status).isEqualTo(-1); // getRecord must not be called (retrieve from cache) @@ -309,15 +310,15 @@ public void getRecord_cacheEnabledExpired_shouldReturnRecordFromPersistence() Instant now = Instant.now(); DataRecord dr = new DataRecord( - "testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2", + "testFunction.myfunc#8d6a8f173b46479eff55e0997864a514", DataRecord.Status.COMPLETED, now.minus(3, ChronoUnit.SECONDS).getEpochSecond(), "result of the function", null); - cache.put("testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2", dr); + cache.put("testFunction.myfunc#8d6a8f173b46479eff55e0997864a514", dr); DataRecord record = persistenceStore.getRecord(JsonConfig.get().getObjectMapper().valueToTree(event), now); - assertThat(record.getIdempotencyKey()).isEqualTo("testFunction.myfunc#7b40f56c086de5aa91dc467456329ed2"); + assertThat(record.getIdempotencyKey()).isEqualTo("testFunction.myfunc#8d6a8f173b46479eff55e0997864a514"); assertThat(record.getStatus()).isEqualTo(DataRecord.Status.INPROGRESS); assertThat(record.getResponseData()).isEqualTo("Response"); assertThat(status).isEqualTo(0); @@ -362,8 +363,8 @@ public void deleteRecord_cacheEnabled_shouldDeleteRecordFromCache() { persistenceStore.configure(IdempotencyConfig.builder() .withUseLocalCache(true).build(), null, cache); - cache.put("testFunction#7b40f56c086de5aa91dc467456329ed2", - new DataRecord("testFunction#7b40f56c086de5aa91dc467456329ed2", DataRecord.Status.COMPLETED, 123, null, + cache.put("testFunction#8d6a8f173b46479eff55e0997864a514", + new DataRecord("testFunction#8d6a8f173b46479eff55e0997864a514", DataRecord.Status.COMPLETED, 123, null, null)); persistenceStore.deleteRecord(JsonConfig.get().getObjectMapper().valueToTree(event), new ArithmeticException()); assertThat(status).isEqualTo(3); diff --git a/powertools-idempotency/src/test/resources/apigw_event.json b/powertools-idempotency/powertools-idempotency-core/src/test/resources/apigw_event.json similarity index 100% rename from powertools-idempotency/src/test/resources/apigw_event.json rename to powertools-idempotency/powertools-idempotency-core/src/test/resources/apigw_event.json diff --git a/powertools-idempotency/powertools-idempotency-dynamodb/dynamodb-local-metadata.json b/powertools-idempotency/powertools-idempotency-dynamodb/dynamodb-local-metadata.json new file mode 100644 index 000000000..c76c2c76d --- /dev/null +++ b/powertools-idempotency/powertools-idempotency-dynamodb/dynamodb-local-metadata.json @@ -0,0 +1 @@ +{"installationId":"e43b8515-8484-485c-8315-bead4568972b","telemetryEnabled":"true"} \ No newline at end of file diff --git a/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml b/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml new file mode 100644 index 000000000..e985f8f14 --- /dev/null +++ b/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml @@ -0,0 +1,126 @@ + + + + + 4.0.0 + + + software.amazon.lambda + powertools-idempotency + 2.0.0-RC1 + + + powertools-idempotency-dynamodb + jar + + Powertools for AWS Lambda (Java) library Idempotency - DynamoDB + + DynamoDB implementation for the idempotency module + + + + + software.amazon.lambda + powertools-idempotency-core + ${project.version} + + + software.amazon.awssdk + url-connection-client + ${aws.sdk.version} + + + software.amazon.awssdk + dynamodb + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + + + + + + com.amazonaws + DynamoDBLocal + [1.12,2.0) + test + + + + io.github.ganadist.sqlite4java + libsqlite4java-osx-aarch64 + 1.0.392 + test + dylib + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + + software.amazon.awssdk.enhanced.dynamodb + + + + + + dev.aspectj + aspectj-maven-plugin + ${aspectj-maven-plugin.version} + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + ignore + ${project.build.sourceEncoding} + + + software.amazon.lambda + powertools-idempotency-core + + + + + + + compile + test-compile + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + + \ No newline at end of file diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStore.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStore.java similarity index 94% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStore.java rename to powertools-idempotency/powertools-idempotency-dynamodb/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStore.java index 82e7b9ead..9c96541f6 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStore.java +++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStore.java @@ -12,19 +12,8 @@ * */ -package software.amazon.lambda.powertools.idempotency.persistence; +package software.amazon.lambda.powertools.idempotency.persistence.dynamodb; -import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_REGION_ENV; -import static software.amazon.lambda.powertools.core.internal.LambdaConstants.LAMBDA_FUNCTION_NAME_ENV; -import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.INPROGRESS; - -import java.time.Instant; -import java.util.AbstractMap; -import java.util.HashMap; -import java.util.Map; -import java.util.OptionalLong; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; @@ -39,11 +28,25 @@ import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; -import software.amazon.awssdk.utils.StringUtils; -import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator; +import software.amazon.lambda.powertools.common.internal.UserAgentConfigurator; import software.amazon.lambda.powertools.idempotency.Constants; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException; +import software.amazon.lambda.powertools.idempotency.persistence.BasePersistenceStore; +import software.amazon.lambda.powertools.idempotency.persistence.DataRecord; +import software.amazon.lambda.powertools.idempotency.persistence.PersistenceStore; + +import java.time.Instant; +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; +import java.util.OptionalLong; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static software.amazon.lambda.powertools.common.internal.LambdaConstants.AWS_REGION_ENV; +import static software.amazon.lambda.powertools.common.internal.LambdaConstants.LAMBDA_FUNCTION_NAME_ENV; +import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.INPROGRESS; /** * DynamoDB version of the {@link PersistenceStore}. Will store idempotency data in DynamoDB.
@@ -186,10 +189,17 @@ public void putRecord(DataRecord record, Instant now) throws IdempotencyItemAlre "attribute_not_exists(#id) OR #expiry < :now OR (attribute_exists(#in_progress_expiry) AND #in_progress_expiry < :now_milliseconds AND #status = :inprogress)") .expressionAttributeNames(expressionAttributeNames) .expressionAttributeValues(expressionAttributeValues) - .build() - ); + .returnValuesOnConditionCheckFailure("ALL_OLD") + .build()); } catch (ConditionalCheckFailedException e) { LOG.debug("Failed to put record for already existing idempotency key: {}", record.getIdempotencyKey()); + if (e.hasItem()) { + DataRecord existingRecord = itemToRecord(e.item()); + throw new IdempotencyItemAlreadyExistsException( + "Failed to put record for already existing idempotency key: " + record.getIdempotencyKey() + + ". Existing record: " + existingRecord, + e, existingRecord); + } throw new IdempotencyItemAlreadyExistsException( "Failed to put record for already existing idempotency key: " + record.getIdempotencyKey(), e); } @@ -310,7 +320,7 @@ public static class Builder { * @return an instance of the {@link DynamoDBPersistenceStore} */ public DynamoDBPersistenceStore build() { - if (StringUtils.isEmpty(tableName)) { + if (tableName == null || "".equals(tableName)) { throw new IllegalArgumentException("Table name is not specified"); } return new DynamoDBPersistenceStore(tableName, keyAttr, staticPkValue, sortKeyAttr, expiryAttr, diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/DynamoDBConfig.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBConfig.java similarity index 93% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/DynamoDBConfig.java rename to powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBConfig.java index 66ddb53ac..289b0f1cd 100644 --- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/DynamoDBConfig.java +++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBConfig.java @@ -12,15 +12,18 @@ * */ -package software.amazon.lambda.powertools.idempotency; +package software.amazon.lambda.powertools.idempotency.persistence.dynamodb; -import com.amazonaws.services.dynamodbv2.local.main.ServerRunner; -import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer; import java.io.IOException; import java.net.ServerSocket; import java.net.URI; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; + +import com.amazonaws.services.dynamodbv2.local.main.ServerRunner; +import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer; + import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; @@ -66,13 +69,12 @@ public static void setupDynamo() { .tableName(TABLE_NAME) .keySchema(KeySchemaElement.builder().keyType(KeyType.HASH).attributeName("id").build()) .attributeDefinitions( - AttributeDefinition.builder().attributeName("id").attributeType(ScalarAttributeType.S).build() - ) + AttributeDefinition.builder().attributeName("id").attributeType(ScalarAttributeType.S).build()) .billingMode(BillingMode.PAY_PER_REQUEST) .build()); - DescribeTableResponse response = - client.describeTable(DescribeTableRequest.builder().tableName(TABLE_NAME).build()); + DescribeTableResponse response = client + .describeTable(DescribeTableRequest.builder().tableName(TABLE_NAME).build()); if (response == null) { throw new RuntimeException("Table was not created within expected time"); } diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStoreTest.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java similarity index 94% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStoreTest.java rename to powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java index b19cebfe1..56b32c4f9 100644 --- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStoreTest.java +++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java @@ -12,16 +12,8 @@ * */ -package software.amazon.lambda.powertools.idempotency.persistence; +package software.amazon.lambda.powertools.idempotency.persistence.dynamodb; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -39,10 +31,20 @@ import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; import software.amazon.awssdk.services.dynamodb.model.ScanRequest; import software.amazon.lambda.powertools.idempotency.Constants; -import software.amazon.lambda.powertools.idempotency.DynamoDBConfig; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; + import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException; +import software.amazon.lambda.powertools.idempotency.persistence.DataRecord; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * These test are using DynamoDBLocal and sqlite, see https://nickolasfisher.com/blog/Configuring-an-In-Memory-DynamoDB-instance-with-Java-for-Integration-Testing @@ -153,13 +155,14 @@ public void putRecord_shouldThrowIdempotencyItemAlreadyExistsException_IfRecordA DataRecord.Status.INPROGRESS, expiry2, null, - null - ), now) - ).isInstanceOf(IdempotencyItemAlreadyExistsException.class); + null), + now)).isInstanceOf(IdempotencyItemAlreadyExistsException.class) + // DataRecord should be present due to returnValuesOnConditionCheckFailure("ALL_OLD") + .matches(e -> ((IdempotencyItemAlreadyExistsException) e).getDataRecord().isPresent()); // THEN: item was not updated, retrieve the initial one - Map itemInDb = - client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); + Map itemInDb = client + .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); assertThat(itemInDb).isNotNull(); assertThat(itemInDb.get("status").s()).isEqualTo("COMPLETED"); assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry)); @@ -188,13 +191,16 @@ public void putRecord_shouldBlockUpdate_IfRecordAlreadyExistAndProgressNotExpire DataRecord.Status.INPROGRESS, expiry2, "Fake Data 2", - null - ), now)) - .isInstanceOf(IdempotencyItemAlreadyExistsException.class); + null), + now)) + .isInstanceOf(IdempotencyItemAlreadyExistsException.class) + // DataRecord should be present due to returnValuesOnConditionCheckFailure("ALL_OLD") + .matches(e -> ((IdempotencyItemAlreadyExistsException) e).getDataRecord().isPresent()); + ; // THEN: item was not updated, retrieve the initial one - Map itemInDb = - client.getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); + Map itemInDb = client + .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item(); assertThat(itemInDb).isNotNull(); assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS"); assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry)); diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/IdempotencyTest.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/IdempotencyTest.java similarity index 78% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/IdempotencyTest.java rename to powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/IdempotencyTest.java index c94fec3db..7b43542c8 100644 --- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/IdempotencyTest.java +++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/IdempotencyTest.java @@ -12,20 +12,21 @@ * */ -package software.amazon.lambda.powertools.idempotency; - +package software.amazon.lambda.powertools.idempotency.persistence.dynamodb; import static org.assertj.core.api.Assertions.assertThat; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.amazonaws.services.lambda.runtime.tests.EventLoader; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.amazonaws.services.lambda.runtime.tests.EventLoader; + import software.amazon.awssdk.services.dynamodb.model.ScanRequest; -import software.amazon.lambda.powertools.idempotency.handlers.IdempotencyFunction; +import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.handlers.IdempotencyFunction; public class IdempotencyTest extends DynamoDBConfig { @@ -41,14 +42,14 @@ void setUp() { public void endToEndTest() { IdempotencyFunction function = new IdempotencyFunction(client); - APIGatewayProxyResponseEvent response = - function.handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context); + APIGatewayProxyResponseEvent response = function + .handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context); assertThat(function.handlerExecuted).isTrue(); function.handlerExecuted = false; - APIGatewayProxyResponseEvent response2 = - function.handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context); + APIGatewayProxyResponseEvent response2 = function + .handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context); assertThat(function.handlerExecuted).isFalse(); assertThat(response).isEqualTo(response2); diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/handlers/IdempotencyFunction.java similarity index 59% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java rename to powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/handlers/IdempotencyFunction.java index 76c36ae9f..d816af801 100644 --- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java +++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/handlers/IdempotencyFunction.java @@ -12,45 +12,37 @@ * */ -package software.amazon.lambda.powertools.idempotency.handlers; +package software.amazon.lambda.powertools.idempotency.persistence.dynamodb.handlers; + +import java.util.HashMap; +import java.util.Map; 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 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 org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; + import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.lambda.powertools.idempotency.Idempotency; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; import software.amazon.lambda.powertools.idempotency.Idempotent; -import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore; -import software.amazon.lambda.powertools.utilities.JsonConfig; +import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.DynamoDBPersistenceStore; public class IdempotencyFunction implements RequestHandler { - private final static Logger LOG = LogManager.getLogger(IdempotencyFunction.class); - public boolean handlerExecuted = false; public IdempotencyFunction(DynamoDbClient client) { // we need to initialize idempotency configuration before the handleRequest method is called Idempotency.config().withConfig( - IdempotencyConfig.builder() - .withEventKeyJMESPath("powertools_json(body).address") - .build()) + IdempotencyConfig.builder() + .withEventKeyJMESPath("powertools_json(body).address") + .build()) .withPersistenceStore( DynamoDBPersistenceStore.builder() .withTableName("idempotency_table") .withDynamoDbClient(client) - .build() - ).configure(); + .build()) + .configure(); } @Idempotent @@ -65,28 +57,10 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); - try { - String address = JsonConfig.get().getObjectMapper().readTree(input.getBody()).get("address").asText(); - final String pageContents = this.getPageContents(address); - String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - LOG.debug("ip is {}", pageContents); - return response - .withStatusCode(200) - .withBody(output); - - } catch (IOException e) { - return response - .withBody("{}") - .withStatusCode(500); - } - } + return response + .withStatusCode(200) + .withBody("{ \"message\": \"hello world\"}"); - // we could actually also put the @Idempotent annotation here - 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/powertools-idempotency/src/test/resources/apigw_event2.json b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/resources/apigw_event2.json similarity index 100% rename from powertools-idempotency/src/test/resources/apigw_event2.json rename to powertools-idempotency/powertools-idempotency-dynamodb/src/test/resources/apigw_event2.json diff --git a/powertools-idempotency/spotbugs-exclude.xml b/powertools-idempotency/spotbugs-exclude.xml new file mode 100644 index 000000000..9a2369c75 --- /dev/null +++ b/powertools-idempotency/spotbugs-exclude.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/powertools-large-messages/pom.xml b/powertools-large-messages/pom.xml index 04e46dc6e..9569c9ea8 100644 --- a/powertools-large-messages/pom.xml +++ b/powertools-large-messages/pom.xml @@ -23,44 +23,23 @@ software.amazon.lambda powertools-parent - 1.19.0-SNAPSHOT + 2.0.0-RC1 powertools-large-messages jar - Powertools for AWS Lambda (Java) library Large messages - - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - + Powertools for AWS Lambda (Java) - Large messages - - software.amazon.lambda - powertools-core - org.aspectj aspectjrt + provided + + + software.amazon.lambda + powertools-common com.amazonaws @@ -118,11 +97,26 @@ junit-pioneer test + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + org.apache.commons commons-lang3 test + + org.aspectj + aspectjweaver + test + org.assertj assertj-core @@ -146,11 +140,7 @@ - - org.apache.maven.plugins - maven-checkstyle-plugin - - \ No newline at end of file + diff --git a/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessageConfig.java b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessageConfig.java index fb8ea9b15..6ad529496 100644 --- a/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessageConfig.java +++ b/powertools-large-messages/src/main/java/software/amazon/lambda/powertools/largemessages/LargeMessageConfig.java @@ -14,7 +14,7 @@ package software.amazon.lambda.powertools.largemessages; -import static software.amazon.lambda.powertools.core.internal.LambdaConstants.AWS_REGION_ENV; +import static software.amazon.lambda.powertools.common.internal.LambdaConstants.AWS_REGION_ENV; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; diff --git a/powertools-logging/pom.xml b/powertools-logging/pom.xml index 656b9ee81..ade7c95fc 100644 --- a/powertools-logging/pom.xml +++ b/powertools-logging/pom.xml @@ -18,46 +18,25 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - powertools-logging - jar - powertools-parent software.amazon.lambda - 1.19.0-SNAPSHOT + 2.0.0-RC1 - Powertools for AWS Lambda (Java) library Logging - - A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. - - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - + Powertools for AWS Lambda (Java) - Logging + Set of utility for better logging - common + powertools-logging + jar software.amazon.lambda - powertools-core + powertools-common + + + software.amazon.lambda + powertools-serialization com.amazonaws @@ -68,24 +47,13 @@ jackson-databind - org.apache.logging.log4j - log4j-layout-template-json - - - org.apache.logging.log4j - log4j-core - - - org.apache.logging.log4j - log4j-slf4j2-impl - - - org.apache.logging.log4j - log4j-api + org.slf4j + slf4j-api org.aspectj aspectjrt + provided @@ -99,6 +67,21 @@ junit-jupiter-engine test + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + + + org.junit-pioneer + junit-pioneer + test + org.apache.commons commons-lang3 @@ -130,13 +113,104 @@ test - + + + generate-graalvm-files + + + org.mockito + mockito-subclass + 5.17.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging,experimental-class-define-support + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + 5.17.0 + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.2 + true + + + test-native + + test + + test + + + + powertools-logging + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + + + + + + + + + + + src/main/resources + + - - org.apache.maven.plugins - maven-checkstyle-plugin - org.apache.maven.plugins maven-surefire-plugin @@ -149,4 +223,4 @@ - \ No newline at end of file + diff --git a/powertools-logging/powertools-logging-log4j/pom.xml b/powertools-logging/powertools-logging-log4j/pom.xml new file mode 100644 index 000000000..e85fc6567 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/pom.xml @@ -0,0 +1,238 @@ + + + 4.0.0 + + + powertools-parent + software.amazon.lambda + 2.0.0-RC1 + ../../pom.xml + + + powertools-logging-log4j + jar + Powertools for AWS Lambda (Java) - Logging with Log4j2 + Set of utility for better logging with log4j + + + + software.amazon.lambda + powertools-logging + ${project.version} + + + org.aspectj + aspectjrt + provided + + + org.apache.logging.log4j + log4j-slf4j2-impl + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-layout-template-json + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + org.junit-pioneer + junit-pioneer + test + + + org.apache.commons + commons-lang3 + test + + + org.aspectj + aspectjweaver + test + + + org.assertj + assertj-core + test + + + com.amazonaws + aws-lambda-java-events + test + + + com.amazonaws + aws-lambda-java-tests + test + + + org.skyscreamer + jsonassert + test + + + + + generate-graalvm-files + + + org.mockito + mockito-subclass + 5.17.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j,experimental-class-define-support + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + 5.17.0 + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.2 + true + + + test-native + + test + + test + + + + powertools-logging-log4j + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + + + + + + + + + + + + src/main/resources + + + + + dev.aspectj + aspectj-maven-plugin + 1.14 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-logging + + + + + + + compile + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + testLog4j + eu-central-1 + <_X_AMZN_TRACE_ID>Root=1-63441c4a-abcdef012345678912345678 + + + + + + + diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java new file mode 100644 index 000000000..8ada50f49 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java @@ -0,0 +1,244 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package org.apache.logging.log4j.layout.template.json.resolver; + +import static java.util.Arrays.stream; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_ARN; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_COLD_START; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_NAME; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_REQUEST_ID; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_TRACE_ID; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_VERSION; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.SAMPLING_RATE; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.SERVICE; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.layout.template.json.util.JsonWriter; +import org.apache.logging.log4j.util.ReadOnlyStringMap; +import software.amazon.lambda.powertools.common.internal.LambdaConstants; +import software.amazon.lambda.powertools.common.internal.SystemWrapper; +import software.amazon.lambda.powertools.logging.argument.StructuredArgument; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; +import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; + +/** + * Custom {@link org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver} + * used by {@link org.apache.logging.log4j.layout.template.json.JsonTemplateLayout} + * to be able to recognize powertools fields in the LambdaJsonLayout.json file. + */ +final class PowertoolsResolver implements EventResolver { + + private static final EventResolver COLD_START_RESOLVER = new EventResolver() { + @Override + public boolean isResolvable(LogEvent logEvent) { + final String coldStart = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_COLD_START.getName()); + return null != coldStart; + } + + @Override + public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { + final String coldStart = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_COLD_START.getName()); + jsonWriter.writeBoolean(Boolean.parseBoolean(coldStart)); + } + }; + + private static final EventResolver FUNCTION_NAME_RESOLVER = + (final LogEvent logEvent, final JsonWriter jsonWriter) -> { + final String functionName = + logEvent.getContextData().getValue(FUNCTION_NAME.getName()); + jsonWriter.writeString(functionName); + }; + + private static final EventResolver FUNCTION_VERSION_RESOLVER = + (final LogEvent logEvent, final JsonWriter jsonWriter) -> { + final String functionVersion = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_VERSION.getName()); + jsonWriter.writeString(functionVersion); + }; + + private static final EventResolver FUNCTION_ARN_RESOLVER = + (final LogEvent logEvent, final JsonWriter jsonWriter) -> { + final String functionArn = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_ARN.getName()); + jsonWriter.writeString(functionArn); + }; + + private static final EventResolver FUNCTION_REQ_RESOLVER = + (final LogEvent logEvent, final JsonWriter jsonWriter) -> { + final String functionRequestId = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_REQUEST_ID.getName()); + jsonWriter.writeString(functionRequestId); + }; + + private static final EventResolver FUNCTION_MEMORY_RESOLVER = new EventResolver() { + @Override + public boolean isResolvable(LogEvent logEvent) { + final String functionMemory = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE.getName()); + return null != functionMemory; + } + + @Override + public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { + final String functionMemory = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE.getName()); + jsonWriter.writeNumber(Integer.parseInt(functionMemory)); + } + }; + + private static final EventResolver SAMPLING_RATE_RESOLVER = new EventResolver() { + @Override + public boolean isResolvable(LogEvent logEvent) { + final String samplingRate = + logEvent.getContextData().getValue(PowertoolsLoggedFields.SAMPLING_RATE.getName()); + try { + return (null != samplingRate && Float.parseFloat(samplingRate) > 0.f); + } catch (NumberFormatException nfe) { + return false; + } + } + + @Override + public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { + final String samplingRate = + logEvent.getContextData().getValue(PowertoolsLoggedFields.SAMPLING_RATE.getName()); + jsonWriter.writeNumber(Float.parseFloat(samplingRate)); + } + }; + + private static final EventResolver XRAY_TRACE_RESOLVER = new EventResolver() { + @Override + public boolean isResolvable(LogEvent logEvent) { + final String traceId = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_TRACE_ID.getName()); + return null != traceId; + } + + @Override + public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { + final String traceId = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_TRACE_ID.getName()); + jsonWriter.writeString(traceId); + } + }; + + private static final EventResolver SERVICE_RESOLVER = + (final LogEvent logEvent, final JsonWriter jsonWriter) -> { + final String service = logEvent.getContextData().getValue(PowertoolsLoggedFields.SERVICE.getName()); + jsonWriter.writeString(service); + }; + + private static final EventResolver REGION_RESOLVER = + (final LogEvent logEvent, final JsonWriter jsonWriter) -> + jsonWriter.writeString(SystemWrapper.getenv(LambdaConstants.AWS_REGION_ENV)); + + public static final String LAMBDA_ARN_REGEX = + "^arn:(aws|aws-us-gov|aws-cn):lambda:[a-zA-Z0-9-]+:\\d{12}:function:[a-zA-Z0-9-_]+(:[a-zA-Z0-9-_]+)?$"; + + private static final EventResolver ACCOUNT_ID_RESOLVER = new EventResolver() { + @Override + public boolean isResolvable(LogEvent logEvent) { + final String arn = logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_ARN.getName()); + return null != arn && !arn.isEmpty() && arn.matches(LAMBDA_ARN_REGEX); + } + + @Override + public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { + final String arn = logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_ARN.getName()); + jsonWriter.writeString(arn.split(":")[4]); + } + }; + + @SuppressWarnings("java:S106") + private static final EventResolver NON_POWERTOOLS_FIELD_RESOLVER = + (LogEvent logEvent, JsonWriter jsonWriter) -> { + StringBuilder stringBuilder = jsonWriter.getStringBuilder(); + try (JsonSerializer serializer = new JsonSerializer(stringBuilder)) { + + // remove dummy field to kick in powertools resolver + stringBuilder.setLength(stringBuilder.length() - 4); + + // log other MDC values + ReadOnlyStringMap contextData = logEvent.getContextData(); + contextData.forEach((key, value) -> { + if (!PowertoolsLoggedFields.stringValues().contains(key)) { + serializer.writeSeparator(); + serializer.writeObjectField(key, value); + } + }); + + // log structured arguments + Object[] arguments = logEvent.getMessage().getParameters(); + if (arguments != null) { + stream(arguments).filter(StructuredArgument.class::isInstance).forEach(argument -> { + try { + serializer.writeRaw(','); + ((StructuredArgument) argument).writeTo(serializer); + } catch (IOException e) { + System.err.printf("Failed to encode log event, error: %s.%n", e.getMessage()); + } + }); + } + } + }; + + private final EventResolver internalResolver; + + private static final Map eventResolverMap = Collections + .unmodifiableMap(Stream.of(new Object[][] { + { SERVICE.getName(), SERVICE_RESOLVER }, + { FUNCTION_NAME.getName(), FUNCTION_NAME_RESOLVER }, + { FUNCTION_VERSION.getName(), FUNCTION_VERSION_RESOLVER }, + { FUNCTION_ARN.getName(), FUNCTION_ARN_RESOLVER }, + { FUNCTION_MEMORY_SIZE.getName(), FUNCTION_MEMORY_RESOLVER }, + { FUNCTION_REQUEST_ID.getName(), FUNCTION_REQ_RESOLVER }, + { FUNCTION_COLD_START.getName(), COLD_START_RESOLVER }, + { FUNCTION_TRACE_ID.getName(), XRAY_TRACE_RESOLVER }, + { SAMPLING_RATE.getName(), SAMPLING_RATE_RESOLVER }, + { "region", REGION_RESOLVER }, + { "account_id", ACCOUNT_ID_RESOLVER } + }).collect(Collectors.toMap(data -> (String) data[0], data -> (EventResolver) data[1]))); + + + PowertoolsResolver(final TemplateResolverConfig config) { + final String fieldName = config.getString("field"); + if (fieldName == null) { + internalResolver = NON_POWERTOOLS_FIELD_RESOLVER; + } else { + internalResolver = eventResolverMap.get(fieldName); + if (internalResolver == null) { + throw new IllegalArgumentException("Unknown field: " + fieldName); + } + } + } + + @Override + public void resolve(LogEvent value, JsonWriter jsonWriter) { + internalResolver.resolve(value, jsonWriter); + } + + @Override + public boolean isResolvable(LogEvent value) { + return value != null && value.getContextData() != null && internalResolver.isResolvable(value); + } +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolverFactory.java b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverFactory.java similarity index 71% rename from powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolverFactory.java rename to powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverFactory.java index 7d688f469..297c00691 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolverFactory.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverFactory.java @@ -12,21 +12,20 @@ * */ -package software.amazon.lambda.powertools.logging.internal; +package org.apache.logging.log4j.layout.template.json.resolver; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.config.plugins.PluginFactory; -import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext; -import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory; +/** + * Factory for {@link PowertoolsResolver}. Log4j plugin to process powertools fields in the layout.json + */ @Plugin(name = "PowertoolsResolverFactory", category = TemplateResolverFactory.CATEGORY) public final class PowertoolsResolverFactory implements EventResolverFactory { private static final PowertoolsResolverFactory INSTANCE = new PowertoolsResolverFactory(); + private static final String RESOLVER_NAME = "powertools"; private PowertoolsResolverFactory() { } @@ -38,12 +37,12 @@ public static PowertoolsResolverFactory getInstance() { @Override public String getName() { - return PowertoolsResolver.getName(); + return RESOLVER_NAME; } @Override public TemplateResolver create(EventResolverContext context, TemplateResolverConfig config) { - return new PowertoolsResolver(); + return new PowertoolsResolver(config); } } diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4/internal/Log4jLoggingManager.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4/internal/Log4jLoggingManager.java new file mode 100644 index 000000000..4e57a8e45 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4/internal/Log4jLoggingManager.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.log4.internal; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configurator; +import org.slf4j.Logger; +import software.amazon.lambda.powertools.logging.internal.LoggingManager; + +/** + * LoggingManager for Log4j2 (see {@link LoggingManager}). + */ +public class Log4jLoggingManager implements LoggingManager { + + /** + * @inheritDoc + */ + @Override + @SuppressWarnings("java:S4792") + public void setLogLevel(org.slf4j.event.Level logLevel) { + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + Configurator.setAllLevels(LogManager.getRootLogger().getName(), Level.getLevel(logLevel.toString())); + ctx.updateLoggers(); + } + + /** + * @inheritDoc + */ + @Override + public org.slf4j.event.Level getLogLevel(Logger logger) { + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + return org.slf4j.event.Level.valueOf(ctx.getLogger(logger.getName()).getLevel().toString()); + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaEcsLayout.json b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaEcsLayout.json new file mode 100644 index 000000000..19f13f199 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaEcsLayout.json @@ -0,0 +1,89 @@ +{ + "@timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "timeZone": "UTC" + } + }, + "ecs.version": "1.2.0", + "log.level": { + "$resolver": "level", + "field": "name" + }, + "message": { + "$resolver": "message" + }, + "error.type": { + "$resolver": "exception", + "field": "className" + }, + "error.message": { + "$resolver": "exception", + "field": "message" + }, + "error.stack_trace": { + "$resolver": "exception", + "field": "stackTrace", + "stackTrace": { + "stringified": true + } + }, + "service.name": { + "$resolver": "powertools", + "field": "service" + }, + "service.version": { + "$resolver": "powertools", + "field": "function_version" + }, + "log.logger": { + "$resolver": "logger", + "field": "name" + }, + "process.thread.name": { + "$resolver": "thread", + "field": "name" + }, + "cloud.provider" : "aws", + "cloud.service.name" : "lambda", + "cloud.region" : { + "$resolver": "powertools", + "field": "region" + }, + "cloud.account.id" : { + "$resolver": "powertools", + "field": "account_id" + }, + "faas.id": { + "$resolver": "powertools", + "field": "function_arn" + }, + "faas.name": { + "$resolver": "powertools", + "field": "function_name" + }, + "faas.version": { + "$resolver": "powertools", + "field": "function_version" + }, + "faas.memory": { + "$resolver": "powertools", + "field": "function_memory_size" + }, + "faas.execution": { + "$resolver": "powertools", + "field": "function_request_id" + }, + "faas.coldstart": { + "$resolver": "powertools", + "field": "cold_start" + }, + "trace.id": { + "$resolver": "powertools", + "field": "xray_trace_id" + }, + "": { + "$resolver": "powertools" + } +} \ No newline at end of file diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json new file mode 100644 index 000000000..8b811ee5f --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json @@ -0,0 +1,71 @@ +{ + "level": { + "$resolver": "level", + "field": "name" + }, + "message": { + "$resolver": "message" + }, + "error": { + "message": { + "$resolver": "exception", + "field": "message" + }, + "name": { + "$resolver": "exception", + "field": "className" + }, + "stack": { + "$resolver": "exception", + "field": "stackTrace", + "stackTrace": { + "stringified": true + } + } + }, + "cold_start": { + "$resolver": "powertools", + "field": "cold_start" + }, + "function_arn": { + "$resolver": "powertools", + "field": "function_arn" + }, + "function_memory_size": { + "$resolver": "powertools", + "field": "function_memory_size" + }, + "function_name": { + "$resolver": "powertools", + "field": "function_name" + }, + "function_request_id": { + "$resolver": "powertools", + "field": "function_request_id" + }, + "function_version": { + "$resolver": "powertools", + "field": "function_version" + }, + "sampling_rate": { + "$resolver": "powertools", + "field": "sampling_rate" + }, + "service": { + "$resolver": "powertools", + "field": "service" + }, + "timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + }, + "xray_trace_id": { + "$resolver": "powertools", + "field": "xray_trace_id" + }, + "": { + "$resolver": "powertools" + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/jni-config.json b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/jni-config.json new file mode 100644 index 000000000..2c4de0562 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/jni-config.json @@ -0,0 +1,26 @@ +[ +{ + "name":"java.lang.Boolean", + "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.String", + "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"org.apache.maven.surefire.booter.ForkedBooter", + "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"sun.instrument.InstrumentationImpl", + "methods":[{"name":"","parameterTypes":["long","boolean","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] +}, +{ + "name":"sun.management.VMManagementImpl", + "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] +} +] diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/reflect-config.json b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/reflect-config.json new file mode 100644 index 000000000..9b2afe183 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/reflect-config.json @@ -0,0 +1,1225 @@ +[ +{ + "name":"[Lorg.apache.logging.log4j.core.Appender;" +}, +{ + "name":"[Lorg.apache.logging.log4j.core.config.AppenderRef;" +}, +{ + "name":"[Lorg.apache.logging.log4j.core.config.LoggerConfig;" +}, +{ + "name":"[Lorg.apache.logging.log4j.core.config.Property;" +}, +{ + "name":"[Lorg.apache.logging.log4j.layout.template.json.JsonTemplateLayout$EventTemplateAdditionalField;" +}, +{ + "name":"com.amazonaws.services.lambda.runtime.Context", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getAwsRequestId","parameterTypes":[] }, {"name":"getClientContext","parameterTypes":[] }, {"name":"getFunctionName","parameterTypes":[] }, {"name":"getFunctionVersion","parameterTypes":[] }, {"name":"getIdentity","parameterTypes":[] }, {"name":"getInvokedFunctionArn","parameterTypes":[] }, {"name":"getLogGroupName","parameterTypes":[] }, {"name":"getLogStreamName","parameterTypes":[] }, {"name":"getLogger","parameterTypes":[] }, {"name":"getMemoryLimitInMB","parameterTypes":[] }, {"name":"getRemainingTimeInMillis","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.RequestHandler", + "allDeclaredClasses":true, + "queryAllPublicMethods":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.SQSEvent$MessageAttribute", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getBinaryListValues","parameterTypes":[] }, {"name":"getBinaryValue","parameterTypes":[] }, {"name":"getDataType","parameterTypes":[] }, {"name":"getStringListValues","parameterTypes":[] }, {"name":"getStringValue","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.SQSEvent$SQSMessage", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getAttributes","parameterTypes":[] }, {"name":"getAwsRegion","parameterTypes":[] }, {"name":"getBody","parameterTypes":[] }, {"name":"getEventSource","parameterTypes":[] }, {"name":"getEventSourceArn","parameterTypes":[] }, {"name":"getMd5OfBody","parameterTypes":[] }, {"name":"getMd5OfMessageAttributes","parameterTypes":[] }, {"name":"getMessageAttributes","parameterTypes":[] }, {"name":"getMessageId","parameterTypes":[] }, {"name":"getReceiptHandle","parameterTypes":[] }] +}, +{ + "name":"com.fasterxml.jackson.core.JsonParser" +}, +{ + "name":"com.fasterxml.jackson.databind.JsonNode" +}, +{ + "name":"com.fasterxml.jackson.databind.ObjectMapper" +}, +{ + "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.fasterxml.jackson.dataformat.yaml.YAMLFactory" +}, +{ + "name":"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.tools.attach.VirtualMachine" +}, +{ + "name":"jakarta.servlet.Servlet" +}, +{ + "name":"java.io.Serializable", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.Class", + "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] +}, +{ + "name":"java.lang.ClassLoader", + "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] +}, +{ + "name":"java.lang.Cloneable", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.Comparable", + "allDeclaredClasses":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.Enum", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.Module", + "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] +}, +{ + "name":"java.lang.Object", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] +}, +{ + "name":"java.lang.ProcessEnvironment", + "fields":[{"name":"theCaseInsensitiveEnvironment"}, {"name":"theEnvironment"}] +}, +{ + "name":"java.lang.ProcessHandle", + "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime", + "methods":[{"name":"version","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime$Version", + "methods":[{"name":"feature","parameterTypes":[] }] +}, +{ + "name":"java.lang.StackWalker" +}, +{ + "name":"java.lang.String" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.Thread", + "fields":[{"name":"threadLocalRandomProbe"}] +}, +{ + "name":"java.lang.annotation.Retention", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.annotation.Target", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.constant.Constable", + "allDeclaredClasses":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.invoke.MethodHandle", + "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles", + "methods":[{"name":"lookup","parameterTypes":[] }] +}, +{ + "name":"java.lang.invoke.MethodHandles$Lookup", + "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] +}, +{ + "name":"java.lang.invoke.MethodType", + "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] +}, +{ + "name":"java.lang.reflect.AccessibleObject", + "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] +}, +{ + "name":"java.lang.reflect.AnnotatedArrayType", + "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedType", + "methods":[{"name":"getType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Executable", + "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Parameter", + "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.sql.Date" +}, +{ + "name":"java.sql.Time" +}, +{ + "name":"java.util.Collections$UnmodifiableMap", + "fields":[{"name":"m"}] +}, +{ + "name":"java.util.concurrent.ForkJoinTask", + "fields":[{"name":"aux"}, {"name":"status"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.Striped64", + "fields":[{"name":"base"}, {"name":"cellsBusy"}] +}, +{ + "name":"java.util.function.Consumer", + "queryAllPublicMethods":true +}, +{ + "name":"javax.servlet.Servlet" +}, +{ + "name":"jdk.internal.misc.Unsafe" +}, +{ + "name":"kotlin.jvm.JvmInline" +}, +{ + "name":"org.apache.logging.log4j.core.appender.AbstractAppender$Builder", + "allDeclaredFields":true +}, +{ + "name":"org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender$Builder", + "allDeclaredFields":true +}, +{ + "name":"org.apache.logging.log4j.core.appender.AppenderSet" +}, +{ + "name":"org.apache.logging.log4j.core.appender.AsyncAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.ConsoleAppender", + "queryAllDeclaredMethods":true, + "methods":[{"name":"newBuilder","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.appender.ConsoleAppender$Builder", + "allDeclaredFields":true +}, +{ + "name":"org.apache.logging.log4j.core.appender.CountingNoOpAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.FailoverAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.FailoversPlugin" +}, +{ + "name":"org.apache.logging.log4j.core.appender.FileAppender", + "queryAllDeclaredMethods":true, + "methods":[{"name":"newBuilder","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.appender.FileAppender$Builder", + "allDeclaredFields":true +}, +{ + "name":"org.apache.logging.log4j.core.appender.HttpAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.MemoryMappedFileAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.NullAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.OutputStreamAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.RandomAccessFileAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.RollingFileAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.RollingRandomAccessFileAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.ScriptAppenderSelector" +}, +{ + "name":"org.apache.logging.log4j.core.appender.SmtpAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.SocketAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.SyslogAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.WriterAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.db.ColumnMapping" +}, +{ + "name":"org.apache.logging.log4j.core.appender.db.jdbc.ColumnConfig" +}, +{ + "name":"org.apache.logging.log4j.core.appender.db.jdbc.DataSourceConnectionSource" +}, +{ + "name":"org.apache.logging.log4j.core.appender.db.jdbc.DriverManagerConnectionSource" +}, +{ + "name":"org.apache.logging.log4j.core.appender.db.jdbc.FactoryMethodConnectionSource" +}, +{ + "name":"org.apache.logging.log4j.core.appender.db.jdbc.JdbcAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.mom.JmsAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.mom.jeromq.JeroMqAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.nosql.NoSqlAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rewrite.LoggerNameLevelRewritePolicy" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rewrite.MapRewritePolicy" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rewrite.PropertiesRewritePolicy" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rewrite.RewriteAppender" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.CompositeTriggeringPolicy" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.CronTriggeringPolicy" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.DirectWriteRolloverStrategy" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.NoOpTriggeringPolicy" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.OnStartupTriggeringPolicy" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.SizeBasedTriggeringPolicy" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.TimeBasedTriggeringPolicy" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.action.DeleteAction" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.action.IfAccumulatedFileCount" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.action.IfAccumulatedFileSize" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.action.IfAll" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.action.IfAny" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.action.IfFileName" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.action.IfLastModified" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.action.IfNot" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.action.PathSortByModificationTime" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction" +}, +{ + "name":"org.apache.logging.log4j.core.appender.rolling.action.ScriptCondition" +}, +{ + "name":"org.apache.logging.log4j.core.appender.routing.IdlePurgePolicy" +}, +{ + "name":"org.apache.logging.log4j.core.appender.routing.Route" +}, +{ + "name":"org.apache.logging.log4j.core.appender.routing.Routes" +}, +{ + "name":"org.apache.logging.log4j.core.appender.routing.RoutingAppender" +}, +{ + "name":"org.apache.logging.log4j.core.async.ArrayBlockingQueueFactory" +}, +{ + "name":"org.apache.logging.log4j.core.async.AsyncLoggerConfig" +}, +{ + "name":"org.apache.logging.log4j.core.async.AsyncLoggerConfig$RootLogger" +}, +{ + "name":"org.apache.logging.log4j.core.async.AsyncWaitStrategyFactoryConfig" +}, +{ + "name":"org.apache.logging.log4j.core.async.DisruptorBlockingQueueFactory" +}, +{ + "name":"org.apache.logging.log4j.core.async.JCToolsBlockingQueueFactory" +}, +{ + "name":"org.apache.logging.log4j.core.async.LinkedTransferQueueFactory" +}, +{ + "name":"org.apache.logging.log4j.core.config.AppenderControlArraySet", + "fields":[{"name":"appenderArray"}] +}, +{ + "name":"org.apache.logging.log4j.core.config.AppenderRef", + "queryAllDeclaredMethods":true, + "methods":[{"name":"createAppenderRef","parameterTypes":["java.lang.String","org.apache.logging.log4j.Level","org.apache.logging.log4j.core.Filter"] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.AppendersPlugin", + "queryAllDeclaredMethods":true, + "methods":[{"name":"createAppenders","parameterTypes":["org.apache.logging.log4j.core.Appender[]"] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.CustomLevelConfig" +}, +{ + "name":"org.apache.logging.log4j.core.config.CustomLevels" +}, +{ + "name":"org.apache.logging.log4j.core.config.DefaultAdvertiser" +}, +{ + "name":"org.apache.logging.log4j.core.config.HttpWatcher" +}, +{ + "name":"org.apache.logging.log4j.core.config.LoggerConfig", + "queryAllDeclaredMethods":true, + "methods":[{"name":"newBuilder","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.LoggerConfig$Builder", + "allDeclaredFields":true +}, +{ + "name":"org.apache.logging.log4j.core.config.LoggerConfig$RootLogger", + "queryAllDeclaredMethods":true, + "methods":[{"name":"newRootBuilder","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.LoggerConfig$RootLogger$Builder", + "allDeclaredFields":true +}, +{ + "name":"org.apache.logging.log4j.core.config.LoggersPlugin", + "queryAllDeclaredMethods":true, + "methods":[{"name":"createLoggers","parameterTypes":["org.apache.logging.log4j.core.config.LoggerConfig[]"] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.PropertiesPlugin" +}, +{ + "name":"org.apache.logging.log4j.core.config.Property" +}, +{ + "name":"org.apache.logging.log4j.core.config.ScriptsPlugin" +}, +{ + "name":"org.apache.logging.log4j.core.config.arbiters.ClassArbiter" +}, +{ + "name":"org.apache.logging.log4j.core.config.arbiters.DefaultArbiter" +}, +{ + "name":"org.apache.logging.log4j.core.config.arbiters.EnvironmentArbiter" +}, +{ + "name":"org.apache.logging.log4j.core.config.arbiters.ScriptArbiter" +}, +{ + "name":"org.apache.logging.log4j.core.config.arbiters.SelectArbiter" +}, +{ + "name":"org.apache.logging.log4j.core.config.arbiters.SystemPropertyArbiter" +}, +{ + "name":"org.apache.logging.log4j.core.config.json.JsonConfigurationFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$BigDecimalConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$BigIntegerConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$BooleanConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$ByteArrayConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$ByteConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$CharArrayConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$CharacterConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$CharsetConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$ClassConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$CronExpressionConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$DoubleConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$DurationConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$FileConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$FloatConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$InetAddressConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$IntegerConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$LevelConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$LongConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$PathConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$PatternConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$SecurityProviderConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$ShortConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$StringConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$UriConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$UrlConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.convert.TypeConverters$UuidConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.validation.validators.RequiredValidator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.visitors.PluginAttributeVisitor", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.visitors.PluginBuilderAttributeVisitor", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.visitors.PluginConfigurationVisitor", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.plugins.visitors.PluginElementVisitor", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.xml.XmlConfigurationFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.config.yaml.YamlConfigurationFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.filter.AbstractFilterable$Builder", + "allDeclaredFields":true +}, +{ + "name":"org.apache.logging.log4j.core.filter.BurstFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.CompositeFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.DenyAllFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.DynamicThresholdFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.LevelMatchFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.LevelRangeFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.MapFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.MarkerFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.MutableThreadContextMapFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.NoMarkerFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.RegexFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.ScriptFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.StringMatchFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.StructuredDataFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.ThreadContextMapFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.ThresholdFilter" +}, +{ + "name":"org.apache.logging.log4j.core.filter.TimeFilter" +}, +{ + "name":"org.apache.logging.log4j.core.layout.CsvLogEventLayout" +}, +{ + "name":"org.apache.logging.log4j.core.layout.CsvParameterLayout" +}, +{ + "name":"org.apache.logging.log4j.core.layout.GelfLayout" +}, +{ + "name":"org.apache.logging.log4j.core.layout.HtmlLayout" +}, +{ + "name":"org.apache.logging.log4j.core.layout.JsonLayout" +}, +{ + "name":"org.apache.logging.log4j.core.layout.LevelPatternSelector" +}, +{ + "name":"org.apache.logging.log4j.core.layout.LoggerFields" +}, +{ + "name":"org.apache.logging.log4j.core.layout.MarkerPatternSelector" +}, +{ + "name":"org.apache.logging.log4j.core.layout.MessageLayout" +}, +{ + "name":"org.apache.logging.log4j.core.layout.PatternLayout" +}, +{ + "name":"org.apache.logging.log4j.core.layout.PatternMatch" +}, +{ + "name":"org.apache.logging.log4j.core.layout.Rfc5424Layout" +}, +{ + "name":"org.apache.logging.log4j.core.layout.ScriptPatternSelector" +}, +{ + "name":"org.apache.logging.log4j.core.layout.SerializedLayout" +}, +{ + "name":"org.apache.logging.log4j.core.layout.SyslogLayout" +}, +{ + "name":"org.apache.logging.log4j.core.layout.XmlLayout" +}, +{ + "name":"org.apache.logging.log4j.core.layout.YamlLayout" +}, +{ + "name":"org.apache.logging.log4j.core.lookup.ContextMapLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.DateLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.EnvironmentLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.EventLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.JavaLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.JmxRuntimeInputArgumentsLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.JndiLookup" +}, +{ + "name":"org.apache.logging.log4j.core.lookup.Log4jLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.LowerLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.MainMapLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.MapLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.MarkerLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.ResourceBundleLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.StructuredDataLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.SystemPropertiesLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.lookup.UpperLookup", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.core.net.MulticastDnsAdvertiser" +}, +{ + "name":"org.apache.logging.log4j.core.net.SocketAddress" +}, +{ + "name":"org.apache.logging.log4j.core.net.SocketOptions" +}, +{ + "name":"org.apache.logging.log4j.core.net.SocketPerformancePreferences" +}, +{ + "name":"org.apache.logging.log4j.core.net.ssl.KeyStoreConfiguration" +}, +{ + "name":"org.apache.logging.log4j.core.net.ssl.SslConfiguration" +}, +{ + "name":"org.apache.logging.log4j.core.net.ssl.TrustStoreConfiguration" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.AbstractStyleNameConverter$Black" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.AbstractStyleNameConverter$Blue" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.AbstractStyleNameConverter$Cyan" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.AbstractStyleNameConverter$Green" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.AbstractStyleNameConverter$Magenta" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.AbstractStyleNameConverter$Red" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.AbstractStyleNameConverter$White" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.AbstractStyleNameConverter$Yellow" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.ClassNamePatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.DatePatternConverter", + "queryAllDeclaredMethods":true, + "methods":[{"name":"newInstance","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"org.apache.logging.log4j.core.pattern.EncodingPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.EndOfBatchPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.EqualsIgnoreCaseReplacementConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.EqualsReplacementConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.ExtendedThrowablePatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.FileDatePatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.FileLocationPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.FullLocationPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.HighlightConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.IntegerPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.LevelPatternConverter", + "queryAllDeclaredMethods":true, + "methods":[{"name":"newInstance","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"org.apache.logging.log4j.core.pattern.LineLocationPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.LineSeparatorPatternConverter", + "queryAllDeclaredMethods":true, + "methods":[{"name":"newInstance","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"org.apache.logging.log4j.core.pattern.LoggerFqcnPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.LoggerPatternConverter", + "queryAllDeclaredMethods":true, + "methods":[{"name":"newInstance","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"org.apache.logging.log4j.core.pattern.MapPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.MarkerPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.MarkerSimpleNamePatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.MaxLengthConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.MdcPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.MessagePatternConverter", + "queryAllDeclaredMethods":true, + "methods":[{"name":"newInstance","parameterTypes":["org.apache.logging.log4j.core.config.Configuration","java.lang.String[]"] }] +}, +{ + "name":"org.apache.logging.log4j.core.pattern.MethodLocationPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.NanoTimePatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.NdcPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.ProcessIdPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.RegexReplacement" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.RegexReplacementConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.RelativeTimePatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.RepeatPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.RootThrowablePatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.SequenceNumberPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.StyleConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.ThreadIdPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.ThreadNamePatternConverter", + "queryAllDeclaredMethods":true, + "methods":[{"name":"newInstance","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"org.apache.logging.log4j.core.pattern.ThreadPriorityPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.ThrowablePatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.UuidPatternConverter" +}, +{ + "name":"org.apache.logging.log4j.core.pattern.VariablesNotEmptyReplacementConverter" +}, +{ + "name":"org.apache.logging.log4j.core.script.Script" +}, +{ + "name":"org.apache.logging.log4j.core.script.ScriptFile" +}, +{ + "name":"org.apache.logging.log4j.core.script.ScriptRef" +}, +{ + "name":"org.apache.logging.log4j.core.util.KeyValuePair" +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.JsonTemplateLayout", + "queryAllDeclaredMethods":true, + "methods":[{"name":"newBuilder","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.JsonTemplateLayout$Builder", + "allDeclaredFields":true +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.JsonTemplateLayout$EventTemplateAdditionalField" +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.CaseConverterResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.CounterResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.EndOfBatchResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.EventAdditionalFieldInterceptor", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.EventRootObjectKeyInterceptor", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.ExceptionResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.ExceptionRootCauseResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.LevelResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.LoggerResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.MainMapResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.MapResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.MarkerResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.MessageParameterResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.MessageResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.PatternResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.PowerToolsResolverFactoryTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"cleanUp","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }, {"name":"shouldLogInEcsFormat","parameterTypes":[] }, {"name":"shouldLogInJsonFormat","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.PowertoolsResolverArgumentsTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"cleanUp","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }, {"name":"shouldLogArgumentsAsJsonWhenUsingKeyValue","parameterTypes":[] }, {"name":"shouldLogArgumentsAsJsonWhenUsingRawJson","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.PowertoolsResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.SourceResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.ThreadContextDataResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.ThreadContextStackResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.ThreadResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.resolver.TimestampResolverFactory", + "queryAllDeclaredMethods":true, + "methods":[{"name":"getInstance","parameterTypes":[] }] +}, +{ + "name":"org.apache.logging.log4j.layout.template.json.util.RecyclerFactoryConverter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apiguardian.api.API", + "queryAllPublicMethods":true +}, +{ + "name":"org.aspectj.runtime.internal.AroundClosure", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.jctools.queues.MpmcArrayQueue" +}, +{ + "name":"org.osgi.framework.FrameworkUtil" +}, +{ + "name":"software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor", + "fields":[{"name":"IS_COLD_START"}] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.Log4jLoggingManagerTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getLogLevel_shouldReturnConfiguredLogLevel","parameterTypes":[] }, {"name":"resetLogLevel","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["com.amazonaws.services.lambda.runtime.events.SQSEvent$SQSMessage","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments$ArgumentFormat", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"sun.reflect.ReflectionFactory", + "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] +} +] diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/resource-config.json b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/resource-config.json new file mode 100644 index 000000000..aca0e0356 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-log4j/resource-config.json @@ -0,0 +1,95 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QLambdaEcsLayout.json\\E" + }, { + "pattern":"\\QLambdaJsonLayout.json\\E" + }, { + "pattern":"\\QMETA-INF/log4j-provider.properties\\E" + }, { + "pattern":"\\QMETA-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat\\E" + }, { + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.net.spi.InetAddressResolverProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/javax.xml.parsers.DocumentBuilderFactory\\E" + }, { + "pattern":"\\QMETA-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/org.apache.logging.log4j.core.util.WatchEventService\\E" + }, { + "pattern":"\\QMETA-INF/services/org.apache.logging.log4j.spi.Provider\\E" + }, { + "pattern":"\\QMETA-INF/services/org.apache.logging.log4j.util.PropertySource\\E" + }, { + "pattern":"\\QMETA-INF/services/org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.configuration.Configuration\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.presentation.Representation\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager\\E" + }, { + "pattern":"\\QStackTraceElementLayout.json\\E" + }, { + "pattern":"\\Qlog4j2-test.jsn\\E" + }, { + "pattern":"\\Qlog4j2-test.json\\E" + }, { + "pattern":"\\Qlog4j2-test.properties\\E" + }, { + "pattern":"\\Qlog4j2-test.xml\\E" + }, { + "pattern":"\\Qlog4j2-test.yaml\\E" + }, { + "pattern":"\\Qlog4j2-test.yml\\E" + }, { + "pattern":"\\Qlog4j2-test18b4aac2.jsn\\E" + }, { + "pattern":"\\Qlog4j2-test18b4aac2.json\\E" + }, { + "pattern":"\\Qlog4j2-test18b4aac2.properties\\E" + }, { + "pattern":"\\Qlog4j2-test18b4aac2.xml\\E" + }, { + "pattern":"\\Qlog4j2-test18b4aac2.yaml\\E" + }, { + "pattern":"\\Qlog4j2-test18b4aac2.yml\\E" + }, { + "pattern":"\\Qlog4j2.StatusLogger.properties\\E" + }, { + "pattern":"\\Qlog4j2.component.properties\\E" + }, { + "pattern":"\\Qlog4j2.jsn\\E" + }, { + "pattern":"\\Qlog4j2.json\\E" + }, { + "pattern":"\\Qlog4j2.properties\\E" + }, { + "pattern":"\\Qlog4j2.system.properties\\E" + }, { + "pattern":"\\Qlog4j2.xml\\E" + }, { + "pattern":"\\Qlog4j2.yaml\\E" + }, { + "pattern":"\\Qlog4j2.yml\\E" + }, { + "pattern":"\\Qlog4j218b4aac2.jsn\\E" + }, { + "pattern":"\\Qlog4j218b4aac2.json\\E" + }, { + "pattern":"\\Qlog4j218b4aac2.properties\\E" + }, { + "pattern":"\\Qlog4j218b4aac2.xml\\E" + }, { + "pattern":"\\Qlog4j218b4aac2.yaml\\E" + }, { + "pattern":"\\Qlog4j218b4aac2.yml\\E" + }]}, + "bundles":[] +} diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager new file mode 100644 index 000000000..d444c5525 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager @@ -0,0 +1 @@ +software.amazon.lambda.powertools.logging.log4.internal.Log4jLoggingManager \ No newline at end of file diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowerToolsResolverFactoryTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowerToolsResolverFactoryTest.java new file mode 100644 index 000000000..4cf798a47 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowerToolsResolverFactoryTest.java @@ -0,0 +1,95 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package org.apache.logging.log4j.layout.template.json.resolver; + +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +import com.amazonaws.services.lambda.runtime.Context; +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.slf4j.MDC; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled; + +@Order(1) +class PowerToolsResolverFactoryTest { + + @Mock + private Context context; + + @BeforeEach + void setUp() throws IllegalAccessException, IOException { + openMocks(this); + MDC.clear(); + writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); + setupContext(); + // Make sure file is cleaned up before running tests + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + FileChannel.open(Paths.get("target/ecslogfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // file may not exist on the first launch + } + } + + @AfterEach + void cleanUp() throws IOException{ + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + FileChannel.open(Paths.get("target/ecslogfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } + + @Test + void shouldLogInJsonFormat() { + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); + handler.handleRequest("Input", context); + + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains( + "{\"level\":\"DEBUG\",\"message\":\"Test debug event\",\"cold_start\":true,\"function_arn\":\"arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1\",\"function_memory_size\":1024,\"function_name\":\"testFunction\",\"function_request_id\":\"RequestId\",\"function_version\":\"1\",\"service\":\"testLog4j\",\"timestamp\":") + .contains("\"xray_trace_id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\"}\n"); + } + + @Test + void shouldLogInEcsFormat() { + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); + handler.handleRequest("Input", context); + + File logFile = new File("target/ecslogfile.json"); + assertThat(contentOf(logFile)).contains( + "\"ecs.version\":\"1.2.0\",\"log.level\":\"DEBUG\",\"message\":\"Test debug event\",\"service.name\":\"testLog4j\",\"service.version\":\"1\",\"log.logger\":\"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled\",\"process.thread.name\":\"main\",\"cloud.provider\":\"aws\",\"cloud.service.name\":\"lambda\",\"cloud.region\":\"eu-central-1\",\"cloud.account.id\":\"012345678910\",\"faas.id\":\"arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1\",\"faas.name\":\"testFunction\",\"faas.version\":\"1\",\"faas.memory\":1024,\"faas.execution\":\"RequestId\",\"faas.coldstart\":true,\"trace.id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\"}\n"); + } + + private void setupContext() { + when(context.getFunctionName()).thenReturn("testFunction"); + when(context.getInvokedFunctionArn()).thenReturn( + "arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1"); + when(context.getFunctionVersion()).thenReturn("1"); + when(context.getMemoryLimitInMB()).thenReturn(1024); + when(context.getAwsRequestId()).thenReturn("RequestId"); + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverArgumentsTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverArgumentsTest.java new file mode 100644 index 000000000..463ad043d --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverArgumentsTest.java @@ -0,0 +1,141 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package org.apache.logging.log4j.layout.template.json.resolver; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.slf4j.MDC; + +import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments; + +@Order(2) +class PowertoolsResolverArgumentsTest { + + @Mock + private Context context; + + @BeforeEach + void setUp() throws IOException { + openMocks(this); + MDC.clear(); + setupContext(); + + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + FileChannel.open(Paths.get("target/ecslogfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // may not be there in the first run + } + } + + @AfterEach + void cleanUp() throws IOException { + //Make sure file is cleaned up before running full stack logging regression + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + FileChannel.open(Paths.get("target/ecslogfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } + + @Test + void shouldLogArgumentsAsJsonWhenUsingRawJson() { + // GIVEN + PowertoolsArguments requestHandler = new PowertoolsArguments(PowertoolsArguments.ArgumentFormat.JSON); + SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); + msg.setMessageId("1212abcd"); + msg.setBody("plop"); + msg.setEventSource("eb"); + msg.setAwsRegion("eu-west-1"); + SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); + attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); + msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); + + // WHEN + requestHandler.handleRequest(msg, context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains( + "\"input\":{\"awsRegion\":\"eu-west-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}") + .contains("\"message\":\"1212abcd\"") + .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\""); + // Reserved keys should be ignored + PowertoolsLoggedFields.stringValues().stream().forEach(reservedKey -> { + assertThat(contentOf(logFile)).doesNotContain("\"" + reservedKey + "\":\"shouldBeIgnored\""); + assertThat(contentOf(logFile)).contains( + "\"message\":\"Attempted to use reserved key '" + reservedKey + + "' in structured argument. This key will be ignored.\""); + }); + } + + @Test + void shouldLogArgumentsAsJsonWhenUsingKeyValue() { + // GIVEN + PowertoolsArguments requestHandler = new PowertoolsArguments(PowertoolsArguments.ArgumentFormat.ENTRY); + SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); + msg.setMessageId("1212abcd"); + msg.setBody("plop"); + msg.setEventSource("eb"); + msg.setAwsRegion("eu-west-1"); + SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); + attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); + msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); + + // WHEN + requestHandler.handleRequest(msg, context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains( + "\"input\":{\"awsRegion\":\"eu-west-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}") + .contains("\"message\":\"1212abcd\"") + .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\""); + + // Reserved keys should be ignored + PowertoolsLoggedFields.stringValues().stream().forEach(reservedKey -> { + assertThat(contentOf(logFile)).doesNotContain("\"" + reservedKey + "\":\"shouldBeIgnored\""); + assertThat(contentOf(logFile)).contains( + "\"message\":\"Attempted to use reserved key '" + reservedKey + + "' in structured argument. This key will be ignored.\""); + }); + } + + private void setupContext() { + when(context.getFunctionName()).thenReturn("testFunction"); + when(context.getInvokedFunctionArn()).thenReturn("testArn"); + when(context.getFunctionVersion()).thenReturn("1"); + when(context.getMemoryLimitInMB()).thenReturn(10); + when(context.getAwsRequestId()).thenReturn("RequestId"); + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverTest.java new file mode 100644 index 000000000..d1b9fec83 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package org.apache.logging.log4j.layout.template.json.resolver; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_ARN; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_COLD_START; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.SAMPLING_RATE; + +import java.util.HashMap; +import java.util.Map; +import org.apache.logging.log4j.core.impl.Log4jLogEvent; +import org.apache.logging.log4j.layout.template.json.util.JsonWriter; +import org.apache.logging.log4j.util.SortedArrayStringMap; +import org.apache.logging.log4j.util.StringMap; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junitpioneer.jupiter.SetEnvironmentVariable; +import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; + +class PowertoolsResolverTest { + + @ParameterizedTest + @EnumSource(value = PowertoolsLoggedFields.class, + mode = EnumSource.Mode.EXCLUDE, + names = {"FUNCTION_MEMORY_SIZE", "SAMPLING_RATE", "FUNCTION_COLD_START", "CORRELATION_ID"}) + void shouldResolveFunctionStringInfo(PowertoolsLoggedFields field) { + String result = resolveField(field.getName(), "value"); + assertThat(result).isEqualTo("\"value\""); + } + + @Test + void shouldResolveMemorySize() { + String result = resolveField(FUNCTION_MEMORY_SIZE.getName(), "42"); + assertThat(result).isEqualTo("42"); + } + + @Test + void shouldResolveSamplingRate() { + String result = resolveField(SAMPLING_RATE.getName(), "0.4"); + assertThat(result).isEqualTo("0.4"); + } + + @Test + void shouldResolveColdStart() { + String result = resolveField(FUNCTION_COLD_START.getName(), "true"); + assertThat(result).isEqualTo("true"); + } + + @Test + void shouldResolveAccountId() { + String result = resolveField(FUNCTION_ARN.getName(), "account_id", "arn:aws:lambda:us-east-2:123456789012:function:my-function"); + assertThat(result).isEqualTo("\"123456789012\""); + } + + @Test + void unknownField_shouldThrowException() { + assertThatThrownBy(() -> resolveField("custom-random-unknown-field", "custom-random-unknown-field", "Once apon a time in Switzerland...")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unknown field: custom-random-unknown-field"); + } + + @Test + @SetEnvironmentVariable(key = "AWS_REGION", value = "eu-central-2") + void shouldResolveRegion() { + String result = resolveField("region", "dummy, will use the env var"); + assertThat(result).isEqualTo("\"eu-central-2\""); + } + + private static String resolveField(String field, String value) { + return resolveField(field, field, value); + } + + private static String resolveField(String data, String field, String value) { + Map configMap = new HashMap<>(); + configMap.put("field", field); + + TemplateResolverConfig config = new TemplateResolverConfig(configMap); + PowertoolsResolver resolver = new PowertoolsResolver(config); + JsonWriter writer = JsonWriter + .newBuilder() + .setMaxStringLength(1000) + .setTruncatedStringSuffix("") + .build(); + + StringMap contextMap = new SortedArrayStringMap(); + contextMap.putValue(data, value); + + Log4jLogEvent logEvent = Log4jLogEvent.newBuilder().setContextData(contextMap).build(); + if (resolver.isResolvable(logEvent)) { + resolver.resolve(logEvent, writer); + } + + return writer.getStringBuilder().toString(); + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/Log4jLoggingManagerTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/Log4jLoggingManagerTest.java new file mode 100644 index 000000000..69e1ee710 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/Log4jLoggingManagerTest.java @@ -0,0 +1,51 @@ +package software.amazon.lambda.powertools.logging.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.slf4j.event.Level.DEBUG; +import static org.slf4j.event.Level.ERROR; +import static org.slf4j.event.Level.WARN; + +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import software.amazon.lambda.powertools.logging.log4.internal.Log4jLoggingManager; + +class Log4jLoggingManagerTest { + + private final static Logger LOG = LoggerFactory.getLogger(Log4jLoggingManagerTest.class); + private final static Logger ROOT = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + + @Test + @Order(1) + void getLogLevel_shouldReturnConfiguredLogLevel() { + // Given log4j2.xml in resources + + // When + Log4jLoggingManager manager = new Log4jLoggingManager(); + Level logLevel = manager.getLogLevel(LOG); + Level rootLevel = manager.getLogLevel(ROOT); + + // Then + assertThat(logLevel).isEqualTo(DEBUG); + assertThat(rootLevel).isEqualTo(WARN); + } + + @Test + @Order(2) + void resetLogLevel() { + // Given log4j2.xml in resources + + // When + Log4jLoggingManager manager = new Log4jLoggingManager(); + manager.setLogLevel(ERROR); + + Level rootLevel = manager.getLogLevel(ROOT); + Level logLevel = manager.getLogLevel(LOG); + + // Then + assertThat(rootLevel).isEqualTo(ERROR); + assertThat(logLevel).isEqualTo(ERROR); + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java new file mode 100644 index 000000000..1fc235ff7 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal.handler; + +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.CORRELATION_ID; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.fasterxml.jackson.core.JsonProcessingException; + +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.logging.argument.StructuredArguments; +import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; +import software.amazon.lambda.powertools.utilities.JsonConfig; + +public class PowertoolsArguments implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsArguments.class); + private final ArgumentFormat argumentFormat; + + public PowertoolsArguments(ArgumentFormat argumentFormat) { + this.argumentFormat = argumentFormat; + } + + @Override + @Logging(clearState = true) + public String handleRequest(SQSEvent.SQSMessage input, Context context) { + try { + MDC.put(CORRELATION_ID.getName(), input.getMessageId()); + if (argumentFormat == ArgumentFormat.JSON) { + LOG.debug("SQS Event", + StructuredArguments.json("input", + JsonConfig.get().getObjectMapper().writeValueAsString(input)), + // function_name is a reserved key by PowertoolsLoggedFields and should be omitted + StructuredArguments.entry("function_name", "shouldBeIgnored")); + } else { + LOG.debug("SQS Event", + StructuredArguments.entry("input", input), + // function_name is a reserved key by PowertoolsLoggedFields and should be omitted + StructuredArguments.entry("function_name", "shouldBeIgnored")); + } + + // Attempt logging all reserved keys, the values should not be overwritten by "shouldBeIgnored" + final Map reservedKeysMap = new HashMap<>(); + for (String field : PowertoolsLoggedFields.stringValues()) { + reservedKeysMap.put(field, "shouldBeIgnored"); + } + reservedKeysMap.put("message", "shouldBeIgnored"); + reservedKeysMap.put("level", "shouldBeIgnored"); + reservedKeysMap.put("timestamp", "shouldBeIgnored"); + LOG.debug("Reserved keys", StructuredArguments.entries(reservedKeysMap)); + LOG.debug("{}", input.getMessageId()); + LOG.warn("Message body = {} and id = \"{}\"", input.getBody(), input.getMessageId()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return input.getMessageId(); + } + + public enum ArgumentFormat { + JSON, ENTRY + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java new file mode 100644 index 000000000..e8c0c5851 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal.handler; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogEnabled implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogEnabled.class); + + @Override + @Logging(clearState = true) + public Object handleRequest(Object input, Context context) { + MDC.put("myKey", "myValue"); + LOG.debug("Test debug event"); + return "Bonjour le monde"; + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/test/resources/junit-platform.properties b/powertools-logging/powertools-logging-log4j/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..80a2481d7 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/resources/junit-platform.properties @@ -0,0 +1,17 @@ +# +# Copyright 2023 Amazon.com, Inc. or its affiliates. +# 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 +# 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. +# +# + +# because of LambdaLoggingAspect static initialization of the LoggingManager, we need to +# set an order in the unit tests, especially LambdaLoggingAspectTest needs to be first +junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$OrderAnnotation \ No newline at end of file diff --git a/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2.xml b/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2.xml new file mode 100644 index 000000000..778077bc5 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + q + + + + + + + + + + \ No newline at end of file diff --git a/powertools-logging/powertools-logging-logback/pom.xml b/powertools-logging/powertools-logging-logback/pom.xml new file mode 100644 index 000000000..212379439 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/pom.xml @@ -0,0 +1,232 @@ + + + 4.0.0 + + powertools-parent + software.amazon.lambda + 2.0.0-RC1 + ../../pom.xml + + + powertools-logging-logback + Powertools for AWS Lambda (Java) - Logging with LogBack + Set of utility for better logging with logback + + + + software.amazon.lambda + powertools-logging + ${project.version} + + + org.aspectj + aspectjrt + provided + + + ch.qos.logback + logback-classic + 1.5.7 + + + com.sun.mail + javax.mail + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + org.apache.commons + commons-lang3 + test + + + org.aspectj + aspectjweaver + test + + + org.assertj + assertj-core + test + + + com.amazonaws + aws-lambda-java-events + test + + + com.amazonaws + aws-lambda-java-tests + test + + + org.skyscreamer + jsonassert + test + + + + + + generate-graalvm-files + + + org.mockito + mockito-subclass + 5.17.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback,experimental-class-define-support + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + 5.17.0 + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.2 + true + + + test-native + + test + + test + + + + + true + Standard + + powertools-logging-logback + + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + + + + + + + + + + + + + src/main/resources + + + + + dev.aspectj + aspectj-maven-plugin + 1.14 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-logging + + + + + + + compile + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + testLogback + eu-central-1 + <_X_AMZN_TRACE_ID>Root=1-63441c4a-abcdef012345678912345678 + + + + + + + + diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/JsonUtils.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/JsonUtils.java new file mode 100644 index 000000000..67d6b268d --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/JsonUtils.java @@ -0,0 +1,140 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.logback; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; +import java.util.TimeZone; +import java.util.TreeMap; + +import software.amazon.lambda.powertools.logging.argument.StructuredArgument; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; +import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; + +/** + * Json tools to serialize common fields + */ +final class JsonUtils { + + private JsonUtils() { + // static utils + } + + static void serializeTimestamp(JsonSerializer generator, long timestamp, String timestampFormat, + String timestampFormatTimezoneId, String timestampAttributeName) { + String formattedTimestamp; + if (timestampFormat == null || timestamp < 0) { + formattedTimestamp = String.valueOf(timestamp); + } else { + Date date = new Date(timestamp); + DateFormat format = new SimpleDateFormat(timestampFormat); + + if (timestampFormatTimezoneId != null) { + TimeZone tz = TimeZone.getTimeZone(timestampFormatTimezoneId); + format.setTimeZone(tz); + } + formattedTimestamp = format.format(date); + } + generator.writeStringField(timestampAttributeName, formattedTimestamp); + } + + static void serializeMDCEntries(Map mdcPropertyMap, JsonSerializer serializer) { + TreeMap sortedMap = new TreeMap<>(mdcPropertyMap); + for (Map.Entry entry : sortedMap.entrySet()) { + if (!PowertoolsLoggedFields.stringValues().contains(entry.getKey())) { + serializeMDCEntry(entry, serializer); + } + } + } + + static void serializeMDCEntry(Map.Entry entry, JsonSerializer serializer) { + serializer.writeRaw(','); + serializer.writeFieldName(entry.getKey()); + if (isString(entry.getValue())) { + serializer.writeString(entry.getValue()); + } else { + serializer.writeRaw(entry.getValue()); + } + } + + static void serializeArguments(ILoggingEvent event, JsonSerializer serializer) throws IOException { + Object[] arguments = event.getArgumentArray(); + if (arguments != null) { + for (Object argument : arguments) { + if (argument instanceof StructuredArgument) { + serializer.writeRaw(','); + ((StructuredArgument) argument).writeTo(serializer); + } + } + } + } + + /** + * As MDC is a {@code Map}, we need to check the type + * to output numbers and booleans correctly (without quotes) + */ + private static boolean isString(String str) { + if (str == null) { + return true; + } + if ("true".equals(str) || "false".equals(str)) { + return false; // boolean + } + return !isNumeric(str); // number + } + + /** + * Taken from commons-lang3 NumberUtils to avoid include the library + */ + private static boolean isNumeric(final String str) { + if (str == null || str.isEmpty()) { + return false; + } + if (str.charAt(str.length() - 1) == '.') { + return false; + } + if (str.charAt(0) == '-') { + if (str.length() == 1) { + return false; + } + return withDecimalsParsing(str, 1); + } + return withDecimalsParsing(str, 0); + } + + /** + * Taken from commons-lang3 NumberUtils + */ + private static boolean withDecimalsParsing(final String str, final int beginIdx) { + int decimalPoints = 0; + for (int i = beginIdx; i < str.length(); i++) { + final boolean isDecimalPoint = str.charAt(i) == '.'; + if (isDecimalPoint) { + decimalPoints++; + } + if (decimalPoints > 1) { + return false; + } + if (!isDecimalPoint && !Character.isDigit(str.charAt(i))) { + return false; + } + } + return true; + } +} diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java new file mode 100644 index 000000000..a1a7daff1 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java @@ -0,0 +1,282 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.logback; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_ARN; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_COLD_START; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_NAME; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_REQUEST_ID; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_TRACE_ID; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_VERSION; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeArguments; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeMDCEntries; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeTimestamp; + +import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; +import ch.qos.logback.classic.pattern.ThrowableProxyConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.ThrowableProxy; +import ch.qos.logback.core.encoder.EncoderBase; +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; + + +/** + * This class will encode the logback event into the format expected by the Elastic Common Schema (ECS) service (for Elasticsearch). + *
+ * Inspired from co.elastic.logging.logback.EcsEncoder, this class doesn't use + * any JSON (de)serialization library (Jackson, Gson, etc.) or Elastic library to avoid the dependency. + *
+ * This encoder also adds cloud information (see doc) + * and Lambda function information (see doc, currently in beta). + */ +public class LambdaEcsEncoder extends EncoderBase { + + protected static final String TIMESTAMP_ATTR_NAME = "@timestamp"; + protected static final String ECS_VERSION_ATTR_NAME = "ecs.version"; + protected static final String LOGGER_ATTR_NAME = "log.logger"; + protected static final String LEVEL_ATTR_NAME = "log.level"; + protected static final String SERVICE_NAME_ATTR_NAME = "service.name"; + protected static final String SERVICE_VERSION_ATTR_NAME = "service.version"; + protected static final String FORMATTED_MESSAGE_ATTR_NAME = "message"; + protected static final String THREAD_ATTR_NAME = "process.thread.name"; + protected static final String EXCEPTION_MSG_ATTR_NAME = "error.message"; + protected static final String EXCEPTION_CLASS_ATTR_NAME = "error.type"; + protected static final String EXCEPTION_STACK_ATTR_NAME = "error.stack_trace"; + protected static final String CLOUD_PROVIDER_ATTR_NAME = "cloud.provider"; + protected static final String CLOUD_REGION_ATTR_NAME = "cloud.region"; + protected static final String CLOUD_ACCOUNT_ATTR_NAME = "cloud.account.id"; + protected static final String CLOUD_SERVICE_ATTR_NAME = "cloud.service.name"; + protected static final String FUNCTION_COLD_START_ATTR_NAME = "faas.coldstart"; + protected static final String FUNCTION_REQUEST_ID_ATTR_NAME = "faas.execution"; + protected static final String FUNCTION_ARN_ATTR_NAME = "faas.id"; + protected static final String FUNCTION_NAME_ATTR_NAME = "faas.name"; + protected static final String FUNCTION_VERSION_ATTR_NAME = "faas.version"; + protected static final String FUNCTION_MEMORY_ATTR_NAME = "faas.memory"; + protected static final String FUNCTION_TRACE_ID_ATTR_NAME = "trace.id"; + + protected static final String ECS_VERSION = "1.2.0"; + protected static final String CLOUD_PROVIDER = "aws"; + protected static final String CLOUD_SERVICE = "lambda"; + + private final ThrowableProxyConverter throwableProxyConverter = new ThrowableProxyConverter(); + protected ThrowableHandlingConverter throwableConverter = null; + private boolean includeCloudInfo = true; + private boolean includeFaasInfo = true; + + @Override + public byte[] headerBytes() { + return new byte[0]; + } + + /** + * Main method of the encoder. Encode a logging event into Json format (with Elastic Search fields) + * + * @param event the logging event + * @return the encoded bytes + */ + + @SuppressWarnings("java:S106") + @Override + public byte[] encode(ILoggingEvent event) { + final Map mdcPropertyMap = event.getMDCPropertyMap(); + + StringBuilder builder = new StringBuilder(); + try (JsonSerializer serializer = new JsonSerializer(builder)) { + serializer.writeStartObject(); + serializeTimestamp(serializer, event.getTimeStamp(), "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "UTC", TIMESTAMP_ATTR_NAME); + serializer.writeRaw(','); + serializer.writeStringField(ECS_VERSION_ATTR_NAME, ECS_VERSION); + serializer.writeRaw(','); + serializer.writeStringField(LEVEL_ATTR_NAME, event.getLevel().toString()); + serializer.writeRaw(','); + serializer.writeStringField(FORMATTED_MESSAGE_ATTR_NAME, event.getFormattedMessage()); + + serializeException(event, serializer); + + serializer.writeRaw(','); + serializer.writeStringField(SERVICE_NAME_ATTR_NAME, LambdaHandlerProcessor.serviceName()); + serializer.writeRaw(','); + serializer.writeStringField(SERVICE_VERSION_ATTR_NAME, mdcPropertyMap.get(FUNCTION_VERSION.getName())); + serializer.writeRaw(','); + serializer.writeStringField(LOGGER_ATTR_NAME, event.getLoggerName()); + serializer.writeRaw(','); + serializer.writeStringField(THREAD_ATTR_NAME, event.getThreadName()); + + String arn = mdcPropertyMap.get(FUNCTION_ARN.getName()); + + serializeCloudInfo(serializer, arn); + + serializeFunctionInfo(serializer, arn, mdcPropertyMap); + + serializeMDCEntries(mdcPropertyMap, serializer); + + serializeArguments(event, serializer); + + serializer.writeEndObject(); + serializer.writeRaw('\n'); + } catch (IOException e) { + System.err.printf("Failed to encode log event, error: %s.%n", e.getMessage()); + } + return builder.toString().getBytes(UTF_8); + } + + private void serializeFunctionInfo(JsonSerializer serializer, String arn, Map mdcPropertyMap) { + if (includeFaasInfo) { + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_ARN_ATTR_NAME, arn); + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_NAME_ATTR_NAME, mdcPropertyMap.get(FUNCTION_NAME.getName())); + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_VERSION_ATTR_NAME, mdcPropertyMap.get(FUNCTION_VERSION.getName())); + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_MEMORY_ATTR_NAME, mdcPropertyMap.get(FUNCTION_MEMORY_SIZE.getName())); + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_REQUEST_ID_ATTR_NAME, mdcPropertyMap.get(FUNCTION_REQUEST_ID.getName())); + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_COLD_START_ATTR_NAME, mdcPropertyMap.get(FUNCTION_COLD_START.getName())); + serializer.writeRaw(','); + serializer.writeStringField(FUNCTION_TRACE_ID_ATTR_NAME, mdcPropertyMap.get(FUNCTION_TRACE_ID.getName())); + } + } + + private void serializeCloudInfo(JsonSerializer serializer, String arn) { + if (includeCloudInfo) { + serializer.writeRaw(','); + serializer.writeStringField(CLOUD_PROVIDER_ATTR_NAME, CLOUD_PROVIDER); + serializer.writeRaw(','); + serializer.writeStringField(CLOUD_SERVICE_ATTR_NAME, CLOUD_SERVICE); + if (arn != null) { + String[] arnParts = arn.split(":"); + serializer.writeRaw(','); + serializer.writeStringField(CLOUD_REGION_ATTR_NAME, arnParts[3]); + serializer.writeRaw(','); + serializer.writeStringField(CLOUD_ACCOUNT_ATTR_NAME, arnParts[4]); + } + } + } + + private void serializeException(ILoggingEvent event, JsonSerializer serializer) { + IThrowableProxy throwableProxy = event.getThrowableProxy(); + if (throwableProxy != null) { + if (throwableConverter != null) { + serializeException(serializer, throwableProxy.getClassName(), + throwableProxy.getMessage(), throwableConverter.convert(event)); + } else if (throwableProxy instanceof ThrowableProxy) { + Throwable throwable = ((ThrowableProxy) throwableProxy).getThrowable(); + serializeException(serializer, throwable.getClass().getName(), throwable.getMessage(), + Arrays.toString(throwable.getStackTrace())); + } else { + serializeException(serializer, throwableProxy.getClassName(), + throwableProxy.getMessage(), throwableProxyConverter.convert(event)); + } + } + } + + @Override + public byte[] footerBytes() { + return new byte[0]; + } + + /** + * Specify a throwable converter to format the stacktrace according to your need + * (default is null, no throwableConverter): + *
+ *
{@code
+     *     
+     *         
+     *              30
+     *              2048
+     *              20
+     *              sun\.reflect\..*\.invoke.*
+     *              net\.sf\.cglib\.proxy\.MethodProxy\.invoke
+     *              
+     *              true
+     *              true
+     *         
+     *     
+     * }
+ * + * @param throwableConverter converter for the throwable + */ + public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) { + this.throwableConverter = throwableConverter; + } + + /** + * Specify if cloud information should be logged (default is true): + *
    + *
  • cloud.provider
  • + *
  • cloud.service.name
  • + *
  • cloud.region
  • + *
  • cloud.account.id
  • + *
+ *
+ * We strongly recommend to keep these information. + *
+ *
{@code
+     *     
+     *         false
+     *     
+     * }
+ * + * @param includeCloudInfo if thread information should be logged + */ + public void setIncludeCloudInfo(boolean includeCloudInfo) { + this.includeCloudInfo = includeCloudInfo; + } + + /** + * Specify if Lambda function information should be logged (default is true): + *
    + *
  • faas.id
  • + *
  • faas.name
  • + *
  • faas.version
  • + *
  • faas.memory
  • + *
  • faas.execution
  • + *
  • faas.coldstart
  • + *
  • trace.id
  • + *
+ *
+ * We strongly recommend to keep these information. + *
+ *
{@code
+     *     
+     *         false
+     *     
+     * }
+ * + * @param includeFaasInfo if function information should be logged + */ + public void setIncludeFaasInfo(boolean includeFaasInfo) { + this.includeFaasInfo = includeFaasInfo; + } + + private void serializeException(JsonSerializer serializer, String className, String message, String stackTrace) { + serializer.writeRaw(','); + serializer.writeObjectField(EXCEPTION_MSG_ATTR_NAME, message); + serializer.writeRaw(','); + serializer.writeObjectField(EXCEPTION_CLASS_ATTR_NAME, className); + serializer.writeRaw(','); + serializer.writeObjectField(EXCEPTION_STACK_ATTR_NAME, stackTrace); + } +} diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java new file mode 100644 index 000000000..9afaf0ab7 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java @@ -0,0 +1,260 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.logback; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeArguments; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeMDCEntries; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeMDCEntry; +import static software.amazon.lambda.powertools.logging.logback.JsonUtils.serializeTimestamp; + +import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; +import ch.qos.logback.classic.pattern.ThrowableProxyConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.ThrowableProxy; +import ch.qos.logback.core.encoder.EncoderBase; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; +import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; + +/** + * Custom encoder for logback that encodes logs in JSON format. + */ +public class LambdaJsonEncoder extends EncoderBase { + + protected static final String TIMESTAMP_ATTR_NAME = "timestamp"; + protected static final String LEVEL_ATTR_NAME = "level"; + protected static final String FORMATTED_MESSAGE_ATTR_NAME = "message"; + protected static final String THREAD_ATTR_NAME = "thread"; + protected static final String THREAD_ID_ATTR_NAME = "thread_id"; + protected static final String THREAD_PRIORITY_ATTR_NAME = "thread_priority"; + protected static final String EXCEPTION_MSG_ATTR_NAME = "message"; + protected static final String EXCEPTION_CLASS_ATTR_NAME = "name"; + protected static final String EXCEPTION_STACK_ATTR_NAME = "stack"; + protected static final String EXCEPTION_ATTR_NAME = "error"; + + private final ThrowableProxyConverter throwableProxyConverter = new ThrowableProxyConverter(); + protected ThrowableHandlingConverter throwableConverter = null; + protected String timestampFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + protected String timestampFormatTimezoneId = null; + private boolean includeThreadInfo = false; + private boolean includePowertoolsInfo = true; + + @Override + public byte[] headerBytes() { + return new byte[0]; + } + + @Override + public void start() { + super.start(); + throwableProxyConverter.start(); + if (throwableConverter != null) { + throwableConverter.start(); + } + } + + @SuppressWarnings("java:S106") + @Override + public byte[] encode(ILoggingEvent event) { + StringBuilder builder = new StringBuilder(); + try (JsonSerializer serializer = new JsonSerializer(builder)) { + serializer.writeStartObject(); + serializer.writeStringField(LEVEL_ATTR_NAME, event.getLevel().toString()); + serializer.writeRaw(','); + serializer.writeStringField(FORMATTED_MESSAGE_ATTR_NAME, event.getFormattedMessage()); + + serializeException(event, serializer); + + TreeMap sortedMap = new TreeMap<>(event.getMDCPropertyMap()); + serializePowertools(sortedMap, serializer); + + serializeMDCEntries(sortedMap, serializer); + + serializeArguments(event, serializer); + + serializeThreadInfo(event, serializer); + + serializer.writeRaw(','); + serializeTimestamp(serializer, event.getTimeStamp(), + timestampFormat, timestampFormatTimezoneId, TIMESTAMP_ATTR_NAME); + + serializer.writeEndObject(); + serializer.writeRaw('\n'); + } catch (IOException e) { + System.err.printf("Failed to encode log event, error: %s.%n", e.getMessage()); + } + return builder.toString().getBytes(UTF_8); + } + + private void serializeThreadInfo(ILoggingEvent event, JsonSerializer serializer) { + if (includeThreadInfo) { + if (event.getThreadName() != null) { + serializer.writeRaw(','); + serializer.writeStringField(THREAD_ATTR_NAME, event.getThreadName()); + } + serializer.writeRaw(','); + serializer.writeNumberField(THREAD_ID_ATTR_NAME, Thread.currentThread().getId()); + serializer.writeRaw(','); + serializer.writeNumberField(THREAD_PRIORITY_ATTR_NAME, Thread.currentThread().getPriority()); + } + } + + private void serializePowertools(TreeMap sortedMap, JsonSerializer serializer) { + if (includePowertoolsInfo) { + for (Map.Entry entry : sortedMap.entrySet()) { + if (PowertoolsLoggedFields.stringValues().contains(entry.getKey()) + && !(entry.getKey().equals(PowertoolsLoggedFields.SAMPLING_RATE.getName()) && entry.getValue().equals("0.0"))) { + serializeMDCEntry(entry, serializer); + } + } + } + } + + private void serializeException(ILoggingEvent event, JsonSerializer serializer) { + IThrowableProxy throwableProxy = event.getThrowableProxy(); + if (throwableProxy != null) { + if (throwableConverter != null) { + serializeException(serializer, throwableProxy.getClassName(), + throwableProxy.getMessage(), throwableConverter.convert(event)); + } else if (throwableProxy instanceof ThrowableProxy) { + Throwable throwable = ((ThrowableProxy) throwableProxy).getThrowable(); + serializeException(serializer, throwable.getClass().getName(), throwable.getMessage(), + Arrays.toString(throwable.getStackTrace())); + } else { + serializeException(serializer, throwableProxy.getClassName(), throwableProxy.getMessage(), throwableProxyConverter.convert( + event)); + } + } + } + + @Override + public byte[] footerBytes() { + return new byte[0]; + } + + /** + * Specify the format of the timestamp (default is yyyy-MM-dd'T'HH:mm:ss.SSS'Z'). + * Note that if you use the Lambda Advanced Logging Configuration, you should keep the default format. + *
+ *
{@code
+     *     
+     *         yyyy-MM-dd'T'HH:mm:ss.SSSZz
+     *     
+     * }
+ * + * @param timestampFormat format of the timestamp (compatible with {@link java.text.SimpleDateFormat}) + */ + public void setTimestampFormat(String timestampFormat) { + this.timestampFormat = timestampFormat; + } + + /** + * Specify the format of the time zone id for timestamp (default is null, no timezone): + *
+ *
{@code
+     *     
+     *         Europe/Paris
+     *     
+     * }
+ * + * @param timestampFormatTimezoneId Zone Id (see {@link java.util.TimeZone}) + */ + public void setTimestampFormatTimezoneId(String timestampFormatTimezoneId) { + this.timestampFormatTimezoneId = timestampFormatTimezoneId; + } + + /** + * Specify a throwable converter to format the stacktrace according to your need + * (default is null, no throwableConverter): + *
+ *
{@code
+     *     
+     *         
+     *              30
+     *              2048
+     *              20
+     *              sun\.reflect\..*\.invoke.*
+     *              net\.sf\.cglib\.proxy\.MethodProxy\.invoke
+     *              
+     *              true
+     *              true
+     *         
+     *     
+     * }
+ * + * @param throwableConverter converter for the throwable + */ + public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) { + this.throwableConverter = throwableConverter; + } + + /** + * Specify if thread information should be logged (default is false) + *
+ *
{@code
+     *     
+     *         true
+     *     
+     * }
+ * + * @param includeThreadInfo if thread information should be logged + */ + public void setIncludeThreadInfo(boolean includeThreadInfo) { + this.includeThreadInfo = includeThreadInfo; + } + + /** + * Specify if Lambda function information should be logged (default is true): + *
    + *
  • function_name
  • + *
  • function_version
  • + *
  • function_arn
  • + *
  • function_memory_size
  • + *
  • function_request_id
  • + *
  • cold_start
  • + *
  • xray_trace_id
  • + *
  • sampling_rate
  • + *
  • service
  • + *
+ *
+ * We strongly recommend to keep these information. + *
+ *
{@code
+     *     
+     *         false
+     *     
+     * }
+ * + * @param includePowertoolsInfo if function information should be logged + */ + public void setIncludePowertoolsInfo(boolean includePowertoolsInfo) { + this.includePowertoolsInfo = includePowertoolsInfo; + } + + private void serializeException(JsonSerializer serializer, String className, String message, String stackTrace) { + Map map = new HashMap<>(); + map.put(EXCEPTION_MSG_ATTR_NAME, message); + map.put(EXCEPTION_CLASS_ATTR_NAME, className); + map.put(EXCEPTION_STACK_ATTR_NAME, stackTrace); + serializer.writeRaw(','); + serializer.writeObjectField(EXCEPTION_ATTR_NAME, map); + } +} diff --git a/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LogbackLoggingManager.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LogbackLoggingManager.java new file mode 100644 index 000000000..86982b444 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LogbackLoggingManager.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.logback.internal; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import java.util.List; +import org.slf4j.ILoggerFactory; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.logging.internal.LoggingManager; + +/** + * LoggingManager for Logback (see {@link LoggingManager}). + */ +public class LogbackLoggingManager implements LoggingManager { + + private final LoggerContext loggerContext; + + public LogbackLoggingManager() { + ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); + if (!(loggerFactory instanceof LoggerContext)) { + throw new RuntimeException("LoggerFactory does not match required type: " + LoggerContext.class.getName()); + } + loggerContext = (LoggerContext) loggerFactory; + } + + /** + * @inheritDoc + */ + @Override + @SuppressWarnings("java:S4792") + public void setLogLevel(org.slf4j.event.Level logLevel) { + List loggers = loggerContext.getLoggerList(); + for (Logger logger : loggers) { + logger.setLevel(Level.convertAnSLF4JLevel(logLevel)); + } + } + + /** + * @inheritDoc + */ + @Override + public org.slf4j.event.Level getLogLevel(org.slf4j.Logger logger) { + return org.slf4j.event.Level.valueOf(loggerContext.getLogger(logger.getName()).getEffectiveLevel().toString()); + } +} diff --git a/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/jni-config.json b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/jni-config.json new file mode 100644 index 000000000..2c4de0562 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/jni-config.json @@ -0,0 +1,26 @@ +[ +{ + "name":"java.lang.Boolean", + "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.String", + "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"org.apache.maven.surefire.booter.ForkedBooter", + "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"sun.instrument.InstrumentationImpl", + "methods":[{"name":"","parameterTypes":["long","boolean","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] +}, +{ + "name":"sun.management.VMManagementImpl", + "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] +} +] diff --git a/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/reflect-config.json b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/reflect-config.json new file mode 100644 index 000000000..0d3bdbe7e --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/reflect-config.json @@ -0,0 +1,290 @@ +[ +{ + "name":"ch.qos.logback.classic.joran.SerializedModelConfigurator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.classic.util.DefaultJoranConfigurator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.core.FileAppender", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setFile","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"ch.qos.logback.core.OutputStreamAppender", + "methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }] +}, +{ + "name":"ch.qos.logback.core.encoder.Encoder", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.Context", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getAwsRequestId","parameterTypes":[] }, {"name":"getClientContext","parameterTypes":[] }, {"name":"getFunctionName","parameterTypes":[] }, {"name":"getFunctionVersion","parameterTypes":[] }, {"name":"getIdentity","parameterTypes":[] }, {"name":"getInvokedFunctionArn","parameterTypes":[] }, {"name":"getLogGroupName","parameterTypes":[] }, {"name":"getLogStreamName","parameterTypes":[] }, {"name":"getLogger","parameterTypes":[] }, {"name":"getMemoryLimitInMB","parameterTypes":[] }, {"name":"getRemainingTimeInMillis","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.SQSEvent$MessageAttribute", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getBinaryListValues","parameterTypes":[] }, {"name":"getBinaryValue","parameterTypes":[] }, {"name":"getDataType","parameterTypes":[] }, {"name":"getStringListValues","parameterTypes":[] }, {"name":"getStringValue","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.SQSEvent$SQSMessage", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getAttributes","parameterTypes":[] }, {"name":"getAwsRegion","parameterTypes":[] }, {"name":"getBody","parameterTypes":[] }, {"name":"getEventSource","parameterTypes":[] }, {"name":"getEventSourceArn","parameterTypes":[] }, {"name":"getMd5OfBody","parameterTypes":[] }, {"name":"getMd5OfMessageAttributes","parameterTypes":[] }, {"name":"getMessageAttributes","parameterTypes":[] }, {"name":"getMessageId","parameterTypes":[] }, {"name":"getReceiptHandle","parameterTypes":[] }] +}, +{ + "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.tools.attach.VirtualMachine" +}, +{ + "name":"java.io.FilePermission" +}, +{ + "name":"java.io.IOException" +}, +{ + "name":"java.io.InputStream" +}, +{ + "name":"java.io.OutputStream" +}, +{ + "name":"java.io.Serializable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.Class", + "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] +}, +{ + "name":"java.lang.ClassLoader", + "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] +}, +{ + "name":"java.lang.Cloneable", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.Module", + "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] +}, +{ + "name":"java.lang.Object", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] +}, +{ + "name":"java.lang.ProcessHandle", + "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime", + "methods":[{"name":"version","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime$Version", + "methods":[{"name":"feature","parameterTypes":[] }] +}, +{ + "name":"java.lang.RuntimePermission" +}, +{ + "name":"java.lang.StackWalker" +}, +{ + "name":"java.lang.String" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.annotation.Retention", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.annotation.Target", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.invoke.MethodHandle", + "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles", + "methods":[{"name":"lookup","parameterTypes":[] }] +}, +{ + "name":"java.lang.invoke.MethodHandles$Lookup", + "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] +}, +{ + "name":"java.lang.invoke.MethodType", + "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] +}, +{ + "name":"java.lang.reflect.AccessibleObject", + "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] +}, +{ + "name":"java.lang.reflect.AnnotatedArrayType", + "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedType", + "methods":[{"name":"getType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Executable", + "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Parameter", + "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] +}, +{ + "name":"java.net.NetPermission" +}, +{ + "name":"java.net.SocketPermission" +}, +{ + "name":"java.net.URLPermission", + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.security.AllPermission" +}, +{ + "name":"java.security.SecurityPermission" +}, +{ + "name":"java.util.PropertyPermission" +}, +{ + "name":"java.util.concurrent.ForkJoinTask", + "fields":[{"name":"aux"}, {"name":"status"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"javax.smartcardio.CardPermission" +}, +{ + "name":"jdk.internal.misc.Unsafe" +}, +{ + "name":"kotlin.jvm.JvmInline" +}, +{ + "name":"org.apiguardian.api.API", + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor", + "fields":[{"name":"IS_COLD_START"}] +}, +{ + "name":"software.amazon.lambda.powertools.logging.LogbackLoggingManagerTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getLogLevel_shouldReturnConfiguredLogLevel","parameterTypes":[] }, {"name":"resetLogLevel","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.LambdaEcsEncoderTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"cleanUp","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }, {"name":"shouldLogException","parameterTypes":[] }, {"name":"shouldLogInEcsFormat","parameterTypes":[] }, {"name":"shouldNotLogCloudInfo","parameterTypes":[] }, {"name":"shouldNotLogFunctionInfo","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.LambdaJsonEncoderTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"cleanUp","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }, {"name":"shouldLogArgumentsAsJsonWhenUsingKeyValue","parameterTypes":[] }, {"name":"shouldLogArgumentsAsJsonWhenUsingRawJson","parameterTypes":[] }, {"name":"shouldLogEventAsStringForStreamHandler","parameterTypes":[] }, {"name":"shouldLogEventForHandlerWhenEnvVariableSetToTrue","parameterTypes":[] }, {"name":"shouldLogEventForHandlerWithLogEventAnnotation","parameterTypes":[] }, {"name":"shouldLogException","parameterTypes":[] }, {"name":"shouldLogInJsonFormat","parameterTypes":[] }, {"name":"shouldLogResponseForHandlerWhenEnvVariableSetToTrue","parameterTypes":[] }, {"name":"shouldLogResponseForHandlerWithLogResponseAnnotation","parameterTypes":[] }, {"name":"shouldLogResponseForStreamHandler","parameterTypes":[] }, {"name":"shouldLogStructuredArgumentsAsNewEntries","parameterTypes":[] }, {"name":"shouldLogThreadInfo","parameterTypes":[] }, {"name":"shouldLogTimestampDifferently","parameterTypes":[] }, {"name":"shouldNotLogEventForHandlerWhenEnvVariableSetToFalse","parameterTypes":[] }, {"name":"shouldNotLogPowertoolsInfo","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments", + "methods":[{"name":"handleRequest","parameterTypes":["com.amazonaws.services.lambda.runtime.events.SQSEvent$SQSMessage","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled", + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEvent", + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEventDisabled", + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEventForStream", + "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogResponse", + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogResponseForStream", + "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.logback.LambdaEcsEncoder", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.logback.LambdaJsonEncoder", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.reflect.ReflectionFactory", + "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] +} +] diff --git a/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/resource-config.json b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/resource-config.json new file mode 100644 index 000000000..dea71883a --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging-logback/resource-config.json @@ -0,0 +1,29 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E" + }, { + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/javax.xml.parsers.SAXParserFactory\\E" + }, { + "pattern":"\\QMETA-INF/services/org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.configuration.Configuration\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.presentation.Representation\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager\\E" + }, { + "pattern":"\\Qlogback-test.scmo\\E" + }, { + "pattern":"\\Qlogback-test.xml\\E" + }, { + "pattern":"\\Qlogback.scmo\\E" + }]}, + "bundles":[] +} diff --git a/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager new file mode 100644 index 000000000..958f2e459 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager @@ -0,0 +1 @@ +software.amazon.lambda.powertools.logging.logback.internal.LogbackLoggingManager \ No newline at end of file diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java new file mode 100644 index 000000000..214057917 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.slf4j.event.Level.DEBUG; +import static org.slf4j.event.Level.ERROR; +import static org.slf4j.event.Level.WARN; + +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import software.amazon.lambda.powertools.logging.logback.internal.LogbackLoggingManager; + +class LogbackLoggingManagerTest { + + private static final Logger LOG = LoggerFactory.getLogger(LogbackLoggingManagerTest.class); + private static final Logger ROOT = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + + @Test + @Order(1) + void getLogLevel_shouldReturnConfiguredLogLevel() { + LogbackLoggingManager manager = new LogbackLoggingManager(); + Level logLevel = manager.getLogLevel(LOG); + assertThat(logLevel).isEqualTo(DEBUG); + + logLevel = manager.getLogLevel(ROOT); + assertThat(logLevel).isEqualTo(WARN); + } + + @Test + @Order(2) + void resetLogLevel() { + LogbackLoggingManager manager = new LogbackLoggingManager(); + manager.setLogLevel(ERROR); + + Level logLevel = manager.getLogLevel(LOG); + assertThat(logLevel).isEqualTo(ERROR); + } +} diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsEncoderTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsEncoderTest.java new file mode 100644 index 000000000..5dcca2fb2 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsEncoderTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter; +import ch.qos.logback.classic.spi.LoggingEvent; +import com.amazonaws.services.lambda.runtime.Context; +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.logging.logback.LambdaEcsEncoder; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled; + +@Order(3) +class LambdaEcsEncoderTest { + + private static final Logger logger = (Logger) LoggerFactory.getLogger(LambdaEcsEncoderTest.class.getName()); + + + @Mock + private Context context; + + @BeforeEach + void setUp() throws IllegalAccessException, IOException { + openMocks(this); + MDC.clear(); + writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); + setupContext(); + // Make sure file is cleaned up before running tests + try { + FileChannel.open(Paths.get("target/ecslogfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // file may not exist on the first launch + } + } + + @AfterEach + void cleanUp() throws IOException{ + FileChannel.open(Paths.get("target/ecslogfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } + + @Test + void shouldLogInEcsFormat() { + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); + handler.handleRequest("Input", context); + + File logFile = new File("target/ecslogfile.json"); + assertThat(contentOf(logFile)).contains( + "\"ecs.version\":\"1.2.0\",\"log.level\":\"DEBUG\",\"message\":\"Test debug event\",\"service.name\":\"testLogback\",\"service.version\":\"1\",\"log.logger\":\"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled\",\"process.thread.name\":\"main\",\"cloud.provider\":\"aws\",\"cloud.service.name\":\"lambda\",\"cloud.region\":\"eu-west-1\",\"cloud.account.id\":\"012345678910\",\"faas.id\":\"arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1\",\"faas.name\":\"testFunction\",\"faas.version\":\"1\",\"faas.memory\":\"1024\",\"faas.execution\":\"RequestId\",\"faas.coldstart\":\"true\",\"trace.id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\"}\n"); + } + + private final LoggingEvent loggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "message", null, null); + + @Test + void shouldNotLogFunctionInfo() { + // GIVEN + LambdaEcsEncoder encoder = new LambdaEcsEncoder(); + setMDC(); + + // WHEN + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains("\"faas.id\":\"arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1\",\"faas.name\":\"testFunction\",\"faas.version\":\"1\",\"faas.memory\":\"1024\",\"faas.execution\":\"RequestId\",\"faas.coldstart\":\"false\""); + + // WHEN (includeFaasInfo = false) + encoder.setIncludeFaasInfo(false); + encoded = encoder.encode(loggingEvent); + result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (no faas info in logs) + assertThat(result).doesNotContain("faas"); + } + + @Test + void shouldNotLogCloudInfo() { + // GIVEN + LambdaEcsEncoder encoder = new LambdaEcsEncoder(); + setMDC(); + + // WHEN + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains("\"cloud.provider\":\"aws\",\"cloud.service.name\":\"lambda\",\"cloud.region\":\"eu-west-1\",\"cloud.account.id\":\"012345678910\""); + + // WHEN (includeCloudInfo = false) + encoder.setIncludeCloudInfo(false); + encoded = encoder.encode(loggingEvent); + result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (no faas info in logs) + assertThat(result).doesNotContain("cloud"); + } + + @Test + void shouldLogException() { + // GIVEN + LambdaEcsEncoder encoder = new LambdaEcsEncoder(); + encoder.start(); + LoggingEvent errorloggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "Error", new IllegalStateException("Unexpected value"), null); + + // WHEN + byte[] encoded = encoder.encode(errorloggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains("\"message\":\"Error\",\"error.message\":\"Unexpected value\",\"error.type\":\"java.lang.IllegalStateException\",\"error.stack_trace\":\"[software.amazon.lambda.powertools.logging.internal.LambdaEcsEncoderTest.shouldLogException"); + + // WHEN (configure a custom throwableConverter) + encoder = new LambdaEcsEncoder(); + RootCauseFirstThrowableProxyConverter throwableConverter = new RootCauseFirstThrowableProxyConverter(); + encoder.setThrowableConverter(throwableConverter); + encoder.start(); + encoded = encoder.encode(errorloggingEvent); + result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (stack is logged with root cause first) + assertThat(result).contains("\"message\":\"Error\",\"error.message\":\"Unexpected value\",\"error.type\":\"java.lang.IllegalStateException\",\"error.stack_trace\":\"java.lang.IllegalStateException: Unexpected value\n"); + } + + private void setMDC() { + MDC.put(PowertoolsLoggedFields.FUNCTION_NAME.getName(), context.getFunctionName()); + MDC.put(PowertoolsLoggedFields.FUNCTION_ARN.getName(), context.getInvokedFunctionArn()); + MDC.put(PowertoolsLoggedFields.FUNCTION_VERSION.getName(), context.getFunctionVersion()); + MDC.put(PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE.getName(), + String.valueOf(context.getMemoryLimitInMB())); + MDC.put(PowertoolsLoggedFields.FUNCTION_REQUEST_ID.getName(), context.getAwsRequestId()); + MDC.put(PowertoolsLoggedFields.FUNCTION_COLD_START.getName(), "false"); + MDC.put(PowertoolsLoggedFields.SAMPLING_RATE.getName(), "0.2"); + MDC.put(PowertoolsLoggedFields.SERVICE.getName(), "Service"); + } + + private void setupContext() { + when(context.getFunctionName()).thenReturn("testFunction"); + when(context.getInvokedFunctionArn()).thenReturn( + "arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1"); + when(context.getFunctionVersion()).thenReturn("1"); + when(context.getMemoryLimitInMB()).thenReturn(1024); + when(context.getAwsRequestId()).thenReturn("RequestId"); + } +} diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java new file mode 100644 index 000000000..4a7067540 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java @@ -0,0 +1,433 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.joining; +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter; +import ch.qos.logback.classic.spi.LoggingEvent; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.util.JSONPObject; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.TimeZone; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.logging.argument.StructuredArgument; +import software.amazon.lambda.powertools.logging.argument.StructuredArguments; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsArguments; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEvent; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEventDisabled; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEventForStream; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogResponse; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogResponseForStream; +import software.amazon.lambda.powertools.logging.logback.LambdaJsonEncoder; + +@Order(2) +class LambdaJsonEncoderTest { + private static final Logger logger = (Logger) LoggerFactory.getLogger(LambdaJsonEncoderTest.class.getName()); + + @Mock + private Context context; + + @BeforeEach + void setUp() throws IllegalAccessException, IOException { + openMocks(this); + MDC.clear(); + writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); + setupContext(); + // Make sure file is cleaned up before running tests + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // file may not exist on the first launch + } + } + + @AfterEach + void cleanUp() throws IOException{ + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } + + @Test + void shouldLogInJsonFormat() { + // GIVEN + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); + + // WHEN + handler.handleRequest("Input", context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains( + "{\"level\":\"DEBUG\",\"message\":\"Test debug event\",\"cold_start\":true,\"function_arn\":\"arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1\",\"function_memory_size\":1024,\"function_name\":\"testFunction\",\"function_request_id\":\"RequestId\",\"function_version\":1,\"service\":\"testLogback\",\"xray_trace_id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\",\"timestamp\":"); + } + + @Test + void shouldLogArgumentsAsJsonWhenUsingRawJson() { + // GIVEN + PowertoolsArguments requestHandler = new PowertoolsArguments(PowertoolsArguments.ArgumentFormat.JSON); + SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); + msg.setMessageId("1212abcd"); + msg.setBody("plop"); + msg.setEventSource("eb"); + msg.setAwsRegion("eu-west-1"); + SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); + attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); + msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); + + // WHEN + requestHandler.handleRequest(msg, context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"input\":{\"awsRegion\":\"eu-west-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}") + .contains("\"message\":\"1212abcd\"") + // Should auto-escape double quotes around id + .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\""); + // Reserved keys should be ignored + PowertoolsLoggedFields.stringValues().stream().forEach(reservedKey -> { + assertThat(contentOf(logFile)).doesNotContain("\"" + reservedKey + "\":\"shouldBeIgnored\""); + assertThat(contentOf(logFile)).contains( + "\"message\":\"Attempted to use reserved key '" + reservedKey + + "' in structured argument. This key will be ignored.\""); + }); + } + + @Test + void shouldLogArgumentsAsJsonWhenUsingKeyValue() { + // GIVEN + PowertoolsArguments requestHandler = new PowertoolsArguments(PowertoolsArguments.ArgumentFormat.ENTRY); + SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); + msg.setMessageId("1212abcd"); + msg.setBody("plop"); + msg.setEventSource("eb"); + msg.setAwsRegion("eu-west-1"); + SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); + attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); + msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); + + // WHEN + requestHandler.handleRequest(msg, context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"input\":{\"awsRegion\":\"eu-west-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}") + .contains("\"message\":\"1212abcd\"") + // Should auto-escape double quotes around id + .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\""); + // Reserved keys should be ignored + PowertoolsLoggedFields.stringValues().stream().forEach(reservedKey -> { + assertThat(contentOf(logFile)).doesNotContain("\"" + reservedKey + "\":\"shouldBeIgnored\""); + assertThat(contentOf(logFile)).contains( + "\"message\":\"Attempted to use reserved key '" + reservedKey + + "' in structured argument. This key will be ignored.\""); + }); + } + + private final LoggingEvent loggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "message", null, null); + + @Test + void shouldNotLogPowertoolsInfo() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + + MDC.put(PowertoolsLoggedFields.FUNCTION_NAME.getName(), context.getFunctionName()); + MDC.put(PowertoolsLoggedFields.FUNCTION_ARN.getName(), context.getInvokedFunctionArn()); + MDC.put(PowertoolsLoggedFields.FUNCTION_VERSION.getName(), context.getFunctionVersion()); + MDC.put(PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE.getName(), + String.valueOf(context.getMemoryLimitInMB())); + MDC.put(PowertoolsLoggedFields.FUNCTION_REQUEST_ID.getName(), context.getAwsRequestId()); + MDC.put(PowertoolsLoggedFields.FUNCTION_COLD_START.getName(), "false"); + MDC.put(PowertoolsLoggedFields.SAMPLING_RATE.getName(), "0.2"); + MDC.put(PowertoolsLoggedFields.SERVICE.getName(), "Service"); + + // WHEN + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains("{\"level\":\"INFO\",\"message\":\"message\",\"cold_start\":false,\"function_arn\":\"arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1\",\"function_memory_size\":1024,\"function_name\":\"testFunction\",\"function_request_id\":\"RequestId\",\"function_version\":1,\"sampling_rate\":0.2,\"service\":\"Service\",\"timestamp\":"); + + // WHEN (powertoolsInfo = false) + encoder.setIncludePowertoolsInfo(false); + encoded = encoder.encode(loggingEvent); + result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (no powertools info in logs) + assertThat(result).doesNotContain("cold_start", "function_arn", "function_memory_size", "function_name", "function_request_id", "function_version", "sampling_rate", "service"); + } + + @Test + void shouldLogStructuredArgumentsAsNewEntries() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + + SQSEvent.SQSMessage msg = new SQSEvent.SQSMessage(); + msg.setMessageId("1212abcd"); + msg.setBody("plop"); + msg.setEventSource("eb"); + msg.setAwsRegion("eu-west-1"); + SQSEvent.MessageAttribute attribute = new SQSEvent.MessageAttribute(); + attribute.setStringListValues(Arrays.asList("val1", "val2", "val3")); + msg.setMessageAttributes(Collections.singletonMap("keyAttribute", attribute)); + StructuredArgument argument = StructuredArguments.entry("msg", msg); + + // WHEN + LoggingEvent loggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "A message", null, new Object[]{argument}); + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (logged as JSON) + assertThat(result) + .contains("\"message\":\"A message\",\"msg\":{\"awsRegion\":\"eu-west-1\",\"body\":\"plop\",\"eventSource\":\"eb\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}},\"messageId\":\"1212abcd\"}"); + } + + @Test + void shouldLogEventForHandlerWithLogEventAnnotation() { + // GIVEN + PowertoolsLogEvent requestHandler = new PowertoolsLogEvent(); + + // WHEN + requestHandler.handleRequest(singletonList("ListOfOneElement"), context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("\"event\":[\"ListOfOneElement\"]"); + } + + @Test + void shouldLogEventForHandlerWhenEnvVariableSetToTrue() { + try { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_EVENT = true; + + PowertoolsLogEnabled requestHandler = new PowertoolsLogEnabled(); + + SQSEvent.SQSMessage message = new SQSEvent.SQSMessage(); + message.setBody("body"); + message.setMessageId("1234abcd"); + message.setAwsRegion("eu-west-1"); + + // WHEN + requestHandler.handleRequest(message, context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Event\"") + .contains("\"event\":{\"awsRegion\":\"eu-west-1\",\"body\":\"body\",\"messageId\":\"1234abcd\"}"); + } finally { + LoggingConstants.POWERTOOLS_LOG_EVENT = false; + } + } + + @Test + void shouldNotLogEventForHandlerWhenEnvVariableSetToFalse() throws IOException { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_EVENT = false; + + // WHEN + PowertoolsLogEventDisabled requestHandler = new PowertoolsLogEventDisabled(); + requestHandler.handleRequest(singletonList("ListOfOneElement"), context); + + // THEN + Assertions.assertEquals(0, + Files.lines(Paths.get("target/logfile.json")).collect(joining()).length()); + } + + @Test + void shouldLogEventAsStringForStreamHandler() throws IOException { + // GIVEN + PowertoolsLogEventForStream requestStreamHandler = new PowertoolsLogEventForStream(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + // WHEN + requestStreamHandler.handleRequest(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(Collections.singletonMap("key", "value"))), output, context); + + // THEN + assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) + .isNotEmpty(); + + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Event\"") + // logged as String for StreamHandler (should auto-escape double-quotes to avoid breaking JSON format) + .contains("\"event\":\"{\\\"key\\\":\\\"value\\\"}\""); + } + + @Test + void shouldLogResponseForHandlerWithLogResponseAnnotation() { + // GIVEN + PowertoolsLogResponse requestHandler = new PowertoolsLogResponse(); + + // WHEN + requestHandler.handleRequest("input", context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Response\"") + .contains("\"response\":\"Hola mundo\""); + } + + @Test + void shouldLogResponseForHandlerWhenEnvVariableSetToTrue() { + try { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_RESPONSE = true; + + PowertoolsLogEnabled requestHandler = new PowertoolsLogEnabled(); + + // WHEN + requestHandler.handleRequest("input", context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Response\"") + .contains("\"response\":\"Bonjour le monde\""); + } finally { + LoggingConstants.POWERTOOLS_LOG_RESPONSE = false; + } + } + + @Test + void shouldLogResponseForStreamHandler() throws IOException { + // GIVEN + PowertoolsLogResponseForStream requestStreamHandler = new PowertoolsLogResponseForStream(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + String input = "BobThe Sponge"; + + // WHEN + requestStreamHandler.handleRequest(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)), output, context); + + // THEN + assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) + .isEqualTo(input); + + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)) + .contains("\"message\":\"Handler Response\"") + .contains("\"response\":\""+input+"\""); + } + + @Test + void shouldLogThreadInfo() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + encoder.setIncludeThreadInfo(true); + + // WHEN + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains("\"thread\":\"main\",\"thread_id\":1,\"thread_priority\":5"); + } + + @Test + void shouldLogTimestampDifferently() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + String pattern = "yyyy-MM-dd_HH"; + String timeZone = "Europe/Paris"; + encoder.setTimestampFormat(pattern); + encoder.setTimestampFormatTimezoneId(timeZone); + + // WHEN + Date date = new Date(); + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern); + simpleDateFormat.setTimeZone(TimeZone.getTimeZone(timeZone)); + assertThat(result).contains("\"timestamp\":\""+simpleDateFormat.format(date)+"\""); + } + + @Test + void shouldLogException() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + encoder.start(); + LoggingEvent errorloggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "Error", new IllegalStateException("Unexpected value"), null); + + // WHEN + byte[] encoded = encoder.encode(errorloggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains("\"message\":\"Error\",\"error\":{") + .contains("\"message\":\"Unexpected value\"") + .contains("\"name\":\"java.lang.IllegalStateException\"") + .contains("\"stack\":\"[software.amazon.lambda.powertools.logging.internal.LambdaJsonEncoderTest.shouldLogException"); + + // WHEN (configure a custom throwableConverter) + encoder = new LambdaJsonEncoder(); + RootCauseFirstThrowableProxyConverter throwableConverter = new RootCauseFirstThrowableProxyConverter(); + encoder.setThrowableConverter(throwableConverter); + encoder.start(); + encoded = encoder.encode(errorloggingEvent); + result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (stack is logged with root cause first) + assertThat(result).contains("\"message\":\"Unexpected value\"") + .contains("\"name\":\"java.lang.IllegalStateException\"") + .contains("\"stack\":\"java.lang.IllegalStateException: Unexpected value\n"); + } + + private void setupContext() { + when(context.getFunctionName()).thenReturn("testFunction"); + when(context.getInvokedFunctionArn()).thenReturn( + "arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1"); + when(context.getFunctionVersion()).thenReturn("1"); + when(context.getMemoryLimitInMB()).thenReturn(1024); + when(context.getAwsRequestId()).thenReturn("RequestId"); + } +} diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java new file mode 100644 index 000000000..1fc235ff7 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsArguments.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal.handler; + +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.CORRELATION_ID; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.fasterxml.jackson.core.JsonProcessingException; + +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.logging.argument.StructuredArguments; +import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; +import software.amazon.lambda.powertools.utilities.JsonConfig; + +public class PowertoolsArguments implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsArguments.class); + private final ArgumentFormat argumentFormat; + + public PowertoolsArguments(ArgumentFormat argumentFormat) { + this.argumentFormat = argumentFormat; + } + + @Override + @Logging(clearState = true) + public String handleRequest(SQSEvent.SQSMessage input, Context context) { + try { + MDC.put(CORRELATION_ID.getName(), input.getMessageId()); + if (argumentFormat == ArgumentFormat.JSON) { + LOG.debug("SQS Event", + StructuredArguments.json("input", + JsonConfig.get().getObjectMapper().writeValueAsString(input)), + // function_name is a reserved key by PowertoolsLoggedFields and should be omitted + StructuredArguments.entry("function_name", "shouldBeIgnored")); + } else { + LOG.debug("SQS Event", + StructuredArguments.entry("input", input), + // function_name is a reserved key by PowertoolsLoggedFields and should be omitted + StructuredArguments.entry("function_name", "shouldBeIgnored")); + } + + // Attempt logging all reserved keys, the values should not be overwritten by "shouldBeIgnored" + final Map reservedKeysMap = new HashMap<>(); + for (String field : PowertoolsLoggedFields.stringValues()) { + reservedKeysMap.put(field, "shouldBeIgnored"); + } + reservedKeysMap.put("message", "shouldBeIgnored"); + reservedKeysMap.put("level", "shouldBeIgnored"); + reservedKeysMap.put("timestamp", "shouldBeIgnored"); + LOG.debug("Reserved keys", StructuredArguments.entries(reservedKeysMap)); + LOG.debug("{}", input.getMessageId()); + LOG.warn("Message body = {} and id = \"{}\"", input.getBody(), input.getMessageId()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return input.getMessageId(); + } + + public enum ArgumentFormat { + JSON, ENTRY + } +} diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java new file mode 100644 index 000000000..e8c0c5851 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal.handler; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogEnabled implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogEnabled.class); + + @Override + @Logging(clearState = true) + public Object handleRequest(Object input, Context context) { + MDC.put("myKey", "myValue"); + LOG.debug("Test debug event"); + return "Bonjour le monde"; + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabled.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEvent.java similarity index 86% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabled.java rename to powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEvent.java index 8a960fa87..14a7874cb 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabled.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEvent.java @@ -12,16 +12,16 @@ * */ -package software.amazon.lambda.powertools.logging.handlers; +package software.amazon.lambda.powertools.logging.internal.handler; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.lambda.powertools.logging.Logging; -public class PowerToolLogEventEnabled implements RequestHandler { +public class PowertoolsLogEvent implements RequestHandler { - @Logging(logEvent = true) @Override + @Logging(logEvent = true) public Object handleRequest(Object input, Context context) { return null; } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventDisabled.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEventDisabled.java similarity index 85% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventDisabled.java rename to powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEventDisabled.java index 77103e450..8171bee3e 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventDisabled.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEventDisabled.java @@ -12,16 +12,16 @@ * */ -package software.amazon.lambda.powertools.logging.handlers; +package software.amazon.lambda.powertools.logging.internal.handler; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.lambda.powertools.logging.Logging; -public class PowerToolLogEventDisabled implements RequestHandler { +public class PowertoolsLogEventDisabled implements RequestHandler { - @Logging(logEvent = false) @Override + @Logging(logEvent = false) public Object handleRequest(Object input, Context context) { return null; } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledForStream.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEventForStream.java similarity index 75% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledForStream.java rename to powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEventForStream.java index 9de76586f..443051204 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledForStream.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEventForStream.java @@ -12,7 +12,7 @@ * */ -package software.amazon.lambda.powertools.logging.handlers; +package software.amazon.lambda.powertools.logging.internal.handler; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; @@ -23,12 +23,12 @@ import java.util.Map; import software.amazon.lambda.powertools.logging.Logging; -public class PowerToolLogEventEnabledForStream implements RequestStreamHandler { +public class PowertoolsLogEventForStream implements RequestStreamHandler { - @Logging(logEvent = true) @Override - public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { + @Logging(logEvent = true) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { ObjectMapper mapper = new ObjectMapper(); - mapper.writeValue(output, mapper.readValue(input, Map.class)); + mapper.writeValue(outputStream, mapper.readValue(inputStream, Map.class)); } } diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithNoMetaDataDeprecated.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogResponse.java similarity index 73% rename from powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithNoMetaDataDeprecated.java rename to powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogResponse.java index c13f28df0..5cbeef487 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/handlers/PowerTracerToolEnabledWithNoMetaDataDeprecated.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogResponse.java @@ -12,17 +12,17 @@ * */ -package software.amazon.lambda.powertools.tracing.handlers; +package software.amazon.lambda.powertools.logging.internal.handler; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.lambda.powertools.tracing.Tracing; +import software.amazon.lambda.powertools.logging.Logging; -public class PowerTracerToolEnabledWithNoMetaDataDeprecated implements RequestHandler { +public class PowertoolsLogResponse implements RequestHandler { @Override - @Tracing(captureResponse = false, captureError = false) + @Logging(logResponse = true) public Object handleRequest(Object input, Context context) { - return null; + return "Hola mundo"; } } diff --git a/examples/powertools-examples-core/serverless/src/main/java/helloworld/AppStream.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogResponseForStream.java similarity index 60% rename from examples/powertools-examples-core/serverless/src/main/java/helloworld/AppStream.java rename to powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogResponseForStream.java index 401ef8c48..3378b9421 100644 --- a/examples/powertools-examples-core/serverless/src/main/java/helloworld/AppStream.java +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogResponseForStream.java @@ -12,27 +12,24 @@ * */ -package helloworld; +package software.amazon.lambda.powertools.logging.internal.handler; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Map; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; -public class AppStream implements RequestStreamHandler { - private static final ObjectMapper mapper = new ObjectMapper(); +public class PowertoolsLogResponseForStream implements RequestStreamHandler { @Override - @Logging(logEvent = true) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) - public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { - Map map = mapper.readValue(input, Map.class); - - System.out.println(map.size()); + @Logging(logResponse = true) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + byte[] buf = new byte[1024]; + int length; + while ((length = inputStream.read(buf)) != -1) { + outputStream.write(buf, 0, length); + } } } diff --git a/powertools-logging/powertools-logging-logback/src/test/resources/junit-platform.properties b/powertools-logging/powertools-logging-logback/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..80a2481d7 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/resources/junit-platform.properties @@ -0,0 +1,17 @@ +# +# Copyright 2023 Amazon.com, Inc. or its affiliates. +# 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 +# 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. +# +# + +# because of LambdaLoggingAspect static initialization of the LoggingManager, we need to +# set an order in the unit tests, especially LambdaLoggingAspectTest needs to be first +junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$OrderAnnotation \ No newline at end of file diff --git a/powertools-logging/powertools-logging-logback/src/test/resources/logback-test.xml b/powertools-logging/powertools-logging-logback/src/test/resources/logback-test.xml new file mode 100644 index 000000000..e118a03de --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/resources/logback-test.xml @@ -0,0 +1,27 @@ + + + + + + + + target/logfile.json + + + + + + target/ecslogfile.json + + + + + + + + + + + + + \ No newline at end of file diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPathConstants.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPaths.java similarity index 69% rename from powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPathConstants.java rename to powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPaths.java index ce43c9aa0..6fb38502f 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPathConstants.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPaths.java @@ -17,21 +17,25 @@ /** * Supported Event types from which Correlation ID can be extracted */ -public class CorrelationIdPathConstants { +public class CorrelationIdPaths { /** * To use when function is expecting API Gateway Rest API Request event */ - public static final String API_GATEWAY_REST = "/requestContext/requestId"; + public static final String API_GATEWAY_REST = "requestContext.requestId"; /** * To use when function is expecting API Gateway HTTP API Request event */ - public static final String API_GATEWAY_HTTP = "/requestContext/requestId"; + public static final String API_GATEWAY_HTTP = "requestContext.requestId"; /** * To use when function is expecting Application Load balancer Request event */ - public static final String APPLICATION_LOAD_BALANCER = "/headers/x-amzn-trace-id"; + public static final String APPLICATION_LOAD_BALANCER = "headers.\"x-amzn-trace-id\""; /** * To use when function is expecting Event Bridge Request event */ - public static final String EVENT_BRIDGE = "/id"; + public static final String EVENT_BRIDGE = "id"; + /** + * To use when function is expecting an AppSync request + */ + public static final String APPSYNC_RESOLVER = "request.headers.\"x-amzn-trace-id\""; } diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java index 9932eb700..9e5e735d1 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/Logging.java @@ -34,18 +34,19 @@ * {@code com.amazonaws.services.lambda.runtime.Context}

* *
    - *
  • FunctionName
  • - *
  • FunctionVersion
  • - *
  • InvokedFunctionArn
  • - *
  • MemoryLimitInMB
  • + *
  • function_name
  • + *
  • function_version
  • + *
  • function_arn
  • + *
  • function_memory_size
  • + *
  • function_request_id
  • *
* *

By default {@code Logging} will also create keys for:

* *
    - *
  • coldStart - True if this is the first invocation of this Lambda execution environment; else False
  • + *
  • cold_start - True if this is the first invocation of this Lambda execution environment; else False
  • *
  • service - The value of the 'POWER_TOOLS_SERVICE_NAME' environment variable or 'service_undefined'
  • - *
  • samplingRate - The value of the 'POWERTOOLS_LOGGER_SAMPLE_RATE' environment variable or value of samplingRate field or 0. + *
  • sampling_rate - The value of the 'POWERTOOLS_LOGGER_SAMPLE_RATE' environment variable or value of sampling_rate field or 0. * Valid value is from 0.0 to 1.0. Value outside this range is silently ignored.
  • *
* @@ -56,17 +57,36 @@ *

By default {@code Logging} will not log the event which has trigger the invoke of the Lambda function. * This can be enabled using {@code @Logging(logEvent = true)}.

* - *

By default {@code Logging} all debug logs will follow log4j2 configuration unless configured via - * POWERTOOLS_LOGGER_SAMPLE_RATE environment variable {@code @Logging(samplingRate = <0.0-1.0>)}.

- * - *

To append additional keys to each log entry you can use {@link LoggingUtils#appendKey(String, String)}

+ *

To append additional keys to each log entry you can either use {@link org.slf4j.MDC#put(String, String)} + * or {@link software.amazon.lambda.powertools.logging.argument.StructuredArguments}

*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Logging { + /** + * Set to true if you want to log the event received by the Lambda function handler.
+ * Can also be configured with the 'POWERTOOLS_LOGGER_LOG_EVENT' environment variable + */ boolean logEvent() default false; + /** + * Set to true if you want to log the response sent by the Lambda function handler.
+ * Can also be configured with the 'POWERTOOLS_LOGGER_LOG_RESPONE' environment variable + */ + boolean logResponse() default false; + + /** + * Set to true if you want to log the exception thrown by the Lambda function handler. + * It is already logged by AWS Lambda but with no context information. Setting this to true + * will log the exception and all the powertools additional fields, for more context.
+ * Can also be configured with the 'POWERTOOLS_LOGGER_LOG_ERROR' environment variable + */ + boolean logError() default false; + + /** + * Sampling rate to change log level to DEBUG. (values must be >=0.0, <=1.0) + */ double samplingRate() default 0; /** diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/LoggingUtils.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/LoggingUtils.java deleted file mode 100644 index 6e11573cc..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/LoggingUtils.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.logging; - -import static java.util.Arrays.asList; - -import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.Map; -import org.apache.logging.log4j.ThreadContext; - -/** - * A class of helper functions to add additional functionality to Logging. - *

- * {@see Logging} - */ -public final class LoggingUtils { - private static ObjectMapper objectMapper; - - private LoggingUtils() { - } - - /** - * Appends an additional key and value to each log entry made. Duplicate values - * for the same key will be replaced with the latest. - * - * @param key The name of the key to be logged - * @param value The value to be logged - */ - public static void appendKey(String key, String value) { - ThreadContext.put(key, value); - } - - - /** - * Appends additional key and value to each log entry made. Duplicate values - * for the same key will be replaced with the latest. - * - * @param customKeys Map of custom keys values to be appended to logs - */ - public static void appendKeys(Map customKeys) { - ThreadContext.putAll(customKeys); - } - - /** - * Remove an additional key from log entry. - * - * @param customKey The name of the key to be logged - */ - public static void removeKey(String customKey) { - ThreadContext.remove(customKey); - } - - - /** - * Removes additional keys from log entry. - * - * @param keys Map of custom keys values to be appended to logs - */ - public static void removeKeys(String... keys) { - ThreadContext.removeAll(asList(keys)); - } - - /** - * Sets correlation id attribute on the logs. - * - * @param value The value of the correlation id - */ - public static void setCorrelationId(String value) { - ThreadContext.put("correlation_id", value); - } - - /** - * Sets the instance of ObjectMapper object which is used for serialising event when - * {@code @Logging(logEvent = true)}. - * - * @param objectMapper Custom implementation of object mapper to be used for logging serialised event - */ - public static void defaultObjectMapper(ObjectMapper objectMapper) { - LoggingUtils.objectMapper = objectMapper; - } - - public static ObjectMapper objectMapper() { - if (null == objectMapper) { - objectMapper = new ObjectMapper(); - } - - return objectMapper; - } -} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/ArrayArgument.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/ArrayArgument.java new file mode 100644 index 000000000..cbedbdc0f --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/ArrayArgument.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import java.util.Objects; + +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; + +/** + * See {@link StructuredArguments#array(String, Object...)} + */ +class ArrayArgument implements StructuredArgument { + private final String key; + private final Object[] values; + + public ArrayArgument(String key, Object[] values) { + this.key = Objects.requireNonNull(key, "Key must not be null"); + this.values = new Object[values.length]; + System.arraycopy(values, 0, this.values, 0, values.length); + } + + @Override + public void writeTo(JsonSerializer serializer) { + serializer.writeFieldName(key); + serializer.writeObject(values); + } + + @Override + public String toString() { + return key + "=" + StructuredArguments.toString(values); + } + +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/JsonArgument.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/JsonArgument.java new file mode 100644 index 000000000..debea18c5 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/JsonArgument.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import java.util.Objects; + +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; + +/** + * See {@link StructuredArguments#json(String, String)} + */ +class JsonArgument implements StructuredArgument { + private final String key; + private final String rawJson; + + public JsonArgument(String key, String rawJson) { + this.key = Objects.requireNonNull(key, "key must not be null"); + this.rawJson = Objects.requireNonNull(rawJson, "rawJson must not be null"); + } + + @Override + public void writeTo(JsonSerializer serializer) { + serializer.writeFieldName(key); + serializer.writeRaw(rawJson); + } + + @Override + public String toString() { + return key + "=" + rawJson; + } + +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/KeyValueArgument.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/KeyValueArgument.java new file mode 100644 index 000000000..54648d99e --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/KeyValueArgument.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import java.util.Objects; + +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; + +/** + * See {@link StructuredArguments#entry(String, Object)} + */ +class KeyValueArgument implements StructuredArgument { + private final String key; + private final Object value; + + public KeyValueArgument(String key, Object value) { + this.key = Objects.requireNonNull(key, "Key must not be null"); + this.value = value; + } + + @Override + public void writeTo(JsonSerializer serializer) { + serializer.writeObjectField(key, value); + } + + @Override + public String toString() { + return key + "=" + StructuredArguments.toString(value); + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/MapArgument.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/MapArgument.java new file mode 100644 index 000000000..1155fa93b --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/MapArgument.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; + +/** + * See {@link StructuredArguments#entries(Map)} + */ +class MapArgument implements StructuredArgument { + private final Map map; + + public MapArgument(Map map) { + if (map != null) { + this.map = new HashMap<>(map); + } else { + this.map = null; + } + } + + @Override + public void writeTo(JsonSerializer serializer) { + if (map != null) { + for (Iterator> entries = map.entrySet().iterator(); entries.hasNext();) { + Map.Entry entry = entries.next(); + serializer.writeObjectField(String.valueOf(entry.getKey()), entry.getValue()); + // If the map has more than one entry, we need to print a (comma) separator to avoid breaking the JSON + if (entries.hasNext()) { + serializer.writeSeparator(); + } + } + } + } + + @Override + public String toString() { + return String.valueOf(map); + } + +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/StructuredArgument.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/StructuredArgument.java new file mode 100644 index 000000000..00cc651eb --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/StructuredArgument.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import java.io.IOException; + +import org.slf4j.Logger; + +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; + +/** + * A wrapper for an argument passed to a log method (e.g. {@link Logger#info(String, Object...)}) + * that adds data to the JSON event. + */ +public interface StructuredArgument { + /** + * Writes the data associated with this argument to the given {@link JsonSerializer}. + * + * @param serializer + * the {@link JsonSerializer} to produce JSON content + * @throws IOException + * if an I/O error occurs + */ + void writeTo(JsonSerializer serializer) throws IOException; + + /** + * Writes the data associated with this argument to a {@link String} to be + * included in a log event's formatted message (via parameter substitution). + *

+ * Note that this will only be included in the log event's formatted + * message if the message format includes a parameter for this argument (using {}). + * + * @return String representation of the data associated with this argument + */ + String toString(); + +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/StructuredArguments.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/StructuredArguments.java new file mode 100644 index 000000000..f56a42ea3 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/argument/StructuredArguments.java @@ -0,0 +1,193 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; + +/** + * Factory for creating {@link StructuredArgument}s. + * Inspired from the StructuredArgument of logstash-logback-encoder. + */ +public class StructuredArguments { + + private static final Logger LOGGER = LoggerFactory.getLogger(StructuredArguments.class); + + /** + * Set of reserved keys that should not be used in structured arguments. + * When a reserved key is used, the method will return null. + */ + private static final Set RESERVED_KEYS = Stream + .concat(PowertoolsLoggedFields.stringValues().stream(), + List.of("message", "level", "timestamp", "error").stream()) + .collect(Collectors.toSet()); + + private StructuredArguments() { + // nothing to do, use static methods only + } + + /** + * Checks if the provided key is a reserved key. + * If the key is reserved, logs a warning message. + * + * @param key the key to check + * @return true if the key is reserved, false otherwise + */ + private static boolean isReservedKey(String key) { + if (key != null && RESERVED_KEYS.contains(key)) { + LOGGER.warn( + "Attempted to use reserved key '{}' in structured argument. This key will be ignored.", key); + return true; + } + return false; + } + + /** + * Adds "key": "value" to the JSON structure and "key=value" to the formatted message. + * Returns null if the key is a reserved key. + * + * @param key the field name + * @param value the value associated with the key (can be any kind of object) + * @return a {@link StructuredArgument} populated with the data, or null if key is reserved + */ + public static StructuredArgument entry(String key, Object value) { + if (isReservedKey(key)) { + return null; + } + return new KeyValueArgument(key, value); + } + + /** + * Adds a "key": "value" to the JSON structure for each entry in the map + * and {@code map.toString()} to the formatted message. + * If the map contains any reserved keys, those entries will be filtered out. + * + * @param map {@link Map} holding the key/value pairs + * @return a {@link MapArgument} populated with the data, with reserved keys filtered out + */ + public static StructuredArgument entries(Map map) { + if (map == null) { + return null; + } + + // Create a new map without reserved keys + Map filteredMap = new java.util.HashMap<>(); + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() != null) { + String keyStr = String.valueOf(entry.getKey()); + if (!isReservedKey(keyStr)) { + filteredMap.put(entry.getKey(), entry.getValue()); + } + } + } + + return new MapArgument(filteredMap); + } + + /** + * Adds a field to the JSON structure with key as the key and where value + * is a JSON array of objects AND a string version of the array to the formatted message: + * {@code "key": [value, value]} + * Returns null if the key is a reserved key. + * + * @param key the field name + * @param values elements of the array associated with the key + * @return an {@link ArrayArgument} populated with the data, or null if key is reserved + */ + public static StructuredArgument array(String key, Object... values) { + if (isReservedKey(key)) { + return null; + } + return new ArrayArgument(key, values); + } + + /** + * Adds the {@code rawJson} to the JSON structure and + * the {@code rawJson} to the formatted message. + * Returns null if the key is a reserved key. + * + * @param key the field name + * @param rawJson the raw JSON String + * @return a {@link JsonArgument} populated with the data, or null if key is reserved + */ + public static StructuredArgument json(String key, String rawJson) { + if (isReservedKey(key)) { + return null; + } + return new JsonArgument(key, rawJson); + } + + /** + * Format the argument into a string. + * + * This method mimics the slf4j behavior: + * array objects are formatted as array using {@link Arrays#toString}, + * non array object using {@link String#valueOf}. + * + *

See org.slf4j.helpers.MessageFormatter#deeplyAppendParameter(StringBuilder, Object, Map)} + * + * @param arg the argument to format + * @return formatted string version of the argument + */ + @SuppressWarnings("java:S106") + public static String toString(Object arg) { + + if (arg == null) { + return "null"; + } + + Class argClass = arg.getClass(); + + try { + if (!argClass.isArray()) { + return String.valueOf(arg); + } else { + if (argClass == byte[].class) { + return Arrays.toString((byte[]) arg); + } else if (argClass == short[].class) { + return Arrays.toString((short[]) arg); + } else if (argClass == int[].class) { + return Arrays.toString((int[]) arg); + } else if (argClass == long[].class) { + return Arrays.toString((long[]) arg); + } else if (argClass == char[].class) { + return Arrays.toString((char[]) arg); + } else if (argClass == float[].class) { + return Arrays.toString((float[]) arg); + } else if (argClass == double[].class) { + return Arrays.toString((double[]) arg); + } else if (argClass == boolean[].class) { + return Arrays.toString((boolean[]) arg); + } else { + return Arrays.deepToString((Object[]) arg); + } + } + + } catch (Exception e) { + System.err.println("Failed toString() invocation on an object of type [" + argClass.getName() + "]"); + return "[FAILED toString()]"; + } + } + +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java deleted file mode 100644 index 17d09729f..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java +++ /dev/null @@ -1,519 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.logging.internal; - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonRootName; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import com.fasterxml.jackson.core.JsonGenerationException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectWriter; -import java.io.IOException; -import java.io.Writer; -import java.nio.charset.Charset; -import java.util.LinkedHashMap; -import java.util.Map; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.Marker; -import org.apache.logging.log4j.ThreadContext; -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; -import org.apache.logging.log4j.core.config.plugins.PluginElement; -import org.apache.logging.log4j.core.impl.Log4jLogEvent; -import org.apache.logging.log4j.core.impl.ThrowableProxy; -import org.apache.logging.log4j.core.jackson.XmlConstants; -import org.apache.logging.log4j.core.layout.AbstractStringLayout; -import org.apache.logging.log4j.core.lookup.StrSubstitutor; -import org.apache.logging.log4j.core.time.Instant; -import org.apache.logging.log4j.core.util.KeyValuePair; -import org.apache.logging.log4j.core.util.StringBuilderWriter; -import org.apache.logging.log4j.message.Message; -import org.apache.logging.log4j.util.ReadOnlyStringMap; -import org.apache.logging.log4j.util.Strings; - -@Deprecated -abstract class AbstractJacksonLayoutCopy extends AbstractStringLayout { - - protected static final String DEFAULT_EOL = "\r\n"; - protected static final String COMPACT_EOL = Strings.EMPTY; - protected final String eol; - protected final ObjectWriter objectWriter; - protected final boolean compact; - protected final boolean complete; - protected final boolean includeNullDelimiter; - protected final ResolvableKeyValuePair[] additionalFields; - - @Deprecated - protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, - final Charset charset, - final boolean compact, final boolean complete, final boolean eventEol, - final Serializer headerSerializer, - final Serializer footerSerializer) { - this(config, objectWriter, charset, compact, complete, eventEol, headerSerializer, footerSerializer, false); - } - - @Deprecated - protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, - final Charset charset, - final boolean compact, final boolean complete, final boolean eventEol, - final Serializer headerSerializer, - final Serializer footerSerializer, final boolean includeNullDelimiter) { - this(config, objectWriter, charset, compact, complete, eventEol, null, headerSerializer, footerSerializer, - includeNullDelimiter, null); - } - - protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, - final Charset charset, - final boolean compact, final boolean complete, final boolean eventEol, - final String endOfLine, final Serializer headerSerializer, - final Serializer footerSerializer, final boolean includeNullDelimiter, - final KeyValuePair[] additionalFields) { - super(config, charset, headerSerializer, footerSerializer); - this.objectWriter = objectWriter; - this.compact = compact; - this.complete = complete; - this.eol = endOfLine != null ? endOfLine : compact && !eventEol ? COMPACT_EOL : DEFAULT_EOL; - this.includeNullDelimiter = includeNullDelimiter; - this.additionalFields = prepareAdditionalFields(config, additionalFields); - } - - protected static boolean valueNeedsLookup(final String value) { - return value != null && value.contains("${"); - } - - private static ResolvableKeyValuePair[] prepareAdditionalFields(final Configuration config, - final KeyValuePair[] additionalFields) { - if (additionalFields == null || additionalFields.length == 0) { - // No fields set - return ResolvableKeyValuePair.EMPTY_ARRAY; - } - - // Convert to specific class which already determines whether values needs lookup during serialization - final ResolvableKeyValuePair[] resolvableFields = new ResolvableKeyValuePair[additionalFields.length]; - - for (int i = 0; i < additionalFields.length; i++) { - final ResolvableKeyValuePair resolvable = - resolvableFields[i] = new ResolvableKeyValuePair(additionalFields[i]); - - // Validate - if (config == null && resolvable.valueNeedsLookup) { - throw new IllegalArgumentException( - "configuration needs to be set when there are additional fields with variables"); - } - } - - return resolvableFields; - } - - private static LogEvent convertMutableToLog4jEvent(final LogEvent event) { - return event instanceof Log4jLogEvent ? event : Log4jLogEvent.createMemento(event); - } - - /** - * Formats a {@link org.apache.logging.log4j.core.LogEvent}. - * - * @param event The LogEvent. - * @return The XML representation of the LogEvent. - */ - @Override - public String toSerializable(final LogEvent event) { - final StringBuilderWriter writer = new StringBuilderWriter(); - try { - toSerializable(event, writer); - return writer.toString(); - } catch (final IOException e) { - // Should this be an ISE or IAE? - LOGGER.error(e); - return Strings.EMPTY; - } - } - - protected Object wrapLogEvent(final LogEvent event) { - if (additionalFields.length > 0) { - // Construct map for serialization - note that we are intentionally using original LogEvent - final Map additionalFieldsMap = resolveAdditionalFields(event); - // This class combines LogEvent with AdditionalFields during serialization - return new LogEventWithAdditionalFields(event, additionalFieldsMap); - } else if (event instanceof Message) { - // If the LogEvent implements the Messagee interface Jackson will not treat is as a LogEvent. - return new ReadOnlyLogEventWrapper(event); - } else { - // No additional fields, return original object - return event; - } - } - - private Map resolveAdditionalFields(final LogEvent logEvent) { - // Note: LinkedHashMap retains order - final Map additionalFieldsMap = new LinkedHashMap<>(additionalFields.length); - final StrSubstitutor strSubstitutor = configuration.getStrSubstitutor(); - - // Go over each field - for (final ResolvableKeyValuePair pair : additionalFields) { - if (pair.valueNeedsLookup) { - // Resolve value - additionalFieldsMap.put(pair.key, strSubstitutor.replace(logEvent, pair.value)); - } else { - // Plain text value - additionalFieldsMap.put(pair.key, pair.value); - } - } - - return additionalFieldsMap; - } - - public void toSerializable(final LogEvent event, final Writer writer) - throws JsonGenerationException, JsonMappingException, IOException { - objectWriter.writeValue(writer, wrapLogEvent(convertMutableToLog4jEvent(event))); - writer.write(eol); - if (includeNullDelimiter) { - writer.write('\0'); - } - markEvent(); - } - - public static abstract class Builder> extends AbstractStringLayout.Builder { - - @PluginBuilderAttribute - private boolean eventEol; - - @PluginBuilderAttribute - private String endOfLine; - - @PluginBuilderAttribute - private boolean compact; - - @PluginBuilderAttribute - private boolean complete; - - @PluginBuilderAttribute - private boolean locationInfo; - - @PluginBuilderAttribute - private boolean properties; - - @PluginBuilderAttribute - private boolean includeStacktrace = true; - - @PluginBuilderAttribute - private boolean stacktraceAsString = false; - - @PluginBuilderAttribute - private boolean includeNullDelimiter = false; - - @PluginBuilderAttribute - private boolean includeTimeMillis = false; - - @PluginElement("AdditionalField") - private KeyValuePair[] additionalFields; - - protected String toStringOrNull(final byte[] header) { - return header == null ? null : new String(header, Charset.defaultCharset()); - } - - public boolean getEventEol() { - return eventEol; - } - - public B setEventEol(final boolean eventEol) { - this.eventEol = eventEol; - return asBuilder(); - } - - public String getEndOfLine() { - return endOfLine; - } - - public B setEndOfLine(final String endOfLine) { - this.endOfLine = endOfLine; - return asBuilder(); - } - - public boolean isCompact() { - return compact; - } - - public B setCompact(final boolean compact) { - this.compact = compact; - return asBuilder(); - } - - public boolean isComplete() { - return complete; - } - - public B setComplete(final boolean complete) { - this.complete = complete; - return asBuilder(); - } - - public boolean isLocationInfo() { - return locationInfo; - } - - public B setLocationInfo(final boolean locationInfo) { - this.locationInfo = locationInfo; - return asBuilder(); - } - - public boolean isProperties() { - return properties; - } - - public B setProperties(final boolean properties) { - this.properties = properties; - return asBuilder(); - } - - /** - * If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true". - * - * @return If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true". - */ - public boolean isIncludeStacktrace() { - return includeStacktrace; - } - - /** - * If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true". - * - * @param includeStacktrace If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true". - * @return this builder - */ - public B setIncludeStacktrace(final boolean includeStacktrace) { - this.includeStacktrace = includeStacktrace; - return asBuilder(); - } - - public boolean isStacktraceAsString() { - return stacktraceAsString; - } - - /** - * Whether to format the stacktrace as a string, and not a nested object (optional, defaults to false). - * - * @return this builder - */ - public B setStacktraceAsString(final boolean stacktraceAsString) { - this.stacktraceAsString = stacktraceAsString; - return asBuilder(); - } - - public boolean isIncludeNullDelimiter() { - return includeNullDelimiter; - } - - /** - * Whether to include NULL byte as delimiter after each event (optional, default to false). - * - * @return this builder - */ - public B setIncludeNullDelimiter(final boolean includeNullDelimiter) { - this.includeNullDelimiter = includeNullDelimiter; - return asBuilder(); - } - - public boolean isIncludeTimeMillis() { - return includeTimeMillis; - } - - /** - * Whether to include the timestamp (in addition to the Instant) (optional, default to false). - * - * @return this builder - */ - public B setIncludeTimeMillis(final boolean includeTimeMillis) { - this.includeTimeMillis = includeTimeMillis; - return asBuilder(); - } - - public KeyValuePair[] getAdditionalFields() { - return additionalFields; - } - - /** - * Additional fields to set on each log event. - * - * @return this builder - */ - public B setAdditionalFields(final KeyValuePair[] additionalFields) { - this.additionalFields = additionalFields; - return asBuilder(); - } - } - - @JsonRootName(XmlConstants.ELT_EVENT) - public static class LogEventWithAdditionalFields { - - private final Object logEvent; - private final Map additionalFields; - - public LogEventWithAdditionalFields(final Object logEvent, final Map additionalFields) { - this.logEvent = logEvent; - this.additionalFields = additionalFields; - } - - @JsonUnwrapped - public Object getLogEvent() { - return logEvent; - } - - @JsonAnyGetter - @SuppressWarnings("unused") - public Map getAdditionalFields() { - return additionalFields; - } - } - - protected static class ResolvableKeyValuePair { - - /** - * The empty array. - */ - static final ResolvableKeyValuePair[] EMPTY_ARRAY = {}; - - final String key; - final String value; - final boolean valueNeedsLookup; - - ResolvableKeyValuePair(final KeyValuePair pair) { - this.key = pair.getKey(); - this.value = pair.getValue(); - this.valueNeedsLookup = AbstractJacksonLayoutCopy.valueNeedsLookup(this.value); - } - } - - private static class ReadOnlyLogEventWrapper implements LogEvent { - - @JsonIgnore - private final LogEvent event; - - public ReadOnlyLogEventWrapper(LogEvent event) { - this.event = event; - } - - @Override - public LogEvent toImmutable() { - return event.toImmutable(); - } - - @Override - public Map getContextMap() { - return event.getContextMap(); - } - - @Override - public ReadOnlyStringMap getContextData() { - return event.getContextData(); - } - - @Override - public ThreadContext.ContextStack getContextStack() { - return event.getContextStack(); - } - - @Override - public String getLoggerFqcn() { - return event.getLoggerFqcn(); - } - - @Override - public Level getLevel() { - return event.getLevel(); - } - - @Override - public String getLoggerName() { - return event.getLoggerName(); - } - - @Override - public Marker getMarker() { - return event.getMarker(); - } - - @Override - public Message getMessage() { - return event.getMessage(); - } - - @Override - public long getTimeMillis() { - return event.getTimeMillis(); - } - - @Override - public Instant getInstant() { - return event.getInstant(); - } - - @Override - public StackTraceElement getSource() { - return event.getSource(); - } - - @Override - public String getThreadName() { - return event.getThreadName(); - } - - @Override - public long getThreadId() { - return event.getThreadId(); - } - - @Override - public int getThreadPriority() { - return event.getThreadPriority(); - } - - @Override - public Throwable getThrown() { - return event.getThrown(); - } - - @Override - public ThrowableProxy getThrownProxy() { - return event.getThrownProxy(); - } - - @Override - public boolean isEndOfBatch() { - return event.isEndOfBatch(); - } - - @Override - public void setEndOfBatch(boolean endOfBatch) { - - } - - @Override - public boolean isIncludeLocation() { - return event.isIncludeLocation(); - } - - @Override - public void setIncludeLocation(boolean locationRequired) { - - } - - @Override - public long getNanoTime() { - return event.getNanoTime(); - } - } -} \ No newline at end of file diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefautlLoggingManager.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefautlLoggingManager.java new file mode 100644 index 000000000..5326f53e6 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefautlLoggingManager.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +import org.slf4j.Logger; +import org.slf4j.event.Level; + +/** + * When no LoggingManager is found, setting a default one with no action on logging implementation + * Powertools cannot change the log level based on the environment variable, will use the logger configuration + */ +public class DefautlLoggingManager implements LoggingManager { + + @Override + public void setLogLevel(Level logLevel) { + // do nothing + } + + @Override + public Level getLogLevel(Logger logger) { + return Level.ERROR; + } +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java deleted file mode 100644 index 6b568be30..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.logging.internal; - -import com.fasterxml.jackson.core.PrettyPrinter; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.fasterxml.jackson.core.util.MinimalPrettyPrinter; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; -import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; -import java.util.HashSet; -import java.util.Set; -import org.apache.logging.log4j.core.impl.Log4jLogEvent; -import org.apache.logging.log4j.core.jackson.JsonConstants; -import org.apache.logging.log4j.core.jackson.Log4jJsonObjectMapper; - -@Deprecated -abstract class JacksonFactoryCopy { - - abstract protected String getPropertyNameForTimeMillis(); - - abstract protected String getPropertyNameForInstant(); - - abstract protected String getPropertNameForContextMap(); - - abstract protected String getPropertNameForSource(); - - abstract protected String getPropertNameForNanoTime(); - - abstract protected PrettyPrinter newCompactPrinter(); - - abstract protected ObjectMapper newObjectMapper(); - - abstract protected PrettyPrinter newPrettyPrinter(); - - ObjectWriter newWriter(final boolean locationInfo, final boolean properties, final boolean compact) { - return newWriter(locationInfo, properties, compact, false); - } - - ObjectWriter newWriter(final boolean locationInfo, final boolean properties, final boolean compact, - final boolean includeMillis) { - final SimpleFilterProvider filters = new SimpleFilterProvider(); - final Set except = new HashSet<>(3); - if (!locationInfo) { - except.add(this.getPropertNameForSource()); - } - if (!properties) { - except.add(this.getPropertNameForContextMap()); - } - if (includeMillis) { - except.add(getPropertyNameForInstant()); - } else { - except.add(getPropertyNameForTimeMillis()); - } - except.add(this.getPropertNameForNanoTime()); - filters.addFilter(Log4jLogEvent.class.getName(), SimpleBeanPropertyFilter.serializeAllExcept(except)); - final ObjectWriter writer = - this.newObjectMapper().writer(compact ? this.newCompactPrinter() : this.newPrettyPrinter()); - return writer.with(filters); - } - - static class JSON extends JacksonFactoryCopy { - - private final boolean encodeThreadContextAsList; - private final boolean includeStacktrace; - private final boolean stacktraceAsString; - private final boolean objectMessageAsJsonObject; - - public JSON(final boolean encodeThreadContextAsList, final boolean includeStacktrace, - final boolean stacktraceAsString, final boolean objectMessageAsJsonObject) { - this.encodeThreadContextAsList = encodeThreadContextAsList; - this.includeStacktrace = includeStacktrace; - this.stacktraceAsString = stacktraceAsString; - this.objectMessageAsJsonObject = objectMessageAsJsonObject; - } - - @Override - protected String getPropertNameForContextMap() { - return JsonConstants.ELT_CONTEXT_MAP; - } - - @Override - protected String getPropertyNameForTimeMillis() { - return JsonConstants.ELT_TIME_MILLIS; - } - - @Override - protected String getPropertyNameForInstant() { - return JsonConstants.ELT_INSTANT; - } - - @Override - protected String getPropertNameForSource() { - return JsonConstants.ELT_SOURCE; - } - - @Override - protected String getPropertNameForNanoTime() { - return JsonConstants.ELT_NANO_TIME; - } - - @Override - protected PrettyPrinter newCompactPrinter() { - return new MinimalPrettyPrinter(); - } - - @Override - protected ObjectMapper newObjectMapper() { - return new Log4jJsonObjectMapper(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, - objectMessageAsJsonObject); - } - - @Override - protected PrettyPrinter newPrettyPrinter() { - return new DefaultPrettyPrinter(); - } - - } - -} \ No newline at end of file diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JsonSerializer.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JsonSerializer.java new file mode 100644 index 000000000..82bc76a38 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JsonSerializer.java @@ -0,0 +1,597 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.MissingNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.NumericNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.POJONode; +import com.fasterxml.jackson.databind.node.TextNode; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import software.amazon.lambda.powertools.utilities.JsonConfig; + +/** + * A simple JSON serializer. + * Used internally for json serialization, not to be used externally. + * We do not use Jackson as we need to serialize each fields of the log event individually. + * Mainly used by logback as log4j is using its own JsonWriter + */ +public class JsonSerializer implements AutoCloseable { + + private final StringBuilder builder; + + public JsonSerializer(StringBuilder builder) { + super(); + if (builder == null) { + throw new IllegalArgumentException("StringBuilder cannot be null"); + } + this.builder = builder; + } + + public void writeStartArray() { + builder.append('['); + } + + public void writeEndArray() { + builder.append(']'); + } + + public void writeStartObject() { + builder.append('{'); + } + + public void writeEndObject() { + builder.append('}'); + } + + public void writeSeparator() { + writeRaw(','); + } + + public void writeFieldName(String name) { + Objects.requireNonNull(name, "field name must not be null"); + writeString(name); + writeRaw(':'); + } + + public void writeString(String text) { + if (text == null) { + writeNull(); + } else { + // Escape double quotes to avoid breaking JSON format + builder.append("\"").append(text.replace("\"", "\\\"")).append("\""); + } + } + + public void writeRaw(String text) { + builder.append(text); + } + + public void writeRaw(char c) { + builder.append(c); + } + + public void writeNumber(short v) { + builder.append(v); + } + + public void writeNumber(int v) { + builder.append(v); + } + + public void writeNumber(long v) { + builder.append(v); + } + + public void writeNumber(BigInteger v) { + builder.append(v); + } + + public void writeNumber(double v) { + builder.append(v); + } + + public void writeNumber(float v) { + builder.append(v); + } + + public void writeNumber(BigDecimal v) { + builder.append(v.toPlainString()); + } + + public void writeBoolean(boolean state) { + builder.append(state); + } + + public void writeArray(final char[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + builder.append('\''); + builder.append(items[itemIndex]); + builder.append('\''); + } + writeEndArray(); + } + } + + public void writeArray(final boolean[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final boolean item = items[itemIndex]; + writeBoolean(item); + } + writeEndArray(); + } + } + + public void writeArray(final byte[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final byte item = items[itemIndex]; + writeNumber(item); + } + writeEndArray(); + } + } + + public void writeArray(final short[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final short item = items[itemIndex]; + writeNumber(item); + } + writeEndArray(); + } + } + + public void writeArray(final int[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final int item = items[itemIndex]; + writeNumber(item); + } + writeEndArray(); + } + } + + public void writeArray(final long[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final long item = items[itemIndex]; + writeNumber(item); + } + writeEndArray(); + } + } + + public void writeArray(final float[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final float item = items[itemIndex]; + writeNumber(item); + } + writeEndArray(); + } + } + + public void writeArray(final double[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final double item = items[itemIndex]; + writeNumber(item); + } + writeEndArray(); + } + } + + public void writeArray(final Object[] items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.length; itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final Object item = items[itemIndex]; + writeObject(item); + } + writeEndArray(); + } + } + + public void writeNull() { + builder.append("null"); + } + + public void writeArray(final List items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) { + if (itemIndex > 0) { + writeSeparator(); + } + final Object item = items.get(itemIndex); + writeObject(item); + } + writeEndArray(); + } + } + + public void writeArray(final Collection items) { + if (items == null) { + writeNull(); + } else { + writeStartArray(); + Iterator iterator = items.iterator(); + while (iterator.hasNext()) { + writeObject(iterator.next()); + if (iterator.hasNext()) { + writeSeparator(); + } + } + writeEndArray(); + } + } + + public void writeMap(final Map map) { + if (map == null) { + writeNull(); + } else { + writeStartObject(); + for (Iterator> entries = map.entrySet().iterator(); entries.hasNext();) { + Map.Entry entry = entries.next(); + writeObjectField(String.valueOf(entry.getKey()), entry.getValue()); + if (entries.hasNext()) { + builder.append(','); + } + } + writeEndObject(); + } + } + + public void writeObject(Object value) { + + // null + if (value == null) { + writeNull(); + } + + else if (value instanceof String) { + writeString((String) value); + } + + // number & boolean + else if (value instanceof Number) { + Number n = (Number) value; + if (n instanceof Integer) { + writeNumber(n.intValue()); + } else if (n instanceof Long) { + writeNumber(n.longValue()); + } else if (n instanceof Double) { + writeNumber(n.doubleValue()); + } else if (n instanceof Float) { + writeNumber(n.floatValue()); + } else if (n instanceof Short) { + writeNumber(n.shortValue()); + } else if (n instanceof Byte) { + writeNumber(n.byteValue()); + } else if (n instanceof BigInteger) { + writeNumber((BigInteger) n); + } else if (n instanceof BigDecimal) { + writeNumber((BigDecimal) n); + } else if (n instanceof AtomicInteger) { + writeNumber(((AtomicInteger) n).get()); + } else if (n instanceof AtomicLong) { + writeNumber(((AtomicLong) n).get()); + } + } else if (value instanceof Boolean) { + writeBoolean((Boolean) value); + } else if (value instanceof AtomicBoolean) { + writeBoolean(((AtomicBoolean) value).get()); + } + + // list & collection + else if (value instanceof List) { + final List list = (List) value; + writeArray(list); + } else if (value instanceof Collection) { + final Collection collection = (Collection) value; + writeArray(collection); + } + + // map + else if (value instanceof Map) { + final Map map = (Map) value; + writeMap(map); + } + + // arrays + else if (value instanceof char[]) { + final char[] charValues = (char[]) value; + writeArray(charValues); + } else if (value instanceof boolean[]) { + final boolean[] booleanValues = (boolean[]) value; + writeArray(booleanValues); + } else if (value instanceof byte[]) { + final byte[] byteValues = (byte[]) value; + writeArray(byteValues); + } else if (value instanceof short[]) { + final short[] shortValues = (short[]) value; + writeArray(shortValues); + } else if (value instanceof int[]) { + final int[] intValues = (int[]) value; + writeArray(intValues); + } else if (value instanceof long[]) { + final long[] longValues = (long[]) value; + writeArray(longValues); + } else if (value instanceof float[]) { + final float[] floatValues = (float[]) value; + writeArray(floatValues); + } else if (value instanceof double[]) { + final double[] doubleValues = (double[]) value; + writeArray(doubleValues); + } else if (value instanceof Object[]) { + final Object[] values = (Object[]) value; + writeArray(values); + } + + else if (value instanceof JsonNode) { + JsonNode node = (JsonNode) value; + + switch (node.getNodeType()) { + case NULL: + case MISSING: + writeNull(); + break; + + case STRING: + writeString(node.asText()); + break; + + case BOOLEAN: + writeBoolean(node.asBoolean()); + break; + + case NUMBER: + if (node.isInt()) { + writeNumber(node.intValue()); + break; + } + if (node.isLong()) { + writeNumber(node.longValue()); + break; + } + if (node.isShort()) { + writeNumber(node.shortValue()); + break; + } + if (node.isDouble()) { + writeNumber(node.doubleValue()); + break; + } + if (node.isFloat()) { + writeNumber(node.floatValue()); + break; + } + if (node.isBigDecimal()) { + writeNumber(node.decimalValue()); + break; + } + if (node.isBigInteger()) { + writeNumber(node.bigIntegerValue()); + break; + } + break; + case OBJECT: + case POJO: + writeStartObject(); + for (Iterator> entries = node.fields(); entries.hasNext();) { + Map.Entry entry = entries.next(); + writeObjectField(entry.getKey(), entry.getValue()); + if (entries.hasNext()) { + builder.append(','); + } + } + writeEndObject(); + return; + + case ARRAY: + ArrayNode arrayNode = (ArrayNode) node; + writeStartArray(); + for (Iterator elements = arrayNode.elements(); elements.hasNext();) { + writeObject(elements.next()); + if (elements.hasNext()) { + builder.append(','); + } + } + writeEndArray(); + return; + + default: + break; + } + } else { + try { + // default: try to write object as JSON + writeRaw(JsonConfig.get().getObjectMapper().writeValueAsString(value)); + } catch (Exception e) { + // last chance: toString + writeString(value.toString()); + } + } + } + + public void writeObjectField(String key, Object value) { + writeFieldName(key); + writeObject(value); + } + + public void writeBooleanField(String key, boolean value) { + writeFieldName(key); + writeBoolean(value); + } + + public void writeNullField(String key) { + writeFieldName(key); + writeNull(); + } + + public void writeNumberField(String key, int value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeNumberField(String key, float value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeNumberField(String key, short value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeNumberField(String key, long value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeNumberField(String key, BigInteger value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeNumberField(String key, double value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeNumberField(String key, BigDecimal value) { + writeFieldName(key); + writeNumber(value); + } + + public void writeStringField(String key, String value) { + writeFieldName(key); + writeString(value); + } + + public void writeTree(TreeNode rootNode) { + if (rootNode == null) { + writeNull(); + } else if (rootNode instanceof TextNode) { + writeString(((TextNode) rootNode).asText()); + } else if (rootNode instanceof BooleanNode) { + writeBoolean(((BooleanNode) rootNode).asBoolean()); + } else if (rootNode instanceof NumericNode) { + NumericNode numericNode = (NumericNode) rootNode; + if (numericNode.isInt()) { + writeNumber(numericNode.intValue()); + } else if (numericNode.isLong()) { + writeNumber(numericNode.longValue()); + } else if (numericNode.isShort()) { + writeNumber(numericNode.shortValue()); + } else if (numericNode.isDouble()) { + writeNumber(numericNode.doubleValue()); + } else if (numericNode.isFloat()) { + writeNumber(numericNode.floatValue()); + } else if (numericNode.isBigDecimal()) { + writeNumber(numericNode.decimalValue()); + } else if (numericNode.isBigInteger()) { + writeNumber(numericNode.bigIntegerValue()); + } + } else if (rootNode instanceof ArrayNode) { + ArrayNode arrayNode = (ArrayNode) rootNode; + writeObject(arrayNode); + } else if (rootNode instanceof ObjectNode) { + ObjectNode objectNode = (ObjectNode) rootNode; + writeObject(objectNode); + } else if (rootNode instanceof NullNode || rootNode instanceof MissingNode) { + writeNull(); + } else if (rootNode instanceof POJONode) { + writeObject(((POJONode) rootNode).getPojo()); + } + } + + @Override + public void close() { + // nothing to do + } +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java deleted file mode 100644 index fd646ab50..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.logging.internal; - -import static java.time.Instant.ofEpochMilli; -import static java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME; - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonRootName; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import java.io.IOException; -import java.io.Writer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import org.apache.logging.log4j.core.Layout; -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.DefaultConfiguration; -import org.apache.logging.log4j.core.config.Node; -import org.apache.logging.log4j.core.config.plugins.Plugin; -import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; -import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; -import org.apache.logging.log4j.core.jackson.XmlConstants; -import org.apache.logging.log4j.core.layout.PatternLayout; -import org.apache.logging.log4j.core.util.KeyValuePair; -import org.apache.logging.log4j.util.Strings; - -/*** - * Note: The LambdaJsonLayout should be considered to be deprecated. Please use JsonTemplateLayout instead. - */ -@Deprecated -@Plugin(name = "LambdaJsonLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) -public final class LambdaJsonLayout extends AbstractJacksonLayoutCopy { - static final String CONTENT_TYPE = "application/json"; - private static final String DEFAULT_FOOTER = "]"; - private static final String DEFAULT_HEADER = "["; - - private LambdaJsonLayout(final Configuration config, final boolean locationInfo, final boolean properties, - final boolean encodeThreadContextAsList, - final boolean complete, final boolean compact, final boolean eventEol, - final String headerPattern, final String footerPattern, final Charset charset, - final boolean includeStacktrace, final boolean stacktraceAsString, - final boolean includeNullDelimiter, - final KeyValuePair[] additionalFields, final boolean objectMessageAsJsonObject) { - super(config, new JacksonFactoryCopy.JSON(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, - objectMessageAsJsonObject).newWriter( - locationInfo, properties, compact), - charset, compact, complete, eventEol, - null, - PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(headerPattern) - .setDefaultPattern(DEFAULT_HEADER).build(), - PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(footerPattern) - .setDefaultPattern(DEFAULT_FOOTER).build(), - includeNullDelimiter, - additionalFields); - } - - @PluginBuilderFactory - public static > B newBuilder() { - return new Builder().asBuilder(); - } - - /** - * Creates a JSON Layout using the default settings. Useful for testing. - * - * @return A JSON Layout. - */ - public static LambdaJsonLayout createDefaultLayout() { - return new LambdaJsonLayout(new DefaultConfiguration(), false, false, false, false, false, false, - DEFAULT_HEADER, DEFAULT_FOOTER, StandardCharsets.UTF_8, true, false, false, null, false); - } - - /** - * Returns appropriate JSON header. - * - * @return a byte array containing the header, opening the JSON array. - */ - @Override - public byte[] getHeader() { - if (!this.complete) { - return null; - } - final StringBuilder buf = new StringBuilder(); - final String str = serializeToString(getHeaderSerializer()); - if (str != null) { - buf.append(str); - } - buf.append(this.eol); - return getBytes(buf.toString()); - } - - /** - * Returns appropriate JSON footer. - * - * @return a byte array containing the footer, closing the JSON array. - */ - @Override - public byte[] getFooter() { - if (!this.complete) { - return null; - } - final StringBuilder buf = new StringBuilder(); - buf.append(this.eol); - final String str = serializeToString(getFooterSerializer()); - if (str != null) { - buf.append(str); - } - buf.append(this.eol); - return getBytes(buf.toString()); - } - - @Override - public Map getContentFormat() { - final Map result = new HashMap<>(); - result.put("version", "2.0"); - return result; - } - - /** - * @return The content type. - */ - @Override - public String getContentType() { - return CONTENT_TYPE + "; charset=" + this.getCharset(); - } - - @Override - public Object wrapLogEvent(final LogEvent event) { - Map additionalFieldsMap = resolveAdditionalFields(event); - // This class combines LogEvent with AdditionalFields during serialization - return new LogEventWithAdditionalFields(event, additionalFieldsMap); - } - - @Override - public void toSerializable(final LogEvent event, final Writer writer) throws IOException { - if (complete && eventCount > 0) { - writer.append(", "); - } - super.toSerializable(event, writer); - } - - private Map resolveAdditionalFields(LogEvent logEvent) { - // Note: LinkedHashMap retains order - final Map additionalFieldsMap = new LinkedHashMap<>(additionalFields.length); - - // Go over MDC - logEvent.getContextData().forEach((key, value) -> - { - if (Strings.isNotBlank(key) && value != null) { - additionalFieldsMap.put(key, value); - } - }); - - return additionalFieldsMap; - } - - public static class Builder> extends AbstractJacksonLayoutCopy.Builder - implements org.apache.logging.log4j.core.util.Builder { - - @PluginBuilderAttribute - private boolean propertiesAsList; - - @PluginBuilderAttribute - private boolean objectMessageAsJsonObject; - - public Builder() { - super(); - setCharset(StandardCharsets.UTF_8); - } - - @Override - public LambdaJsonLayout build() { - final boolean encodeThreadContextAsList = isProperties() && propertiesAsList; - final String headerPattern = toStringOrNull(getHeader()); - final String footerPattern = toStringOrNull(getFooter()); - return new LambdaJsonLayout(getConfiguration(), isLocationInfo(), isProperties(), encodeThreadContextAsList, - isComplete(), isCompact(), getEventEol(), headerPattern, footerPattern, getCharset(), - isIncludeStacktrace(), isStacktraceAsString(), isIncludeNullDelimiter(), - getAdditionalFields(), getObjectMessageAsJsonObject()); - } - - public boolean isPropertiesAsList() { - return propertiesAsList; - } - - public B setPropertiesAsList(final boolean propertiesAsList) { - this.propertiesAsList = propertiesAsList; - return asBuilder(); - } - - public boolean getObjectMessageAsJsonObject() { - return objectMessageAsJsonObject; - } - - public B setObjectMessageAsJsonObject(final boolean objectMessageAsJsonObject) { - this.objectMessageAsJsonObject = objectMessageAsJsonObject; - return asBuilder(); - } - } - - @JsonRootName(XmlConstants.ELT_EVENT) - public static class LogEventWithAdditionalFields { - - private final LogEvent logEvent; - private final Map additionalFields; - - public LogEventWithAdditionalFields(LogEvent logEvent, Map additionalFields) { - this.logEvent = logEvent; - this.additionalFields = additionalFields; - } - - @JsonUnwrapped - public Object getLogEvent() { - return logEvent; - } - - @JsonAnyGetter - public Map getAdditionalFields() { - return additionalFields; - } - - @JsonGetter("timestamp") - public String getTimestamp() { - return ISO_ZONED_DATE_TIME.format( - ZonedDateTime.from(ofEpochMilli(logEvent.getTimeMillis()).atZone(ZoneId.systemDefault()))); - } - } -} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java index da770ccdb..eccfdae4f 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspect.java @@ -15,92 +15,153 @@ package software.amazon.lambda.powertools.logging.internal; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Optional.empty; -import static java.util.Optional.ofNullable; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.coldStartDone; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.extractContext; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.getXrayTraceId; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isColdStart; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnStreamHandler; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName; -import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKey; -import static software.amazon.lambda.powertools.logging.LoggingUtils.appendKeys; -import static software.amazon.lambda.powertools.logging.LoggingUtils.objectMapper; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.coldStartDone; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.extractContext; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.getXrayTraceId; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isColdStart; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isHandlerMethod; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.placedOnRequestHandler; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.placedOnStreamHandler; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.serviceName; +import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.LAMBDA_LOG_LEVEL; +import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.POWERTOOLS_LOG_ERROR; +import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.POWERTOOLS_LOG_EVENT; +import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.POWERTOOLS_LOG_LEVEL; +import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.POWERTOOLS_LOG_RESPONSE; +import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.POWERTOOLS_SAMPLING_RATE; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.CORRELATION_ID; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_COLD_START; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_TRACE_ID; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.SERVICE; import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.core.JsonPointer; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; - +import io.burt.jmespath.Expression; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.util.Map; -import java.util.Optional; +import java.io.PrintStream; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Random; - -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.ThreadContext; -import org.apache.logging.log4j.core.LoggerContext; -import org.apache.logging.log4j.core.config.Configurator; -import org.apache.logging.log4j.core.util.IOUtils; +import java.util.ServiceLoader; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.DeclarePrecedence; import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.slf4j.MarkerFactory; +import org.slf4j.event.Level; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; +import software.amazon.lambda.powertools.utilities.JsonConfig; + @Aspect @DeclarePrecedence("*, software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect") public final class LambdaLoggingAspect { - private static final Logger LOG = LogManager.getLogger(LambdaLoggingAspect.class); - private static final String POWERTOOLS_LOG_LEVEL = System.getenv("POWERTOOLS_LOG_LEVEL"); - + private static final Logger LOG = LoggerFactory.getLogger(LambdaLoggingAspect.class); private static final Random SAMPLER = new Random(); - private static final String SAMPLING_RATE = System.getenv("POWERTOOLS_LOGGER_SAMPLE_RATE"); - private static Boolean LOG_EVENT; + private static Level LEVEL_AT_INITIALISATION; /* not final for test purpose */ - private static Level LEVEL_AT_INITIALISATION; + private static final LoggingManager LOGGING_MANAGER; static { + LOGGING_MANAGER = getLoggingManagerFromServiceLoader(); + + setLogLevel(); + + LEVEL_AT_INITIALISATION = LOGGING_MANAGER.getLogLevel(LOG); + } + + static void setLogLevel() { if (POWERTOOLS_LOG_LEVEL != null) { - Level powertoolsLevel = Level.getLevel(POWERTOOLS_LOG_LEVEL); + Level powertoolsLevel = getLevelFromString(POWERTOOLS_LOG_LEVEL); if (LAMBDA_LOG_LEVEL != null) { - Level lambdaLevel = Level.getLevel(LAMBDA_LOG_LEVEL); - if (powertoolsLevel.intLevel() > lambdaLevel.intLevel()) { + Level lambdaLevel = getLevelFromString(LAMBDA_LOG_LEVEL); + if (powertoolsLevel.toInt() < lambdaLevel.toInt()) { LOG.warn("Current log level ({}) does not match AWS Lambda Advanced Logging Controls minimum log level ({}). This can lead to data loss, consider adjusting them.", POWERTOOLS_LOG_LEVEL, LAMBDA_LOG_LEVEL); } } - resetLogLevels(powertoolsLevel); + setLogLevels(powertoolsLevel); } else if (LAMBDA_LOG_LEVEL != null) { - resetLogLevels(Level.getLevel(LAMBDA_LOG_LEVEL)); + setLogLevels(getLevelFromString(LAMBDA_LOG_LEVEL)); + } + } + + private static Level getLevelFromString(String level) { + if (Arrays.stream(Level.values()).anyMatch(slf4jLevel -> slf4jLevel.name().equalsIgnoreCase(level))) { + return Level.valueOf(level.toUpperCase()); + } else { + // FATAL does not exist in slf4j + if ("FATAL".equalsIgnoreCase(level)) { + return Level.ERROR; + } + } + // default to INFO if incorrect value + return Level.INFO; + } + + /** + * Use {@link ServiceLoader} to lookup for a {@link LoggingManager}. + * A file software.amazon.lambda.powertools.logging.internal.LoggingManager must be created in + * META-INF/services/ folder with the appropriate implementation of the {@link LoggingManager} + * + * @return an instance of {@link LoggingManager} + * @throws IllegalStateException if no {@link LoggingManager} could be found + */ + @SuppressWarnings("java:S106") // S106: System.err is used rather than logger to make sure message is printed + private static LoggingManager getLoggingManagerFromServiceLoader() { + ServiceLoader loggingManagers; + SecurityManager securityManager = System.getSecurityManager(); + if (securityManager == null) { + loggingManagers = ServiceLoader.load(LoggingManager.class); + } else { + final PrivilegedAction> action = () -> ServiceLoader.load(LoggingManager.class); + loggingManagers = AccessController.doPrivileged(action); } - LEVEL_AT_INITIALISATION = LOG.getLevel(); + List loggingManagerList = new ArrayList<>(); + for (LoggingManager lm : loggingManagers) { + loggingManagerList.add(lm); + } + return getLoggingManager(loggingManagerList, System.err); + } - String logEvent = System.getenv("POWERTOOLS_LOGGER_LOG_EVENT"); - if (logEvent != null) { - LOG_EVENT = Boolean.parseBoolean(logEvent); + static LoggingManager getLoggingManager(List loggingManagerList, PrintStream printStream) { + LoggingManager loggingManager; + if (loggingManagerList.isEmpty()) { + printStream.println("ERROR. No LoggingManager was found on the classpath"); + printStream.println("ERROR. Applying default LoggingManager: POWERTOOLS_LOG_LEVEL variable is ignored"); + printStream.println("ERROR. Make sure to add either powertools-logging-log4j or powertools-logging-logback to your dependencies"); + loggingManager = new DefautlLoggingManager(); } else { - LOG_EVENT = false; + if (loggingManagerList.size() > 1) { + printStream.println("WARN. Multiple LoggingManagers were found on the classpath"); + for (LoggingManager manager : loggingManagerList) { + printStream.println("WARN. Found LoggingManager: [" + manager + "]"); + } + printStream.println("WARN. Make sure to have only one of powertools-logging-log4j OR powertools-logging-logback to your dependencies"); + printStream.println("WARN. Using the first LoggingManager found on the classpath: [" + loggingManagerList.get(0) + "]"); + } + loggingManager = loggingManagerList.get(0); } + return loggingManager; } - private static void resetLogLevels(Level logLevel) { - LoggerContext ctx = (LoggerContext) LogManager.getContext(false); - Configurator.setAllLevels(LogManager.getRootLogger().getName(), logLevel); - ctx.updateLoggers(); + private static void setLogLevels(Level logLevel) { + LOGGING_MANAGER.setLogLevel(logLevel); } @SuppressWarnings({"EmptyMethod"}) @@ -108,41 +169,94 @@ private static void resetLogLevels(Level logLevel) { public void callAt(Logging logging) { } + /** + * Main method of the aspect + */ @Around(value = "callAt(logging) && execution(@Logging * *.*(..))", argNames = "pjp,logging") public Object around(ProceedingJoinPoint pjp, Logging logging) throws Throwable { - Object[] proceedArgs = pjp.getArgs(); + + boolean isOnRequestHandler = placedOnRequestHandler(pjp); + boolean isOnRequestStreamHandler = placedOnStreamHandler(pjp); setLogLevelBasedOnSamplingRate(pjp, logging); - Context extractedContext = extractContext(pjp); + addLambdaContextToLoggingContext(pjp); + + getXrayTraceId().ifPresent(xRayTraceId -> MDC.put(FUNCTION_TRACE_ID.getName(), xRayTraceId)); - if (null != extractedContext) { - appendKeys(DefaultLambdaFields.values(extractedContext)); - appendKey("coldStart", isColdStart() ? "true" : "false"); - appendKey("service", serviceName()); + // Log Event + Object[] proceedArgs = logEvent(pjp, logging, isOnRequestHandler, isOnRequestStreamHandler); + + if (!logging.correlationIdPath().isEmpty()) { + captureCorrelationId(logging.correlationIdPath(), proceedArgs, isOnRequestHandler, isOnRequestStreamHandler); } - getXrayTraceId().ifPresent(xRayTraceId -> appendKey("xray_trace_id", xRayTraceId)); + // To log the result of a RequestStreamHandler (OutputStream), we need to do the following: + // 1. backup a reference to the OutputStream provided by Lambda + // 2. create a temporary OutputStream and pass it to the handler method + // 3. retrieve this temporary stream to log it (if enabled) + // 4. write it back to the OutputStream provided by Lambda + OutputStream backupOutputStream = null; + if ((logging.logResponse() || POWERTOOLS_LOG_RESPONSE) && isOnRequestStreamHandler) { + backupOutputStream = (OutputStream) proceedArgs[1]; + proceedArgs[1] = new ByteArrayOutputStream(); + } - // Check that the environment variable was enabled explicitly - // Or that the handler was annotated with @Logging(logEvent = true) - if (LOG_EVENT || logging.logEvent()) { - proceedArgs = logEvent(pjp); + Object lambdaFunctionResponse; + + try { + // Call Function Handler + lambdaFunctionResponse = pjp.proceed(proceedArgs); + } catch (Throwable t) { + if (logging.logError() || POWERTOOLS_LOG_ERROR) { + // logging the exception with additional context + logger(pjp).error(MarkerFactory.getMarker("FATAL"), "Exception in Lambda Handler", t); + } + throw t; + } finally { + if (logging.clearState()) { + MDC.clear(); + } + coldStartDone(); } - if (!logging.correlationIdPath().isEmpty()) { - proceedArgs = captureCorrelationId(logging.correlationIdPath(), pjp); + // Log Response + if ((logging.logResponse() || POWERTOOLS_LOG_RESPONSE)) { + if (isOnRequestHandler) { + logRequestHandlerResponse(pjp, lambdaFunctionResponse); + } else if (isOnRequestStreamHandler && backupOutputStream != null) { + byte[] bytes = ((ByteArrayOutputStream)proceedArgs[1]).toByteArray(); + logRequestStreamHandlerResponse(pjp, bytes); + backupOutputStream.write(bytes); + } } - Object proceed = pjp.proceed(proceedArgs); + return lambdaFunctionResponse; + } + + private Object[] logEvent(ProceedingJoinPoint pjp, Logging logging, + boolean isOnRequestHandler, boolean isOnRequestStreamHandler) { + Object[] proceedArgs = pjp.getArgs(); - if (logging.clearState()) { - ThreadContext.clearMap(); + if (logging.logEvent() || POWERTOOLS_LOG_EVENT) { + if (isOnRequestHandler) { + logRequestHandlerEvent(pjp, pjp.getArgs()[0]); + } else if (isOnRequestStreamHandler) { + proceedArgs = logRequestStreamHandlerEvent(pjp); + } } + return proceedArgs; + } - coldStartDone(); - return proceed; + private void addLambdaContextToLoggingContext(ProceedingJoinPoint pjp) { + Context extractedContext = extractContext(pjp); + + if (extractedContext != null) { + PowertoolsLoggedFields.setValuesFromLambdaContext(extractedContext).forEach(MDC::put); + MDC.put(FUNCTION_COLD_START.getName(), isColdStart() ? "true" : "false"); + MDC.put(SERVICE.getName(), serviceName()); + } } private void setLogLevelBasedOnSamplingRate(final ProceedingJoinPoint pjp, @@ -152,12 +266,12 @@ private void setLogLevelBasedOnSamplingRate(final ProceedingJoinPoint pjp, if (isHandlerMethod(pjp)) { if (samplingRate < 0 || samplingRate > 1) { - LOG.debug("Skipping sampling rate configuration because of invalid value. Sampling rate: {}", + LOG.warn("Skipping sampling rate configuration because of invalid value. Sampling rate: {}", samplingRate); return; } - appendKey("samplingRate", String.valueOf(samplingRate)); + MDC.put(PowertoolsLoggedFields.SAMPLING_RATE.getName(), String.valueOf(samplingRate)); if (samplingRate == 0) { return; @@ -166,130 +280,119 @@ private void setLogLevelBasedOnSamplingRate(final ProceedingJoinPoint pjp, float sample = SAMPLER.nextFloat(); if (samplingRate > sample) { - resetLogLevels(Level.DEBUG); + setLogLevels(Level.DEBUG); - LOG.debug("Changed log level to DEBUG based on Sampling configuration. " + - "Sampling Rate: {}, Sampler Value: {}.", samplingRate, sample); - } else if (LEVEL_AT_INITIALISATION != LOG.getLevel()) { - resetLogLevels(LEVEL_AT_INITIALISATION); + LOG.debug("Changed log level to DEBUG based on Sampling configuration. " + + "Sampling Rate: {}, Sampler Value: {}.", samplingRate, sample); + } else if (LEVEL_AT_INITIALISATION != LOGGING_MANAGER.getLogLevel(LOG)) { + setLogLevels(LEVEL_AT_INITIALISATION); } } } private double samplingRate(final Logging logging) { - if (null != SAMPLING_RATE) { + String sampleRate = POWERTOOLS_SAMPLING_RATE; + if (null != sampleRate) { try { - return Double.parseDouble(SAMPLING_RATE); + return Double.parseDouble(sampleRate); } catch (NumberFormatException e) { - LOG.debug("Skipping sampling rate on environment variable configuration because of invalid " + - "value. Sampling rate: {}", SAMPLING_RATE); + LOG.warn("Skipping sampling rate on environment variable configuration because of invalid " + + "value. Sampling rate: {}", sampleRate); } } return logging.samplingRate(); } - private Object[] logEvent(final ProceedingJoinPoint pjp) { - Object[] args = pjp.getArgs(); - - if (isHandlerMethod(pjp)) { - if (placedOnRequestHandler(pjp)) { - Logger log = logger(pjp); - asJson(pjp, pjp.getArgs()[0]) - .ifPresent(log::info); - } + @SuppressWarnings("java:S3457") + private void logRequestHandlerEvent(final ProceedingJoinPoint pjp, final Object event) { + Logger log = logger(pjp); + if (log.isInfoEnabled()) { + log.info("Handler Event", entry("event", event)); + } + } - if (placedOnStreamHandler(pjp)) { - args = logFromInputStream(pjp); + @SuppressWarnings("java:S3457") + private Object[] logRequestStreamHandlerEvent(final ProceedingJoinPoint pjp) { + Object[] args = pjp.getArgs(); + Logger log = logger(pjp); + if (log.isInfoEnabled()) { + try { + byte[] bytes = bytesFromInputStreamSafely((InputStream) pjp.getArgs()[0]); + args[0] = new ByteArrayInputStream(bytes); + // do not log asJson as it can be something else (String, XML...) + log.info("Handler Event", entry("event", new String(bytes, UTF_8))); + } catch (IOException e) { + LOG.warn("Failed to log event from supplied input stream.", e); } } - return args; } - private Object[] captureCorrelationId(final String correlationIdPath, - final ProceedingJoinPoint pjp) { - Object[] args = pjp.getArgs(); - if (isHandlerMethod(pjp)) { - if (placedOnRequestHandler(pjp)) { - Object arg = pjp.getArgs()[0]; - JsonNode jsonNode = objectMapper().valueToTree(arg); - - setCorrelationIdFromNode(correlationIdPath, pjp, jsonNode); - - return args; - } + @SuppressWarnings("java:S3457") + private void logRequestHandlerResponse(final ProceedingJoinPoint pjp, final Object response) { + Logger log = logger(pjp); + if (log.isInfoEnabled()) { + log.info("Handler Response", entry("response", response)); + } + } - if (placedOnStreamHandler(pjp)) { - try { - byte[] bytes = bytesFromInputStreamSafely((InputStream) pjp.getArgs()[0]); - JsonNode jsonNode = objectMapper().readTree(bytes); - args[0] = new ByteArrayInputStream(bytes); + @SuppressWarnings("java:S3457") + private void logRequestStreamHandlerResponse(final ProceedingJoinPoint pjp, final byte[] bytes) { + Logger log = logger(pjp); + if (log.isInfoEnabled()) { + // we do not log with asJson as it can be something else (String, XML, ...) + log.info("Handler Response", entry("response", new String(bytes, UTF_8))); + } + } - setCorrelationIdFromNode(correlationIdPath, pjp, jsonNode); + private void captureCorrelationId(final String correlationIdPath, + Object[] proceedArgs, + final boolean isOnRequestHandler, + final boolean isOnRequestStreamHandler) { + if (isOnRequestHandler) { + JsonNode jsonNode = JsonConfig.get().getObjectMapper().valueToTree(proceedArgs[0]); + setCorrelationIdFromNode(correlationIdPath, jsonNode); + } else if (isOnRequestStreamHandler) { + try { + byte[] bytes = bytesFromInputStreamSafely((InputStream) proceedArgs[0]); + JsonNode jsonNode = JsonConfig.get().getObjectMapper().readTree(bytes); + proceedArgs[0] = new ByteArrayInputStream(bytes); - return args; - } catch (IOException e) { - Logger log = logger(pjp); - log.warn("Failed to capture correlation id on event from supplied input stream.", e); - } + setCorrelationIdFromNode(correlationIdPath, jsonNode); + } catch (IOException e) { + LOG.warn("Failed to capture correlation id on event from supplied input stream.", e); } } - - return args; } - private void setCorrelationIdFromNode(String correlationIdPath, ProceedingJoinPoint pjp, JsonNode jsonNode) { - JsonNode node = jsonNode.at(JsonPointer.compile(correlationIdPath)); + private void setCorrelationIdFromNode(String correlationIdPath, JsonNode jsonNode) { + Expression jmesExpression = JsonConfig.get().getJmesPath().compile(correlationIdPath); + JsonNode node = jmesExpression.search(jsonNode); String asText = node.asText(); if (null != asText && !asText.isEmpty()) { - LoggingUtils.setCorrelationId(asText); + MDC.put(CORRELATION_ID.getName(), asText); } else { - logger(pjp).debug("Unable to extract any correlation id. Is your function expecting supported event type?"); + LOG.warn("Unable to extract any correlation id. Is your function expecting supported event type?"); } } - private Object[] logFromInputStream(final ProceedingJoinPoint pjp) { - Object[] args = pjp.getArgs(); - - try { - byte[] bytes = bytesFromInputStreamSafely((InputStream) pjp.getArgs()[0]); - args[0] = new ByteArrayInputStream(bytes); - Logger log = logger(pjp); - - asJson(pjp, objectMapper().readValue(bytes, Map.class)) - .ifPresent(log::info); - - } catch (IOException e) { - Logger log = logger(pjp); - log.debug("Failed to log event from supplied input stream.", e); - } - - return args; - } private byte[] bytesFromInputStreamSafely(final InputStream inputStream) throws IOException { try (ByteArrayOutputStream out = new ByteArrayOutputStream(); - InputStreamReader reader = new InputStreamReader(inputStream, UTF_8)) { + InputStreamReader reader = new InputStreamReader(inputStream, UTF_8)) { OutputStreamWriter writer = new OutputStreamWriter(out, UTF_8); - - IOUtils.copy(reader, writer); + int n; + char[] buffer = new char[4096]; + while (-1 != (n = reader.read(buffer))) { + writer.write(buffer, 0, n); + } writer.flush(); return out.toByteArray(); } } - private Optional asJson(final ProceedingJoinPoint pjp, - final Object target) { - try { - return ofNullable(objectMapper().writeValueAsString(target)); - } catch (JsonProcessingException e) { - logger(pjp).error("Failed logging event of type {}", target.getClass(), e); - return empty(); - } - } - private Logger logger(final ProceedingJoinPoint pjp) { - return LogManager.getLogger(pjp.getSignature().getDeclaringType()); + return LoggerFactory.getLogger(pjp.getSignature().getDeclaringType()); } } diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolver.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolver.java deleted file mode 100644 index 500b36c95..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolver.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.logging.internal; - -import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.LAMBDA_LOG_FORMAT; -import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.LOG_DATE_RFC3339_FORMAT; - -import java.util.Locale; -import java.util.TimeZone; -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.time.MutableInstant; -import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults; -import org.apache.logging.log4j.layout.template.json.resolver.EventResolver; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig; -import org.apache.logging.log4j.layout.template.json.util.InstantFormatter; -import org.apache.logging.log4j.layout.template.json.util.JsonWriter; - -/** - * Default timestamp used by log4j is not RFC3339, which is used by Lambda internally to filter logs. - * When `AWS_LAMBDA_LOG_FORMAT` is set to JSON (i.e. using Lambda logging configuration), we should use the appropriate pattern, - * otherwise logs with invalid date format are considered as INFO. - * Inspired from org.apache.logging.log4j.layout.template.json.resolver.TimestampResolver - * - * TODO: remove in v2 an replace with the good pattern in LambdaJsonLayout.json - */ -public class LambdaTimestampResolver implements EventResolver { - - private final EventResolver internalResolver; - - public LambdaTimestampResolver(final TemplateResolverConfig config) { - final PatternResolverContext patternResolverContext = - PatternResolverContext.fromConfig(config); - internalResolver = new PatternResolver(patternResolverContext); - } - - @Override - public void resolve(LogEvent value, JsonWriter jsonWriter) { - internalResolver.resolve(value, jsonWriter); - } - - static String getName() { - return "lambda-timestamp"; - } - - private static final class PatternResolverContext { - - public static final String PATTERN = "pattern"; - private final InstantFormatter formatter; - - private final StringBuilder lastFormattedInstantBuffer = new StringBuilder(); - - private final MutableInstant lastFormattedInstant = new MutableInstant(); - - private PatternResolverContext( - final String pattern, - final TimeZone timeZone, - final Locale locale) { - this.formatter = InstantFormatter - .newBuilder() - .setPattern(pattern) - .setTimeZone(timeZone) - .setLocale(locale) - .build(); - lastFormattedInstant.initFromEpochSecond(-1, 0); - } - - private static PatternResolverContext fromConfig( - final TemplateResolverConfig config) { - final String pattern = readPattern(config); - final TimeZone timeZone = readTimeZone(config); - final Locale locale = config.getLocale(new String[]{PATTERN, "locale"}); - return new PatternResolverContext(pattern, timeZone, locale); - } - - private static String readPattern(final TemplateResolverConfig config) { - final String format = config.getString(new String[]{PATTERN, "format"}); - return format != null - ? format - : getLambdaTimestampFormatOrDefault(); - } - - private static String getLambdaTimestampFormatOrDefault() { - return "JSON".equals(LAMBDA_LOG_FORMAT) ? LOG_DATE_RFC3339_FORMAT : - JsonTemplateLayoutDefaults.getTimestampFormatPattern(); - } - - private static TimeZone readTimeZone(final TemplateResolverConfig config) { - final String timeZoneId = config.getString(new String[]{PATTERN, "timeZone"}); - if (timeZoneId == null) { - return JsonTemplateLayoutDefaults.getTimeZone(); - } - boolean found = false; - for (final String availableTimeZone : TimeZone.getAvailableIDs()) { - if (availableTimeZone.equalsIgnoreCase(timeZoneId)) { - found = true; - break; - } - } - if (!found) { - throw new IllegalArgumentException( - "invalid timestamp time zone: " + config); - } - return TimeZone.getTimeZone(timeZoneId); - } - - } - - private static final class PatternResolver implements EventResolver { - - private final PatternResolverContext patternResolverContext; - - private PatternResolver(final PatternResolverContext patternResolverContext) { - this.patternResolverContext = patternResolverContext; - } - - @Override - public synchronized void resolve( - final LogEvent logEvent, - final JsonWriter jsonWriter) { - - // Format timestamp if it doesn't match the last cached one. - final boolean instantMatching = patternResolverContext.formatter.isInstantMatching( - patternResolverContext.lastFormattedInstant, - logEvent.getInstant()); - if (!instantMatching) { - - // Format the timestamp. - patternResolverContext.lastFormattedInstantBuffer.setLength(0); - patternResolverContext.lastFormattedInstant.initFrom(logEvent.getInstant()); - patternResolverContext.formatter.format( - patternResolverContext.lastFormattedInstant, - patternResolverContext.lastFormattedInstantBuffer); - - // Write the formatted timestamp. - final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder(); - final int startIndex = jsonWriterStringBuilder.length(); - jsonWriter.writeString(patternResolverContext.lastFormattedInstantBuffer); - - // Cache the written value. - patternResolverContext.lastFormattedInstantBuffer.setLength(0); - patternResolverContext.lastFormattedInstantBuffer.append( - jsonWriterStringBuilder, - startIndex, - jsonWriterStringBuilder.length()); - - } - - // Write the cached formatted timestamp. - else { - jsonWriter.writeRawString( - patternResolverContext.lastFormattedInstantBuffer); - } - - } - - } -} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolverFactory.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolverFactory.java deleted file mode 100644 index 2022c6d4a..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolverFactory.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.logging.internal; - -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.config.plugins.Plugin; -import org.apache.logging.log4j.core.config.plugins.PluginFactory; -import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext; -import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory; - -@Plugin(name = "LambdaTimestampResolverFactory", category = TemplateResolverFactory.CATEGORY) -public final class LambdaTimestampResolverFactory implements EventResolverFactory { - - private static final LambdaTimestampResolverFactory INSTANCE = new LambdaTimestampResolverFactory(); - - private LambdaTimestampResolverFactory() { - } - - @PluginFactory - public static LambdaTimestampResolverFactory getInstance() { - return INSTANCE; - } - - @Override - public String getName() { - return LambdaTimestampResolver.getName(); - } - - @Override - public TemplateResolver create(EventResolverContext context, - TemplateResolverConfig config) { - return new LambdaTimestampResolver(config); - } -} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingConstants.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingConstants.java index e58ca4109..989608a77 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingConstants.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingConstants.java @@ -14,12 +14,18 @@ package software.amazon.lambda.powertools.logging.internal; -public class LoggingConstants { - public static final String LAMBDA_LOG_LEVEL = System.getenv("AWS_LAMBDA_LOG_LEVEL"); +class LoggingConstants { + static String LAMBDA_LOG_LEVEL = System.getenv("AWS_LAMBDA_LOG_LEVEL"); /* not final for test purpose */ - public static final String LAMBDA_LOG_FORMAT = System.getenv("AWS_LAMBDA_LOG_FORMAT"); + static String POWERTOOLS_LOG_LEVEL = System.getenv("POWERTOOLS_LOG_LEVEL"); /* not final for test purpose */ - public static final String LOG_DATE_RFC3339_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + static String POWERTOOLS_SAMPLING_RATE = System.getenv("POWERTOOLS_LOGGER_SAMPLE_RATE"); /* not final for test purpose */ + + static boolean POWERTOOLS_LOG_EVENT = "true".equals(System.getenv("POWERTOOLS_LOGGER_LOG_EVENT")); /* not final for test purpose */ + + static boolean POWERTOOLS_LOG_RESPONSE = "true".equals(System.getenv("POWERTOOLS_LOGGER_LOG_RESPONSE")); /* not final for test purpose */ + + static boolean POWERTOOLS_LOG_ERROR = "true".equals(System.getenv("POWERTOOLS_LOGGER_LOG_ERROR")); /* not final for test purpose */ private LoggingConstants() { // constants diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManager.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManager.java new file mode 100644 index 000000000..51d05b25d --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManager.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +import org.slf4j.Logger; +import org.slf4j.event.Level; + +/** + * Due to limitations of SLF4J, we need to rely on implementations for some operations: + *

    + *
  • Accessing to all loggers and change their Level
  • + *
  • Retrieving the log Level of a Logger
  • + *
+ * + *

+ * This interface is used for these operations and implementations are provided in submodules and loaded thanks to a {@link java.util.ServiceLoader} + * (define a file named software.amazon.lambda.powertools.logging.internal.LoggingManager + * in src/main/resources/META-INF/services with the qualified name of the implementation). + */ +public interface LoggingManager { + /** + * Change the log Level of all loggers (named and root) + * + * @param logLevel the log Level (slf4j) to apply + */ + void setLogLevel(Level logLevel); + + /** + * Retrieve the log Level of a specific logger + * + * @param logger the logger (slf4j) for which to retrieve the log Level + * @return the Level (slf4j) of this logger. + * Note that SLF4J only support ERROR, WARN, INFO, DEBUG, TRACE while some frameworks may support others (OFF, FATAL, ...) + */ + Level getLogLevel(Logger logger); +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLambdaFields.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsLoggedFields.java similarity index 59% rename from powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLambdaFields.java rename to powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsLoggedFields.java index 2461ae771..6e0047f4f 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLambdaFields.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsLoggedFields.java @@ -16,22 +16,38 @@ import com.amazonaws.services.lambda.runtime.Context; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; -enum DefaultLambdaFields { - FUNCTION_NAME("functionName"), - FUNCTION_VERSION("functionVersion"), - FUNCTION_ARN("functionArn"), - FUNCTION_MEMORY_SIZE("functionMemorySize"), - FUNCTION_REQUEST_ID("function_request_id"); +/** + * Fields added in the logs by Powertools. + * Same as python + */ +public enum PowertoolsLoggedFields { + FUNCTION_NAME("function_name"), + FUNCTION_VERSION("function_version"), + FUNCTION_ARN("function_arn"), + FUNCTION_MEMORY_SIZE("function_memory_size"), + FUNCTION_REQUEST_ID("function_request_id"), + FUNCTION_COLD_START("cold_start"), + FUNCTION_TRACE_ID("xray_trace_id"), + SAMPLING_RATE("sampling_rate"), + CORRELATION_ID("correlation_id"), + SERVICE("service"); private final String name; - DefaultLambdaFields(String name) { + PowertoolsLoggedFields(String name) { this.name = name; } - static Map values(Context context) { + public static List stringValues() { + return Stream.of(values()).map(PowertoolsLoggedFields::getName).collect(Collectors.toList()); + } + + static Map setValuesFromLambdaContext(Context context) { Map hashMap = new HashMap<>(); hashMap.put(FUNCTION_NAME.name, context.getFunctionName()); diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolver.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolver.java deleted file mode 100644 index dc9816932..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolver.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.logging.internal; - -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.layout.template.json.resolver.EventResolver; -import org.apache.logging.log4j.layout.template.json.util.JsonWriter; -import org.apache.logging.log4j.util.ReadOnlyStringMap; - -final class PowertoolsResolver implements EventResolver { - - private final EventResolver internalResolver; - - PowertoolsResolver() { - internalResolver = new EventResolver() { - @Override - public boolean isResolvable(LogEvent value) { - ReadOnlyStringMap contextData = value.getContextData(); - return null != contextData && !contextData.isEmpty(); - } - - @Override - public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { - StringBuilder stringBuilder = jsonWriter.getStringBuilder(); - // remove dummy field to kick inn powertools resolver - stringBuilder.setLength(stringBuilder.length() - 4); - - // Inject all the context information. - ReadOnlyStringMap contextData = logEvent.getContextData(); - contextData.forEach((key, value) -> - { - jsonWriter.writeSeparator(); - jsonWriter.writeString(key); - stringBuilder.append(':'); - jsonWriter.writeValue(value); - }); - } - }; - } - - static String getName() { - return "powertools"; - } - - @Override - public void resolve(LogEvent value, JsonWriter jsonWriter) { - internalResolver.resolve(value, jsonWriter); - } - - @Override - public boolean isResolvable(LogEvent value) { - return internalResolver.isResolvable(value); - } -} diff --git a/powertools-logging/src/main/resources/LambdaEcsLayout.json b/powertools-logging/src/main/resources/LambdaEcsLayout.json deleted file mode 100644 index 4ab9c7ce2..000000000 --- a/powertools-logging/src/main/resources/LambdaEcsLayout.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "@timestamp": { - "$resolver": "timestamp", - "pattern": { - "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", - "timeZone": "UTC" - } - }, - "ecs.version": "1.2.0", - "log.level": { - "$resolver": "level", - "field": "name" - }, - "message": { - "$resolver": "message", - "stringified": true - }, - "process.thread.name": { - "$resolver": "thread", - "field": "name" - }, - "log.logger": { - "$resolver": "logger", - "field": "name" - }, - "labels": { - "$resolver": "mdc", - "flatten": true, - "stringified": true - }, - "tags": { - "$resolver": "ndc" - }, - "error.type": { - "$resolver": "exception", - "field": "className" - }, - "error.message": { - "$resolver": "exception", - "field": "message" - }, - "error.stack_trace": { - "$resolver": "exception", - "field": "stackTrace", - "stackTrace": { - "stringified": true - } - }, - "": { - "$resolver": "powertools" - } -} \ No newline at end of file diff --git a/powertools-logging/src/main/resources/LambdaJsonLayout.json b/powertools-logging/src/main/resources/LambdaJsonLayout.json deleted file mode 100644 index da3385032..000000000 --- a/powertools-logging/src/main/resources/LambdaJsonLayout.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "timestamp": { - "$resolver": "lambda-timestamp" - }, - "instant": { - "epochSecond": { - "$resolver": "timestamp", - "epoch": { - "unit": "secs", - "rounded": true - } - }, - "nanoOfSecond": { - "$resolver": "timestamp", - "epoch": { - "unit": "secs.nanos" - } - } - }, - "thread": { - "$resolver": "thread", - "field": "name" - }, - "level": { - "$resolver": "level", - "field": "name" - }, - "loggerName": { - "$resolver": "logger", - "field": "name" - }, - "message": { - "$resolver": "message", - "stringified": true - }, - "thrown": { - "message": { - "$resolver": "exception", - "field": "message" - }, - "name": { - "$resolver": "exception", - "field": "className" - }, - "extendedStackTrace": { - "$resolver": "exception", - "field": "stackTrace" - } - }, - "contextStack": { - "$resolver": "ndc" - }, - "endOfBatch": { - "$resolver": "endOfBatch" - }, - "loggerFqcn": { - "$resolver": "logger", - "field": "fqcn" - }, - "threadId": { - "$resolver": "thread", - "field": "id" - }, - "threadPriority": { - "$resolver": "thread", - "field": "priority" - }, - "source": { - "class": { - "$resolver": "source", - "field": "className" - }, - "method": { - "$resolver": "source", - "field": "methodName" - }, - "file": { - "$resolver": "source", - "field": "fileName" - }, - "line": { - "$resolver": "source", - "field": "lineNumber" - } - }, - "": { - "$resolver": "powertools" - } -} diff --git a/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/jni-config.json b/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/jni-config.json new file mode 100644 index 000000000..2c4de0562 --- /dev/null +++ b/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/jni-config.json @@ -0,0 +1,26 @@ +[ +{ + "name":"java.lang.Boolean", + "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.String", + "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"org.apache.maven.surefire.booter.ForkedBooter", + "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"sun.instrument.InstrumentationImpl", + "methods":[{"name":"","parameterTypes":["long","boolean","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] +}, +{ + "name":"sun.management.VMManagementImpl", + "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] +} +] diff --git a/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/reflect-config.json b/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/reflect-config.json new file mode 100644 index 000000000..7347b8400 --- /dev/null +++ b/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/reflect-config.json @@ -0,0 +1,669 @@ +[ +{ + "name":"[Lcom.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers;" +}, +{ + "name":"[Lcom.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.KeyDeserializers;" +}, +{ + "name":"[Lcom.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.ValueInstantiators;" +}, +{ + "name":"[Lcom.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ser.Serializers;" +}, +{ + "name":"[Lsoftware.amazon.lambda.powertools.logging.model.Product;" +}, +{ + "name":"com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7HandlersImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.Context", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getAwsRequestId","parameterTypes":[] }, {"name":"getClientContext","parameterTypes":[] }, {"name":"getFunctionName","parameterTypes":[] }, {"name":"getFunctionVersion","parameterTypes":[] }, {"name":"getIdentity","parameterTypes":[] }, {"name":"getInvokedFunctionArn","parameterTypes":[] }, {"name":"getLogGroupName","parameterTypes":[] }, {"name":"getLogStreamName","parameterTypes":[] }, {"name":"getLogger","parameterTypes":[] }, {"name":"getMemoryLimitInMB","parameterTypes":[] }, {"name":"getRemainingTimeInMillis","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.RequestHandler", + "allDeclaredClasses":true, + "queryAllPublicMethods":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.RequestStreamHandler", + "allDeclaredClasses":true, + "queryAllPublicMethods":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getBody","parameterTypes":[] }, {"name":"getHeaders","parameterTypes":[] }, {"name":"getHttpMethod","parameterTypes":[] }, {"name":"getIsBase64Encoded","parameterTypes":[] }, {"name":"getMultiValueHeaders","parameterTypes":[] }, {"name":"getMultiValueQueryStringParameters","parameterTypes":[] }, {"name":"getPath","parameterTypes":[] }, {"name":"getPathParameters","parameterTypes":[] }, {"name":"getQueryStringParameters","parameterTypes":[] }, {"name":"getRequestContext","parameterTypes":[] }, {"name":"getResource","parameterTypes":[] }, {"name":"getStageVariables","parameterTypes":[] }, {"name":"getVersion","parameterTypes":[] }, {"name":"setBody","parameterTypes":["java.lang.String"] }, {"name":"setHeaders","parameterTypes":["java.util.Map"] }, {"name":"setHttpMethod","parameterTypes":["java.lang.String"] }, {"name":"setIsBase64Encoded","parameterTypes":["java.lang.Boolean"] }, {"name":"setMultiValueHeaders","parameterTypes":["java.util.Map"] }, {"name":"setMultiValueQueryStringParameters","parameterTypes":["java.util.Map"] }, {"name":"setPath","parameterTypes":["java.lang.String"] }, {"name":"setPathParameters","parameterTypes":["java.util.Map"] }, {"name":"setQueryStringParameters","parameterTypes":["java.util.Map"] }, {"name":"setRequestContext","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$ProxyRequestContext"] }, {"name":"setResource","parameterTypes":["java.lang.String"] }, {"name":"setStageVariables","parameterTypes":["java.util.Map"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$ProxyRequestContext", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getAccountId","parameterTypes":[] }, {"name":"getApiId","parameterTypes":[] }, {"name":"getAuthorizer","parameterTypes":[] }, {"name":"getDomainName","parameterTypes":[] }, {"name":"getDomainPrefix","parameterTypes":[] }, {"name":"getExtendedRequestId","parameterTypes":[] }, {"name":"getHttpMethod","parameterTypes":[] }, {"name":"getIdentity","parameterTypes":[] }, {"name":"getOperationName","parameterTypes":[] }, {"name":"getPath","parameterTypes":[] }, {"name":"getProtocol","parameterTypes":[] }, {"name":"getRequestId","parameterTypes":[] }, {"name":"getRequestTime","parameterTypes":[] }, {"name":"getRequestTimeEpoch","parameterTypes":[] }, {"name":"getResourceId","parameterTypes":[] }, {"name":"getResourcePath","parameterTypes":[] }, {"name":"getStage","parameterTypes":[] }, {"name":"setHttpMethod","parameterTypes":["java.lang.String"] }, {"name":"setPath","parameterTypes":["java.lang.String"] }, {"name":"setRequestId","parameterTypes":["java.lang.String"] }, {"name":"setResourcePath","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$RequestIdentity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getBody","parameterTypes":[] }, {"name":"getCookies","parameterTypes":[] }, {"name":"getHeaders","parameterTypes":[] }, {"name":"getIsBase64Encoded","parameterTypes":[] }, {"name":"getPathParameters","parameterTypes":[] }, {"name":"getQueryStringParameters","parameterTypes":[] }, {"name":"getRawPath","parameterTypes":[] }, {"name":"getRawQueryString","parameterTypes":[] }, {"name":"getRequestContext","parameterTypes":[] }, {"name":"getRouteKey","parameterTypes":[] }, {"name":"getStageVariables","parameterTypes":[] }, {"name":"getVersion","parameterTypes":[] }, {"name":"setCookies","parameterTypes":["java.util.List"] }, {"name":"setHeaders","parameterTypes":["java.util.Map"] }, {"name":"setIsBase64Encoded","parameterTypes":["boolean"] }, {"name":"setRawPath","parameterTypes":["java.lang.String"] }, {"name":"setRawQueryString","parameterTypes":["java.lang.String"] }, {"name":"setRequestContext","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext"] }, {"name":"setRouteKey","parameterTypes":["java.lang.String"] }, {"name":"setVersion","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getAccountId","parameterTypes":[] }, {"name":"getApiId","parameterTypes":[] }, {"name":"getAuthorizer","parameterTypes":[] }, {"name":"getDomainName","parameterTypes":[] }, {"name":"getDomainPrefix","parameterTypes":[] }, {"name":"getHttp","parameterTypes":[] }, {"name":"getRequestId","parameterTypes":[] }, {"name":"getRouteKey","parameterTypes":[] }, {"name":"getStage","parameterTypes":[] }, {"name":"getTime","parameterTypes":[] }, {"name":"getTimeEpoch","parameterTypes":[] }, {"name":"setAccountId","parameterTypes":["java.lang.String"] }, {"name":"setApiId","parameterTypes":["java.lang.String"] }, {"name":"setDomainName","parameterTypes":["java.lang.String"] }, {"name":"setDomainPrefix","parameterTypes":["java.lang.String"] }, {"name":"setHttp","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext$Http"] }, {"name":"setRequestId","parameterTypes":["java.lang.String"] }, {"name":"setRouteKey","parameterTypes":["java.lang.String"] }, {"name":"setStage","parameterTypes":["java.lang.String"] }, {"name":"setTime","parameterTypes":["java.lang.String"] }, {"name":"setTimeEpoch","parameterTypes":["long"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext$Authorizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext$Authorizer$JWT", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext$CognitoIdentity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext$Http", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getMethod","parameterTypes":[] }, {"name":"getPath","parameterTypes":[] }, {"name":"getProtocol","parameterTypes":[] }, {"name":"getSourceIp","parameterTypes":[] }, {"name":"getUserAgent","parameterTypes":[] }, {"name":"setMethod","parameterTypes":["java.lang.String"] }, {"name":"setPath","parameterTypes":["java.lang.String"] }, {"name":"setProtocol","parameterTypes":["java.lang.String"] }, {"name":"setSourceIp","parameterTypes":["java.lang.String"] }, {"name":"setUserAgent","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext$IAM", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getBody","parameterTypes":[] }, {"name":"getHeaders","parameterTypes":[] }, {"name":"getHttpMethod","parameterTypes":[] }, {"name":"getIsBase64Encoded","parameterTypes":[] }, {"name":"getMultiValueHeaders","parameterTypes":[] }, {"name":"getMultiValueQueryStringParameters","parameterTypes":[] }, {"name":"getPath","parameterTypes":[] }, {"name":"getQueryStringParameters","parameterTypes":[] }, {"name":"getRequestContext","parameterTypes":[] }, {"name":"setBody","parameterTypes":["java.lang.String"] }, {"name":"setHeaders","parameterTypes":["java.util.Map"] }, {"name":"setHttpMethod","parameterTypes":["java.lang.String"] }, {"name":"setIsBase64Encoded","parameterTypes":["boolean"] }, {"name":"setPath","parameterTypes":["java.lang.String"] }, {"name":"setQueryStringParameters","parameterTypes":["java.util.Map"] }, {"name":"setRequestContext","parameterTypes":["com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent$RequestContext"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent$Elb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getTargetGroupArn","parameterTypes":[] }, {"name":"setTargetGroupArn","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent$RequestContext", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getElb","parameterTypes":[] }, {"name":"setElb","parameterTypes":["com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent$Elb"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.tests.EventArgumentsProvider", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.tests.annotations.Event", + "queryAllPublicMethods":true +}, +{ + "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.tools.attach.VirtualMachine" +}, +{ + "name":"double", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.io.IOException" +}, +{ + "name":"java.io.InputStream" +}, +{ + "name":"java.io.OutputStream" +}, +{ + "name":"java.io.Serializable", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.Boolean" +}, +{ + "name":"java.lang.Class", + "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] +}, +{ + "name":"java.lang.ClassLoader", + "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] +}, +{ + "name":"java.lang.Cloneable", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.Comparable", + "allDeclaredClasses":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.Enum", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.Module", + "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] +}, +{ + "name":"java.lang.Object", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] +}, +{ + "name":"java.lang.ProcessEnvironment", + "fields":[{"name":"theCaseInsensitiveEnvironment"}, {"name":"theEnvironment"}] +}, +{ + "name":"java.lang.ProcessHandle", + "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime", + "methods":[{"name":"version","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime$Version", + "methods":[{"name":"feature","parameterTypes":[] }] +}, +{ + "name":"java.lang.StackWalker" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.annotation.Retention", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.annotation.Target", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.constant.Constable", + "allDeclaredClasses":true, + "queryAllPublicMethods":true +}, +{ + "name":"java.lang.invoke.MethodHandle", + "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles", + "methods":[{"name":"lookup","parameterTypes":[] }] +}, +{ + "name":"java.lang.invoke.MethodHandles$Lookup", + "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] +}, +{ + "name":"java.lang.invoke.MethodType", + "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] +}, +{ + "name":"java.lang.reflect.AccessibleObject", + "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] +}, +{ + "name":"java.lang.reflect.AnnotatedArrayType", + "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedType", + "methods":[{"name":"getType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Executable", + "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Parameter", + "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.util.Collections$UnmodifiableMap", + "fields":[{"name":"m"}] +}, +{ + "name":"java.util.Map" +}, +{ + "name":"java.util.concurrent.ForkJoinTask", + "fields":[{"name":"aux"}, {"name":"status"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.function.Consumer", + "queryAllPublicMethods":true +}, +{ + "name":"jdk.internal.misc.Unsafe" +}, +{ + "name":"kotlin.jvm.JvmInline" +}, +{ + "name":"org.apiguardian.api.API", + "queryAllPublicMethods":true +}, +{ + "name":"org.aspectj.runtime.internal.AroundClosure", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.joda.time.DateTime" +}, +{ + "name":"org.slf4j.ILoggerFactory", + "allDeclaredClasses":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.slf4j.Logger", + "allDeclaredClasses":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.slf4j.helpers.AbstractLogger", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.slf4j.helpers.LegacyAbstractLogger", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.slf4j.spi.SLF4JServiceProvider", + "allDeclaredClasses":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.slf4j.test.OutputChoice", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.slf4j.test.OutputChoice$OutputChoiceType", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.slf4j.test.TestLogger", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.slf4j.test.TestLoggerConfiguration", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.slf4j.test.TestLoggerFactory", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.slf4j.test.TestServiceProvider", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor", + "fields":[{"name":"IS_COLD_START"}, {"name":"SERVICE_NAME"}] +}, +{ + "name":"software.amazon.lambda.powertools.logging.argument.StructuredArgumentsTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"arrayArgument","parameterTypes":[] }, {"name":"jsonArgument","parameterTypes":[] }, {"name":"keyValueArgument","parameterTypes":[] }, {"name":"mapArgument","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAlbCorrelationId", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAlbCorrelationId$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayHttpApiCorrelationId", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayHttpApiCorrelationId$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayRestApiCorrelationId", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayRestApiCorrelationId$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAppSyncCorrelationId", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAppSyncCorrelationId$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogClearState", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.util.Map","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogClearState$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogDisabled", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogDisabledForStream", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabled", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"anotherMethod","parameterTypes":[] }, {"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabled$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabled$AjcClosure3", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabledForStream", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabledForStream$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogError", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogError$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEvent", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEvent$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventBridgeCorrelationId", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventBridgeCorrelationId$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventEnvVar", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventEnvVar$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventForStream", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventForStream$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponse", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponse$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponseForStream", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponseForStream$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogSamplingDisabled", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogSamplingDisabled$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogSamplingEnabled", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.handlers.PowertoolsLogSamplingEnabled$AjcClosure1", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect", + "fields":[{"name":"LEVEL_AT_INITIALISATION"}], + "methods":[{"name":"setLogLevels","parameterTypes":["org.slf4j.event.Level"] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.LoggingConstants", + "fields":[{"name":"LAMBDA_LOG_LEVEL"}, {"name":"POWERTOOLS_LOG_EVENT"}, {"name":"POWERTOOLS_LOG_LEVEL"}, {"name":"POWERTOOLS_SAMPLING_RATE"}] +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.LoggingManager", + "allDeclaredClasses":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.internal.TestLoggingManager", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.logging.model.Basket", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getProducts","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.logging.model.Product", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getId","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPrice","parameterTypes":[] }] +}, +{ + "name":"sun.reflect.ReflectionFactory", + "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] +} +] diff --git a/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/resource-config.json b/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/resource-config.json new file mode 100644 index 000000000..ca77675e0 --- /dev/null +++ b/powertools-logging/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-logging/resource-config.json @@ -0,0 +1,23 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.configuration.Configuration\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.presentation.Representation\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager\\E" + }, { + "pattern":"\\Qcom/amazonaws/lambda/thirdparty/org/joda/time/tz/data/Europe/Berlin\\E" + }, { + "pattern":"\\Qcom/amazonaws/lambda/thirdparty/org/joda/time/tz/data/ZoneInfoMap\\E" + }]}, + "bundles":[] +} diff --git a/powertools-logging/src/main/resources/log4j2.component.properties b/powertools-logging/src/main/resources/log4j2.component.properties deleted file mode 100644 index 3c392dd13..000000000 --- a/powertools-logging/src/main/resources/log4j2.component.properties +++ /dev/null @@ -1,2 +0,0 @@ -log4j.layout.jsonTemplate.timestampFormatPattern=yyyy-MM-dd'T'HH:mm:ss.SSSZz -#log4j.layout.jsonTemplate.timeZone= \ No newline at end of file diff --git a/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java b/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java deleted file mode 100644 index 95fb9c47f..000000000 --- a/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package org.apache.logging.log4j.core.layout; - -import static java.util.Collections.emptyMap; -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; -import static org.assertj.core.api.Assertions.as; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.channels.FileChannel; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.time.format.ResolverStyle; -import java.util.Map; -import org.apache.logging.log4j.Level; -import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabled; -import software.amazon.lambda.powertools.logging.handlers.PowerLogToolSamplingEnabled; -import software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect; - -class LambdaJsonLayoutTest { - private RequestHandler handler = new PowerLogToolEnabled(); - - @Mock - private Context context; - - @BeforeEach - void setUp() throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { - openMocks(this); - setupContext(); - //Make sure file is cleaned up before running full stack logging regression - FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); - resetLogLevel(Level.INFO); - } - - @Test - void shouldLogInStructuredFormat() throws IOException { - handler.handleRequest("test", context); - - assertThat(Files.lines(Paths.get("target/logfile.json"))) - .hasSize(1) - .allSatisfy(line -> assertThat(parseToMap(line)) - .containsEntry("functionName", "testFunction") - .containsEntry("functionVersion", "1") - .containsEntry("functionMemorySize", "10") - .containsEntry("functionArn", "testArn") - .containsKey("timestamp") - .containsKey("message") - .containsKey("service")); - } - - @Test - void shouldLogWithRFC3339TimestampFormat_WhenLambdaLoggingIsJSON() throws Exception { - // Given: AWS_LAMBDA_LOG_FORMAT=JSON defined in pom.xml - - // When - handler.handleRequest("test", context); - - // Then - assertThat(Files.lines(Paths.get("target/logfile.json"))) - .hasSize(1) - .allSatisfy(line -> assertThat(parseToMap(line)) - .extracting("timestamp", as(InstanceOfAssertFactories.STRING)) - .satisfies(s -> assertThat(hasDateFormat(s, "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")).isTrue())); - } - - private boolean hasDateFormat(String timestamp, String format) { - DateTimeFormatter dtf = DateTimeFormatter.ofPattern(format).withResolverStyle(ResolverStyle.STRICT); - try { - dtf.parse(timestamp); - return true; - } catch (DateTimeParseException e) { - return false; - } - } - - @Test - void shouldModifyLogLevelBasedOnEnvVariable() - throws IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException { - resetLogLevel(Level.DEBUG); - - handler.handleRequest("test", context); - - assertThat(Files.lines(Paths.get("target/logfile.json"))) - .hasSize(2) - .satisfies(line -> - { - assertThat(parseToMap(line.get(0))) - .containsEntry("level", "INFO") - .containsEntry("message", "Test event"); - - assertThat(parseToMap(line.get(1))) - .containsEntry("level", "DEBUG") - .containsEntry("message", "Test debug event"); - }); - } - - @Test - void shouldModifyLogLevelBasedOnSamplingRule() throws IOException { - handler = new PowerLogToolSamplingEnabled(); - - handler.handleRequest("test", context); - - assertThat(Files.lines(Paths.get("target/logfile.json"))) - .hasSize(3) - .satisfies(line -> - { - assertThat(parseToMap(line.get(0))) - .containsEntry("level", "DEBUG") - .containsEntry("loggerName", LambdaLoggingAspect.class.getCanonicalName()); - - assertThat(parseToMap(line.get(1))) - .containsEntry("level", "INFO") - .containsEntry("message", "Test event"); - - assertThat(parseToMap(line.get(2))) - .containsEntry("level", "DEBUG") - .containsEntry("message", "Test debug event"); - }); - } - - private void resetLogLevel(Level level) - throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { - Method resetLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("resetLogLevels", Level.class); - resetLogLevels.setAccessible(true); - resetLogLevels.invoke(null, level); - writeStaticField(LambdaLoggingAspect.class, "LEVEL_AT_INITIALISATION", level, true); - } - - private Map parseToMap(String stringAsJson) { - try { - return new ObjectMapper().readValue(stringAsJson, Map.class); - } catch (JsonProcessingException e) { - fail("Failed parsing logger line " + stringAsJson); - return emptyMap(); - } - } - - private void setupContext() { - when(context.getFunctionName()).thenReturn("testFunction"); - when(context.getInvokedFunctionArn()).thenReturn("testArn"); - when(context.getFunctionVersion()).thenReturn("1"); - when(context.getMemoryLimitInMB()).thenReturn(10); - } -} \ No newline at end of file diff --git a/powertools-logging/src/test/java/org/slf4j/test/OutputChoice.java b/powertools-logging/src/test/java/org/slf4j/test/OutputChoice.java new file mode 100644 index 000000000..9a6d56b81 --- /dev/null +++ b/powertools-logging/src/test/java/org/slf4j/test/OutputChoice.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.slf4j.test; + +import java.io.PrintStream; + +/** + * This class encapsulates the user's choice of output target. + * + * @see ... + */ +class OutputChoice { + + final OutputChoiceType outputChoiceType; + final PrintStream targetPrintStream; + OutputChoice(OutputChoiceType outputChoiceType) { + if (outputChoiceType == OutputChoiceType.FILE) { + throw new IllegalArgumentException(); + } + this.outputChoiceType = outputChoiceType; + if (outputChoiceType == OutputChoiceType.CACHED_SYS_OUT) { + this.targetPrintStream = System.out; + } else if (outputChoiceType == OutputChoiceType.CACHED_SYS_ERR) { + this.targetPrintStream = System.err; + } else { + this.targetPrintStream = null; + } + } + + OutputChoice(PrintStream printStream) { + this.outputChoiceType = OutputChoiceType.FILE; + this.targetPrintStream = printStream; + } + + PrintStream getTargetPrintStream() { + switch (outputChoiceType) { + case SYS_OUT: + return System.out; + case SYS_ERR: + return System.err; + case CACHED_SYS_ERR: + case CACHED_SYS_OUT: + case FILE: + return targetPrintStream; + default: + throw new IllegalArgumentException(); + } + + } + + enum OutputChoiceType { + SYS_OUT, CACHED_SYS_OUT, SYS_ERR, CACHED_SYS_ERR, FILE; + } + +} diff --git a/powertools-logging/src/test/java/org/slf4j/test/TestLogger.java b/powertools-logging/src/test/java/org/slf4j/test/TestLogger.java new file mode 100644 index 000000000..acc635e75 --- /dev/null +++ b/powertools-logging/src/test/java/org/slf4j/test/TestLogger.java @@ -0,0 +1,464 @@ +/** + * Copyright (c) 2004-2022 QOS.ch Sarl (Switzerland) + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.slf4j.test; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.Marker; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; +import org.slf4j.helpers.LegacyAbstractLogger; +import org.slf4j.helpers.MessageFormatter; +import org.slf4j.helpers.NormalizedParameters; +import org.slf4j.spi.LocationAwareLogger; + +/** + *

+ * Simple implementation of {@link Logger} that sends all enabled log messages, + * for all defined loggers, to the console ({@code System.err}). The following + * system properties are supported to configure the behavior of this logger: + * + * + *

    + *
  • org.slf4j.simpleLogger.logFile - The output target which can + * be the path to a file, or the special values "System.out" and + * "System.err". Default is "System.err".
  • + * + *
  • org.slf4j.simpleLogger.cacheOutputStream - If the output + * target is set to "System.out" or "System.err" (see preceding entry), by + * default, logs will be output to the latest value referenced by + * System.out/err variables. By setting this parameter to true, the + * output stream will be cached, i.e. assigned once at initialization time and + * re-used independently of the current value referenced by + * System.out/err.
  • + * + *
  • org.slf4j.simpleLogger.defaultLogLevel - Default log level + * for all instances of SimpleLogger. Must be one of ("trace", "debug", "info", + * "warn", "error" or "off"). If not specified, defaults to "info".
  • + * + *
  • org.slf4j.simpleLogger.log.a.b.c - Logging detail + * level for a SimpleLogger instance named "a.b.c". Right-side value must be one + * of "trace", "debug", "info", "warn", "error" or "off". When a SimpleLogger + * named "a.b.c" is initialized, its level is assigned from this property. If + * unspecified, the level of nearest parent logger will be used, and if none is + * set, then the value specified by + * org.slf4j.simpleLogger.defaultLogLevel will be used.
  • + * + *
  • org.slf4j.simpleLogger.showDateTime - Set to + * true if you want the current date and time to be included in + * output messages. Default is false
  • + * + *
  • org.slf4j.simpleLogger.dateTimeFormat - The date and time + * format to be used in the output messages. The pattern describing the date and + * time format is defined by + * SimpleDateFormat. If the format is not specified or is + * invalid, the number of milliseconds since start up will be output.
  • + * + *
  • org.slf4j.simpleLogger.showThreadName -Set to + * true if you want to output the current thread name. Defaults to + * true.
  • + * + *
  • (since version 1.7.33 and 2.0.0-alpha6) org.slf4j.simpleLogger.showThreadId - + * If you would like to output the current thread id, then set to + * true. Defaults to false.
  • + * + *
  • org.slf4j.simpleLogger.showLogName - Set to + * true if you want the Logger instance name to be included in + * output messages. Defaults to true.
  • + * + *
  • org.slf4j.simpleLogger.showShortLogName - Set to + * true if you want the last component of the name to be included + * in output messages. Defaults to false.
  • + * + *
  • org.slf4j.simpleLogger.levelInBrackets - Should the level + * string be output in brackets? Defaults to false.
  • + * + *
  • org.slf4j.simpleLogger.warnLevelString - The string value + * output for the warn level. Defaults to WARN.
  • + * + *
+ * + *

+ * In addition to looking for system properties with the names specified above, + * this implementation also checks for a class loader resource named + * "simplelogger.properties", and includes any matching definitions + * from this resource (if it exists). + * + * + *

+ * With no configuration, the default output includes the relative time in + * milliseconds, thread name, the level, logger name, and the message followed + * by the line separator for the host. In log4j terms it amounts to the "%r [%t] + * %level %logger - %m%n" pattern. + * + *

+ * Sample output follows. + * + * + *

+ * 176 [main] INFO examples.Sort - Populating an array of 2 elements in reverse order.
+ * 225 [main] INFO examples.SortAlgo - Entered the sort method.
+ * 304 [main] INFO examples.SortAlgo - Dump of integer array:
+ * 317 [main] INFO examples.SortAlgo - Element [0] = 0
+ * 331 [main] INFO examples.SortAlgo - Element [1] = 1
+ * 343 [main] INFO examples.Sort - The next log statement should be an error message.
+ * 346 [main] ERROR examples.SortAlgo - Tried to dump an uninitialized array.
+ *   at org.log4j.examples.SortAlgo.dump(SortAlgo.java:58)
+ *   at org.log4j.examples.Sort.main(Sort.java:64)
+ * 467 [main] INFO  examples.Sort - Exiting main method.
+ * 
+ * + *

+ * This implementation is heavily inspired by + * Apache Commons Logging's + * SimpleLog. + * + * + * @author Ceki Gülcü + * @author Scott Sanders + * @author Rod Waldhoff + * @author Robert Burrell Donkin + * @author Cédrik LIME + */ +public class TestLogger extends LegacyAbstractLogger { + + /** + * All system properties used by SimpleLogger start with this + * prefix + */ + public static final String SYSTEM_PREFIX = "org.slf4j.simpleLogger."; + public static final String LOG_KEY_PREFIX = TestLogger.SYSTEM_PREFIX + "log."; + public static final String CACHE_OUTPUT_STREAM_STRING_KEY = TestLogger.SYSTEM_PREFIX + "cacheOutputStream"; + public static final String WARN_LEVEL_STRING_KEY = TestLogger.SYSTEM_PREFIX + "warnLevelString"; + public static final String LEVEL_IN_BRACKETS_KEY = TestLogger.SYSTEM_PREFIX + "levelInBrackets"; + public static final String LOG_FILE_KEY = TestLogger.SYSTEM_PREFIX + "logFile"; + public static final String SHOW_SHORT_LOG_NAME_KEY = TestLogger.SYSTEM_PREFIX + "showShortLogName"; + public static final String SHOW_LOG_NAME_KEY = TestLogger.SYSTEM_PREFIX + "showLogName"; + public static final String SHOW_THREAD_NAME_KEY = TestLogger.SYSTEM_PREFIX + "showThreadName"; + public static final String SHOW_THREAD_ID_KEY = TestLogger.SYSTEM_PREFIX + "showThreadId"; + public static final String DATE_TIME_FORMAT_KEY = TestLogger.SYSTEM_PREFIX + "dateTimeFormat"; + public static final String SHOW_DATE_TIME_KEY = TestLogger.SYSTEM_PREFIX + "showDateTime"; + public static final String DEFAULT_LOG_LEVEL_KEY = TestLogger.SYSTEM_PREFIX + "defaultLogLevel"; + protected static final int LOG_LEVEL_TRACE = LocationAwareLogger.TRACE_INT; + protected static final int LOG_LEVEL_DEBUG = LocationAwareLogger.DEBUG_INT; + protected static final int LOG_LEVEL_INFO = LocationAwareLogger.INFO_INT; + protected static final int LOG_LEVEL_WARN = LocationAwareLogger.WARN_INT; + protected static final int LOG_LEVEL_ERROR = LocationAwareLogger.ERROR_INT; + // The OFF level can only be used in configuration files to disable logging. + // It has + // no printing method associated with it in o.s.Logger interface. + protected static final int LOG_LEVEL_OFF = LOG_LEVEL_ERROR + 10; + static final String TID_PREFIX = "tid="; + static final TestLoggerConfiguration CONFIG_PARAMS = new TestLoggerConfiguration(); + private static final long serialVersionUID = -632788891211436180L; + private static final long START_TIME = System.currentTimeMillis(); + static char SP = ' '; + private static boolean INITIALIZED = false; + /** The current log level */ + protected int currentLogLevel = LOG_LEVEL_INFO; + /** The short name of this simple log instance */ + private transient String shortLogName = null; + + // used for test purpose + private Object[] arguments; + + /** + * Package access allows only {@link TestLoggerFactory} to instantiate + * SimpleLogger instances. + */ + TestLogger(String name) { + this.name = name; + + String levelString = recursivelyComputeLevelString(); + if (levelString != null) { + this.currentLogLevel = TestLoggerConfiguration.stringToLevel(levelString); + } else { + this.currentLogLevel = CONFIG_PARAMS.defaultLogLevel; + } + } + + static void lazyInit() { + if (INITIALIZED) { + return; + } + INITIALIZED = true; + init(); + } + + // external software might be invoking this method directly. Do not rename + // or change its semantics. + static void init() { + CONFIG_PARAMS.init(); + } + + public int getLogLevel() { + return currentLogLevel; + } + + public void setLogLevel(String levelString) { + this.currentLogLevel = TestLoggerConfiguration.stringToLevel(levelString); + } + + String recursivelyComputeLevelString() { + String tempName = name; + String levelString = null; + int indexOfLastDot = tempName.length(); + while ((levelString == null) && (indexOfLastDot > -1)) { + tempName = tempName.substring(0, indexOfLastDot); + levelString = CONFIG_PARAMS.getStringProperty(TestLogger.LOG_KEY_PREFIX + tempName, null); + indexOfLastDot = String.valueOf(tempName).lastIndexOf("."); + } + return levelString; + } + + /** + * To avoid intermingling of log messages and associated stack traces, the two + * operations are done in a synchronized block. + * + * @param buf + * @param t + */ + void write(StringBuilder buf, Throwable t) { + PrintStream targetStream = CONFIG_PARAMS.outputChoice.getTargetPrintStream(); + + synchronized (CONFIG_PARAMS) { + targetStream.println(buf.toString()); + writeThrowable(t, targetStream); + targetStream.flush(); + } + + } + + protected void writeThrowable(Throwable t, PrintStream targetStream) { + if (t != null) { + t.printStackTrace(targetStream); + } + } + + private String getFormattedDate() { + Date now = new Date(); + String dateText; + synchronized (CONFIG_PARAMS.dateFormatter) { + dateText = CONFIG_PARAMS.dateFormatter.format(now); + } + return dateText; + } + + private String computeShortName() { + return name.substring(name.lastIndexOf(".") + 1); + } + + // /** + // * For formatted messages, first substitute arguments and then log. + // * + // * @param level + // * @param format + // * @param arg1 + // * @param arg2 + // */ + // private void formatAndLog(int level, String format, Object arg1, Object arg2) { + // if (!isLevelEnabled(level)) { + // return; + // } + // FormattingTuple tp = MessageFormatter.format(format, arg1, arg2); + // log(level, tp.getMessage(), tp.getThrowable()); + // } + + // /** + // * For formatted messages, first substitute arguments and then log. + // * + // * @param level + // * @param format + // * @param arguments + // * a list of 3 ore more arguments + // */ + // private void formatAndLog(int level, String format, Object... arguments) { + // if (!isLevelEnabled(level)) { + // return; + // } + // FormattingTuple tp = MessageFormatter.arrayFormat(format, arguments); + // log(level, tp.getMessage(), tp.getThrowable()); + // } + + /** + * Is the given log level currently enabled? + * + * @param logLevel is this level enabled? + * @return whether the logger is enabled for the given level + */ + protected boolean isLevelEnabled(int logLevel) { + // log level are numerically ordered so can use simple numeric + // comparison + return (logLevel >= currentLogLevel); + } + + /** Are {@code trace} messages currently enabled? */ + public boolean isTraceEnabled() { + return isLevelEnabled(LOG_LEVEL_TRACE); + } + + /** Are {@code debug} messages currently enabled? */ + public boolean isDebugEnabled() { + return isLevelEnabled(LOG_LEVEL_DEBUG); + } + + /** Are {@code info} messages currently enabled? */ + public boolean isInfoEnabled() { + return isLevelEnabled(LOG_LEVEL_INFO); + } + + /** Are {@code warn} messages currently enabled? */ + public boolean isWarnEnabled() { + return isLevelEnabled(LOG_LEVEL_WARN); + } + + /** Are {@code error} messages currently enabled? */ + public boolean isErrorEnabled() { + return isLevelEnabled(LOG_LEVEL_ERROR); + } + + /** + * SimpleLogger's implementation of + * {@link org.slf4j.helpers.AbstractLogger#handleNormalizedLoggingCall(Level, Marker, String, Object[], Throwable) AbstractLogger#handleNormalizedLoggingCall} + * } + * + * @param level the SLF4J level for this event + * @param marker The marker to be used for this event, may be null. + * @param messagePattern The message pattern which will be parsed and formatted + * @param arguments the array of arguments to be formatted, may be null + * @param throwable The exception whose stack trace should be logged, may be null + */ + @Override + protected void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments, + Throwable throwable) { + + List markers = null; + + if (marker != null) { + markers = new ArrayList<>(); + markers.add(marker); + } + + innerHandleNormalizedLoggingCall(level, markers, messagePattern, arguments, throwable); + } + + private void innerHandleNormalizedLoggingCall(Level level, List markers, String messagePattern, + Object[] arguments, Throwable t) { + + this.arguments = arguments; + + StringBuilder buf = new StringBuilder(32); + + // Append date-time if so configured + if (CONFIG_PARAMS.showDateTime) { + if (CONFIG_PARAMS.dateFormatter != null) { + buf.append(getFormattedDate()); + buf.append(SP); + } else { + buf.append(System.currentTimeMillis() - START_TIME); + buf.append(SP); + } + } + + // Append current thread name if so configured + if (CONFIG_PARAMS.showThreadName) { + buf.append('['); + buf.append(Thread.currentThread().getName()); + buf.append("] "); + } + + if (CONFIG_PARAMS.showThreadId) { + buf.append(TID_PREFIX); + buf.append(Thread.currentThread().getId()); + buf.append(SP); + } + + if (CONFIG_PARAMS.levelInBrackets) { + buf.append('['); + } + + // Append a readable representation of the log level + String levelStr = level.name(); + buf.append(levelStr); + if (CONFIG_PARAMS.levelInBrackets) { + buf.append(']'); + } + buf.append(SP); + + // Append the name of the log instance if so configured + if (CONFIG_PARAMS.showShortLogName) { + if (shortLogName == null) { + shortLogName = computeShortName(); + } + buf.append(String.valueOf(shortLogName)).append(" - "); + } else if (CONFIG_PARAMS.showLogName) { + buf.append(String.valueOf(name)).append(" - "); + } + + if (markers != null) { + buf.append(SP); + for (Marker marker : markers) { + buf.append(marker.getName()).append(SP); + } + } + + String formattedMessage = MessageFormatter.basicArrayFormat(messagePattern, arguments); + + // Append the message + buf.append(formattedMessage); + + write(buf, t); + } + + public void log(LoggingEvent event) { + int levelInt = event.getLevel().toInt(); + + if (!isLevelEnabled(levelInt)) { + return; + } + + NormalizedParameters np = NormalizedParameters.normalize(event); + + innerHandleNormalizedLoggingCall(event.getLevel(), event.getMarkers(), np.getMessage(), np.getArguments(), + event.getThrowable()); + } + + @Override + protected String getFullyQualifiedCallerName() { + return null; + } + + public Object[] getArguments() { + return arguments; + } + + public void clearArguments() { + arguments = null; + } +} diff --git a/powertools-logging/src/test/java/org/slf4j/test/TestLoggerConfiguration.java b/powertools-logging/src/test/java/org/slf4j/test/TestLoggerConfiguration.java new file mode 100644 index 000000000..7601dbfde --- /dev/null +++ b/powertools-logging/src/test/java/org/slf4j/test/TestLoggerConfiguration.java @@ -0,0 +1,204 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.slf4j.test; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Properties; +import org.slf4j.helpers.Util; + +/** + * This class holds configuration values for {@link TestLogger}. The + * values are computed at runtime. See {@link TestLogger} documentation for + * more information. + * + * + * @author Ceki Gülcü + * @author Scott Sanders + * @author Rod Waldhoff + * @author Robert Burrell Donkin + * @author Cédrik LIME + * + * @since 1.7.25 + */ +public class TestLoggerConfiguration { + + final static boolean SHOW_LOG_NAME_DEFAULT = true; + private static final String CONFIGURATION_FILE = "testlogger.properties"; + private static final boolean SHOW_DATE_TIME_DEFAULT = false; + private static final String DATE_TIME_FORMAT_STR_DEFAULT = null; + private static final boolean SHOW_THREAD_NAME_DEFAULT = true; + /** + * See https://jira.qos.ch/browse/SLF4J-499 + * @since 1.7.33 and 2.0.0-alpha6 + */ + private static final boolean SHOW_THREAD_ID_DEFAULT = false; + private static final boolean SHOW_SHORT_LOG_NAME_DEFAULT = false; + private static final boolean LEVEL_IN_BRACKETS_DEFAULT = false; + private static final String LOG_FILE_DEFAULT = "System.err"; + private static final boolean CACHE_OUTPUT_STREAM_DEFAULT = false; + private static final String WARN_LEVELS_STRING_DEFAULT = "WARN"; + static int DEFAULT_LOG_LEVEL_DEFAULT = TestLogger.LOG_LEVEL_INFO; + private static String dateTimeFormatStr = DATE_TIME_FORMAT_STR_DEFAULT; + private final Properties properties = new Properties(); + int defaultLogLevel = DEFAULT_LOG_LEVEL_DEFAULT; + boolean showDateTime = SHOW_DATE_TIME_DEFAULT; + DateFormat dateFormatter = null; + boolean showThreadName = SHOW_THREAD_NAME_DEFAULT; + boolean showThreadId = SHOW_THREAD_ID_DEFAULT; + boolean showLogName = SHOW_LOG_NAME_DEFAULT; + boolean showShortLogName = SHOW_SHORT_LOG_NAME_DEFAULT; + boolean levelInBrackets = LEVEL_IN_BRACKETS_DEFAULT; + OutputChoice outputChoice = null; + String warnLevelString = WARN_LEVELS_STRING_DEFAULT; + private String logFile = LOG_FILE_DEFAULT; + private boolean cacheOutputStream = CACHE_OUTPUT_STREAM_DEFAULT; + + static int stringToLevel(String levelStr) { + if ("trace".equalsIgnoreCase(levelStr)) { + return TestLogger.LOG_LEVEL_TRACE; + } else if ("debug".equalsIgnoreCase(levelStr)) { + return TestLogger.LOG_LEVEL_DEBUG; + } else if ("info".equalsIgnoreCase(levelStr)) { + return TestLogger.LOG_LEVEL_INFO; + } else if ("warn".equalsIgnoreCase(levelStr)) { + return TestLogger.LOG_LEVEL_WARN; + } else if ("error".equalsIgnoreCase(levelStr)) { + return TestLogger.LOG_LEVEL_ERROR; + } else if ("off".equalsIgnoreCase(levelStr)) { + return TestLogger.LOG_LEVEL_OFF; + } + // assume INFO by default + return TestLogger.LOG_LEVEL_INFO; + } + + private static OutputChoice computeOutputChoice(String logFile, boolean cacheOutputStream) { + if ("System.err".equalsIgnoreCase(logFile)) { + if (cacheOutputStream) { + return new OutputChoice(OutputChoice.OutputChoiceType.CACHED_SYS_ERR); + } else { + return new OutputChoice(OutputChoice.OutputChoiceType.SYS_ERR); + } + } else if ("System.out".equalsIgnoreCase(logFile)) { + if (cacheOutputStream) { + return new OutputChoice(OutputChoice.OutputChoiceType.CACHED_SYS_OUT); + } else { + return new OutputChoice(OutputChoice.OutputChoiceType.SYS_OUT); + } + } else { + try { + FileOutputStream fos = new FileOutputStream(logFile); + PrintStream printStream = new PrintStream(fos); + return new OutputChoice(printStream); + } catch (FileNotFoundException e) { + Util.report("Could not open [" + logFile + "]. Defaulting to System.err", e); + return new OutputChoice(OutputChoice.OutputChoiceType.SYS_ERR); + } + } + } + + void init() { + loadProperties(); + + String defaultLogLevelString = getStringProperty(TestLogger.DEFAULT_LOG_LEVEL_KEY, null); + if (defaultLogLevelString != null) { + defaultLogLevel = stringToLevel(defaultLogLevelString); + } + + showLogName = getBooleanProperty(TestLogger.SHOW_LOG_NAME_KEY, TestLoggerConfiguration.SHOW_LOG_NAME_DEFAULT); + showShortLogName = getBooleanProperty(TestLogger.SHOW_SHORT_LOG_NAME_KEY, SHOW_SHORT_LOG_NAME_DEFAULT); + showDateTime = getBooleanProperty(TestLogger.SHOW_DATE_TIME_KEY, SHOW_DATE_TIME_DEFAULT); + showThreadName = getBooleanProperty(TestLogger.SHOW_THREAD_NAME_KEY, SHOW_THREAD_NAME_DEFAULT); + showThreadId = getBooleanProperty(TestLogger.SHOW_THREAD_ID_KEY, SHOW_THREAD_ID_DEFAULT); + dateTimeFormatStr = getStringProperty(TestLogger.DATE_TIME_FORMAT_KEY, DATE_TIME_FORMAT_STR_DEFAULT); + levelInBrackets = getBooleanProperty(TestLogger.LEVEL_IN_BRACKETS_KEY, LEVEL_IN_BRACKETS_DEFAULT); + warnLevelString = getStringProperty(TestLogger.WARN_LEVEL_STRING_KEY, WARN_LEVELS_STRING_DEFAULT); + + logFile = getStringProperty(TestLogger.LOG_FILE_KEY, logFile); + + cacheOutputStream = getBooleanProperty(TestLogger.CACHE_OUTPUT_STREAM_STRING_KEY, CACHE_OUTPUT_STREAM_DEFAULT); + outputChoice = computeOutputChoice(logFile, cacheOutputStream); + + if (dateTimeFormatStr != null) { + try { + dateFormatter = new SimpleDateFormat(dateTimeFormatStr); + } catch (IllegalArgumentException e) { + Util.report("Bad date format in " + CONFIGURATION_FILE + "; will output relative time", e); + } + } + } + + private void loadProperties() { + // Add props from the resource testlogger.properties + InputStream in = AccessController.doPrivileged((PrivilegedAction) () -> { + ClassLoader threadCL = Thread.currentThread().getContextClassLoader(); + if (threadCL != null) { + return threadCL.getResourceAsStream(CONFIGURATION_FILE); + } else { + return ClassLoader.getSystemResourceAsStream(CONFIGURATION_FILE); + } + }); + if (null != in) { + try { + properties.load(in); + } catch (java.io.IOException e) { + // ignored + } finally { + try { + in.close(); + } catch (java.io.IOException e) { + // ignored + } + } + } + } + + String getStringProperty(String name, String defaultValue) { + String prop = getStringProperty(name); + return (prop == null) ? defaultValue : prop; + } + + boolean getBooleanProperty(String name, boolean defaultValue) { + String prop = getStringProperty(name); + return (prop == null) ? defaultValue : "true".equalsIgnoreCase(prop); + } + + String getStringProperty(String name) { + String prop = null; + try { + prop = System.getProperty(name); + } catch (SecurityException e) { + ; // Ignore + } + return (prop == null) ? properties.getProperty(name) : prop; + } + +} diff --git a/powertools-logging/src/test/java/org/slf4j/test/TestLoggerFactory.java b/powertools-logging/src/test/java/org/slf4j/test/TestLoggerFactory.java new file mode 100644 index 000000000..d597b5706 --- /dev/null +++ b/powertools-logging/src/test/java/org/slf4j/test/TestLoggerFactory.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.slf4j.test; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; + +/** + * An implementation of {@link ILoggerFactory} which always returns + * {@link TestLogger} instances. + * + * @author Ceki Gülcü + */ +public class TestLoggerFactory implements ILoggerFactory { + + ConcurrentMap loggerMap; + + public TestLoggerFactory() { + loggerMap = new ConcurrentHashMap<>(); + TestLogger.lazyInit(); + } + + public Map getLoggers() { + return loggerMap; + } + + /** + * Return an appropriate {@link TestLogger} instance by name. + */ + public Logger getLogger(String name) { + Logger simpleLogger = loggerMap.get(name); + if (simpleLogger != null) { + return simpleLogger; + } else { + Logger newInstance = new TestLogger(name); + Logger oldInstance = loggerMap.putIfAbsent(name, newInstance); + return oldInstance == null ? newInstance : oldInstance; + } + } + + /** + * Clear the internal logger cache. + * + * This method is intended to be called by classes (in the same package) for + * testing purposes. This method is internal. It can be modified, renamed or + * removed at any time without notice. + * + * You are strongly discouraged from calling this method in production code. + */ + void reset() { + loggerMap.clear(); + } +} diff --git a/powertools-logging/src/test/java/org/slf4j/test/TestServiceProvider.java b/powertools-logging/src/test/java/org/slf4j/test/TestServiceProvider.java new file mode 100644 index 000000000..357360d1e --- /dev/null +++ b/powertools-logging/src/test/java/org/slf4j/test/TestServiceProvider.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.slf4j.test; + +import org.slf4j.ILoggerFactory; +import org.slf4j.IMarkerFactory; +import org.slf4j.helpers.BasicMDCAdapter; +import org.slf4j.helpers.BasicMarkerFactory; +import org.slf4j.spi.MDCAdapter; +import org.slf4j.spi.SLF4JServiceProvider; + +/** + * Copy of the org.slf4j.simple.SimpleServiceProvider, replacing the NoOpMDCAdapter with a BasicMDCAdapter to test the MDC + */ +public class TestServiceProvider implements SLF4JServiceProvider { + /** + * Declare the version of the SLF4J API this implementation is compiled against. + * The value of this field is modified with each major release. + */ + // to avoid constant folding by the compiler, this field must *not* be final + public static String REQUESTED_API_VERSION = "2.0.99"; // !final + + private ILoggerFactory loggerFactory; + private IMarkerFactory markerFactory; + private MDCAdapter mdcAdapter; + + public ILoggerFactory getLoggerFactory() { + return loggerFactory; + } + + @Override + public IMarkerFactory getMarkerFactory() { + return markerFactory; + } + + @Override + public MDCAdapter getMDCAdapter() { + return mdcAdapter; + } + + @Override + public String getRequestedApiVersion() { + return REQUESTED_API_VERSION; + } + + @Override + public void initialize() { + + loggerFactory = new TestLoggerFactory(); + markerFactory = new BasicMarkerFactory(); + mdcAdapter = new BasicMDCAdapter(); + } + +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/LoggingUtilsTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/LoggingUtilsTest.java deleted file mode 100644 index 8889fb93c..000000000 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/LoggingUtilsTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.logging; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.HashMap; -import java.util.Map; -import org.apache.logging.log4j.ThreadContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - - -class LoggingUtilsTest { - - @BeforeEach - void setUp() { - ThreadContext.clearAll(); - } - - @Test - void shouldSetCustomKeyOnThreadContext() { - LoggingUtils.appendKey("test", "value"); - - assertThat(ThreadContext.getImmutableContext()) - .hasSize(1) - .containsEntry("test", "value"); - } - - @Test - void shouldSetCustomKeyAsMapOnThreadContext() { - Map customKeys = new HashMap<>(); - customKeys.put("test", "value"); - customKeys.put("test1", "value1"); - - LoggingUtils.appendKeys(customKeys); - - assertThat(ThreadContext.getImmutableContext()) - .hasSize(2) - .containsEntry("test", "value") - .containsEntry("test1", "value1"); - } - - @Test - void shouldRemoveCustomKeyOnThreadContext() { - LoggingUtils.appendKey("test", "value"); - - assertThat(ThreadContext.getImmutableContext()) - .hasSize(1) - .containsEntry("test", "value"); - - LoggingUtils.removeKey("test"); - - assertThat(ThreadContext.getImmutableContext()) - .isEmpty(); - } - - @Test - void shouldRemoveCustomKeysOnThreadContext() { - Map customKeys = new HashMap<>(); - customKeys.put("test", "value"); - customKeys.put("test1", "value1"); - - LoggingUtils.appendKeys(customKeys); - - assertThat(ThreadContext.getImmutableContext()) - .hasSize(2) - .containsEntry("test", "value") - .containsEntry("test1", "value1"); - - LoggingUtils.removeKeys("test", "test1"); - - assertThat(ThreadContext.getImmutableContext()) - .isEmpty(); - } -} \ No newline at end of file diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/argument/StructuredArgumentsTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/argument/StructuredArgumentsTest.java new file mode 100644 index 000000000..b478ac0f0 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/argument/StructuredArgumentsTest.java @@ -0,0 +1,157 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.argument; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import software.amazon.lambda.powertools.logging.internal.JsonSerializer; +import software.amazon.lambda.powertools.logging.model.Basket; +import software.amazon.lambda.powertools.logging.model.Product; + +class StructuredArgumentsTest { + private StringBuilder sb; + private JsonSerializer serializer; + + @BeforeEach + void setUp() { + sb = new StringBuilder(); + serializer = new JsonSerializer(sb); + } + + @Test + void keyValueArgument() throws IOException { + // GIVEN + Basket basket = new Basket(); + basket.add(new Product(42, "Nintendo DS", 299.45)); + basket.add(new Product(98, "Playstation 5", 499.99)); + + // WHEN + StructuredArgument argument = StructuredArguments.entry("basket", basket); + argument.writeTo(serializer); + + // THEN + assertThat(sb.toString()).hasToString( + "\"basket\":{\"products\":[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]}"); + assertThat(argument.toString()).hasToString( + "basket=Basket{products=[Product{id=42, name='Nintendo DS', price=299.45}, Product{id=98, name='Playstation 5', price=499.99}]}"); + } + + @Test + void mapArgument() throws IOException { + // GIVEN + Map catalog = new HashMap<>(); + catalog.put("nds", new Product(42, "Nintendo DS", 299.45)); + catalog.put("ps5", new Product(98, "Playstation 5", 499.99)); + + // WHEN + StructuredArgument argument = StructuredArguments.entries(catalog); + argument.writeTo(serializer); + + // THEN + assertThat(sb.toString()) + .contains("\"nds\":{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},") + .contains("\"ps5\":{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}"); + assertThat(argument.toString()) + .contains("nds=Product{id=42, name='Nintendo DS', price=299.45}") + .contains("ps5=Product{id=98, name='Playstation 5', price=499.99}"); + } + + @Test + void emptyMapArgument() throws IOException { + // GIVEN + Map catalog = new HashMap<>(); + + // WHEN + assertNull(StructuredArguments.entries(null)); + StructuredArgument argument = StructuredArguments.entries(catalog); + argument.writeTo(serializer); + + // THEN + assertThat(sb.toString()).isEmpty(); + assertThat(argument.toString()).hasToString("{}"); + } + + @Test + void arrayArgument() throws IOException { + // GIVEN + Product[] products = new Product[] { + new Product(42, "Nintendo DS", 299.45), + new Product(98, "Playstation 5", 499.99) + }; + + // WHEN + StructuredArgument argument = StructuredArguments.array("products", products); + argument.writeTo(serializer); + + // THEN + assertThat(sb.toString()).contains( + "\"products\":[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]"); + assertThat(argument.toString()).contains( + "products=[Product{id=42, name='Nintendo DS', price=299.45}, Product{id=98, name='Playstation 5', price=499.99}]"); + } + + @Test + void jsonArgument() throws IOException { + // GIVEN + String rawJson = "{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}"; + + // WHEN + StructuredArgument argument = StructuredArguments.json("product", rawJson); + argument.writeTo(serializer); + + // THEN + assertThat(sb.toString()).contains("\"product\":{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}"); + assertThat(argument.toString()).contains("product={\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}"); + } + + @Test + void reservedKeywordArgumentIgnored() throws IOException { + // GIVEN + Basket basket = new Basket(); + basket.add(new Product(42, "Nintendo DS", 299.45)); + basket.add(new Product(98, "Playstation 5", 499.99)); + Product[] products = new Product[] { + new Product(42, "Nintendo DS", 299.45), + new Product(98, "Playstation 5", 499.99) + }; + String rawJson = "{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}"; + Map catalog = new HashMap<>(); + catalog.put("nds", new Product(42, "Nintendo DS", 299.45)); + catalog.put("message", new Product(98, "Playstation 5", 499.99)); + + // THEN + assertNull(StructuredArguments.entry("message", basket)); + assertNull(StructuredArguments.array("message", products)); + assertNull(StructuredArguments.json("message", rawJson)); + + StructuredArgument mapArg = StructuredArguments.entries(catalog); + mapArg.writeTo(serializer); + assertThat(sb.toString()) + .contains("\"nds\":{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}"); + assertThat(sb.toString()).doesNotContain("message"); + assertThat(mapArg.toString()) + .contains("nds=Product{id=42, name='Nintendo DS', price=299.45}"); + assertThat(mapArg.toString()).doesNotContain("message"); + } + +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledWithCustomMapper.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledWithCustomMapper.java deleted file mode 100644 index 838de1216..000000000 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledWithCustomMapper.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.logging.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; -import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; - -public class PowerToolLogEventEnabledWithCustomMapper implements RequestHandler { - - static { - ObjectMapper objectMapper = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addSerializer(S3EventNotification.class, new S3EventNotificationSerializer()); - objectMapper.registerModule(module); - LoggingUtils.defaultObjectMapper(objectMapper); - } - - @Logging(logEvent = true) - @Override - public Object handleRequest(S3EventNotification input, Context context) { - return null; - } - - static class S3EventNotificationSerializer extends StdSerializer { - - public S3EventNotificationSerializer() { - this(null); - } - - public S3EventNotificationSerializer(Class t) { - super(t); - } - - @Override - public void serialize(S3EventNotification o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) - throws IOException { - jsonGenerator.writeStartObject(); - jsonGenerator.writeStringField("eventSource", o.getRecords().get(0).getEventSource()); - jsonGenerator.writeEndObject(); - } - } -} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.java index a32e3e06e..065d4c5b0 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.java @@ -14,17 +14,17 @@ package software.amazon.lambda.powertools.logging.handlers; -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.APPLICATION_LOAD_BALANCER; +import static software.amazon.lambda.powertools.logging.CorrelationIdPaths.APPLICATION_LOAD_BALANCER; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; public class PowertoolsLogAlbCorrelationId implements RequestHandler { - private final Logger LOG = LogManager.getLogger(PowertoolsLogAlbCorrelationId.class); + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogAlbCorrelationId.class); @Override @Logging(correlationIdPath = APPLICATION_LOAD_BALANCER) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayHttpApiCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogApiGatewayHttpApiCorrelationId.java similarity index 78% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayHttpApiCorrelationId.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogApiGatewayHttpApiCorrelationId.java index 54d87d5cb..922a09f13 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayHttpApiCorrelationId.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogApiGatewayHttpApiCorrelationId.java @@ -14,17 +14,17 @@ package software.amazon.lambda.powertools.logging.handlers; -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.API_GATEWAY_HTTP; +import static software.amazon.lambda.powertools.logging.CorrelationIdPaths.API_GATEWAY_HTTP; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; -public class PowerLogToolApiGatewayHttpApiCorrelationId implements RequestHandler { - private final Logger LOG = LogManager.getLogger(PowerLogToolApiGatewayHttpApiCorrelationId.class); +public class PowertoolsLogApiGatewayHttpApiCorrelationId implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogApiGatewayHttpApiCorrelationId.class); @Override @Logging(correlationIdPath = API_GATEWAY_HTTP) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayRestApiCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogApiGatewayRestApiCorrelationId.java similarity index 78% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayRestApiCorrelationId.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogApiGatewayRestApiCorrelationId.java index 2b6e5a8d4..7271e1d24 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayRestApiCorrelationId.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogApiGatewayRestApiCorrelationId.java @@ -14,17 +14,17 @@ package software.amazon.lambda.powertools.logging.handlers; -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.API_GATEWAY_REST; +import static software.amazon.lambda.powertools.logging.CorrelationIdPaths.API_GATEWAY_REST; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; -public class PowerLogToolApiGatewayRestApiCorrelationId implements RequestHandler { - private final Logger LOG = LogManager.getLogger(PowerLogToolApiGatewayRestApiCorrelationId.class); +public class PowertoolsLogApiGatewayRestApiCorrelationId implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogApiGatewayRestApiCorrelationId.class); @Override @Logging(correlationIdPath = API_GATEWAY_REST) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAppSyncCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAppSyncCorrelationId.java new file mode 100644 index 000000000..fbe2bc89b --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAppSyncCorrelationId.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.handlers; + +import static software.amazon.lambda.powertools.logging.CorrelationIdPaths.APPSYNC_RESOLVER; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogAppSyncCorrelationId implements RequestStreamHandler { + + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogAppSyncCorrelationId.class); + + @Override + @Logging(correlationIdPath = APPSYNC_RESOLVER) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + LOG.info("Test event"); + } +} \ No newline at end of file diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogClearState.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogClearState.java new file mode 100644 index 000000000..cb7fbb408 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogClearState.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogClearState implements RequestHandler, Object> { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogClearState.class); + + @Override + @Logging(clearState = true) + public Object handleRequest(Map input, Context context) { + MDC.put("mySuperSecret", input.get("mySuperSecret")); + LOG.info("Test event"); + return null; + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogDisabled.java similarity index 91% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabled.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogDisabled.java index 48a2e3b81..54e887e40 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogDisabled.java @@ -17,7 +17,7 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -public class PowerToolDisabled implements RequestHandler { +public class PowertoolsLogDisabled implements RequestHandler { @Override public Object handleRequest(Object input, Context context) { diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabledForStream.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogDisabledForStream.java similarity index 92% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabledForStream.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogDisabledForStream.java index 7f93145c7..7f7418ed6 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabledForStream.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogDisabledForStream.java @@ -19,7 +19,7 @@ import java.io.InputStream; import java.io.OutputStream; -public class PowerToolDisabledForStream implements RequestStreamHandler { +public class PowertoolsLogDisabledForStream implements RequestStreamHandler { @Override public void handleRequest(InputStream input, OutputStream output, Context context) { diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledWithClearState.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabled.java similarity index 53% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledWithClearState.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabled.java index f21d9f118..aa0a5942c 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledWithClearState.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabled.java @@ -16,23 +16,41 @@ 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; -public class PowertoolsLogEnabledWithClearState implements RequestHandler { - private static final Logger LOG = LogManager.getLogger(PowertoolsLogEnabledWithClearState.class); - public static int COUNT = 1; +public class PowertoolsLogEnabled implements RequestHandler { + private static final Logger LOG = LoggerFactory.getLogger(PowertoolsLogEnabled.class); + private final boolean throwError; + + public PowertoolsLogEnabled(boolean throwError) { + this.throwError = throwError; + } + + public PowertoolsLogEnabled() { + this(false); + } @Override - @Logging(clearState = true) + @Logging public Object handleRequest(Object input, Context context) { - if (COUNT == 1) { - LoggingUtils.appendKey("TestKey", "TestValue"); + if (throwError) { + throw new RuntimeException("Something went wrong"); } + LOG.error("Test error event"); + LOG.warn("Test warn event"); LOG.info("Test event"); - COUNT++; - return null; + LOG.debug("Test debug event"); + return "Bonjour le monde"; + } + + @Logging + public void anotherMethod() { + System.out.println("test"); + } + + public static Logger getLogger() { + return LOG; } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledForStream.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledForStream.java similarity index 93% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledForStream.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledForStream.java index 83a370437..c95627302 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledForStream.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledForStream.java @@ -20,7 +20,7 @@ import java.io.OutputStream; import software.amazon.lambda.powertools.logging.Logging; -public class PowerLogToolEnabledForStream implements RequestStreamHandler { +public class PowertoolsLogEnabledForStream implements RequestStreamHandler { @Logging @Override diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogError.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogError.java new file mode 100644 index 000000000..a6b1ed915 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogError.java @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogError implements RequestHandler { + + @Override + @Logging(logError = true) + public Object handleRequest(Object input, Context context) { + throw new UnsupportedOperationException("This is an error"); + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEvent.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEvent.java new file mode 100644 index 000000000..c83692e95 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEvent.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogEvent implements RequestHandler { + + private static final Logger logger = LoggerFactory.getLogger(PowertoolsLogEvent.class); + + @Override + @Logging(logEvent = true) + public Object handleRequest(Object input, Context context) { + return null; + } + + public static Logger getLogger() { + return logger; + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventBridgeCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventBridgeCorrelationId.java index 53e06cb2e..04d56d38c 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventBridgeCorrelationId.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventBridgeCorrelationId.java @@ -14,20 +14,20 @@ package software.amazon.lambda.powertools.logging.handlers; -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.EVENT_BRIDGE; +import static software.amazon.lambda.powertools.logging.CorrelationIdPaths.EVENT_BRIDGE; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; public class PowertoolsLogEventBridgeCorrelationId implements RequestStreamHandler { - private final Logger LOG = LogManager.getLogger(PowertoolsLogEventBridgeCorrelationId.class); + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogEventBridgeCorrelationId.class); @Override @Logging(correlationIdPath = EVENT_BRIDGE) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventEnvVar.java similarity index 71% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventEnvVar.java index df68ea14f..230394bf7 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventEnvVar.java @@ -16,23 +16,21 @@ 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; -public class PowerLogToolEnabled implements RequestHandler { - private final Logger LOG = LogManager.getLogger(PowerLogToolEnabled.class); +public class PowertoolsLogEventEnvVar implements RequestHandler { + + private final Logger logger = LoggerFactory.getLogger(PowertoolsLogEventEnvVar.class); @Override @Logging public Object handleRequest(Object input, Context context) { - LOG.info("Test event"); - LOG.debug("Test debug event"); return null; } - @Logging - public void anotherMethod() { - System.out.println("test"); + public Logger getLogger() { + return logger; } } diff --git a/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/handler/TracingLoggingStreamMessageHandler.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventForStream.java similarity index 65% rename from powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/handler/TracingLoggingStreamMessageHandler.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventForStream.java index 4a60d0949..6e27f47ce 100644 --- a/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/handler/TracingLoggingStreamMessageHandler.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventForStream.java @@ -12,7 +12,7 @@ * */ -package software.amazon.lambda.powertools.testsuite.handler; +package software.amazon.lambda.powertools.logging.handlers; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; @@ -21,16 +21,22 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.tracing.Tracing; -public class TracingLoggingStreamMessageHandler implements RequestStreamHandler { +public class PowertoolsLogEventForStream implements RequestStreamHandler { + + private static final Logger logger = LoggerFactory.getLogger(PowertoolsLogEventForStream.class); - @Logging(logEvent = true) - @Tracing @Override - public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { + @Logging(logEvent = true) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { ObjectMapper mapper = new ObjectMapper(); - mapper.writeValue(output, mapper.readValue(input, Map.class)); + mapper.writeValue(outputStream, mapper.readValue(inputStream, Map.class)); + } + + public static Logger getLogger() { + return logger; } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponse.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponse.java new file mode 100644 index 000000000..e7607d3c9 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponse.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogResponse implements RequestHandler { + private static final Logger logger = LoggerFactory.getLogger(PowertoolsLogResponse.class); + + public static Logger getLogger() { + return logger; + } + + @Override + @Logging(logResponse = true) + public Object handleRequest(Object input, Context context) { + return "Hola mundo"; + } +} diff --git a/examples/powertools-examples-core/cdk/app/src/main/java/helloworld/AppStream.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponseForStream.java similarity index 56% rename from examples/powertools-examples-core/cdk/app/src/main/java/helloworld/AppStream.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponseForStream.java index 401ef8c48..fe591627b 100644 --- a/examples/powertools-examples-core/cdk/app/src/main/java/helloworld/AppStream.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponseForStream.java @@ -12,27 +12,32 @@ * */ -package helloworld; +package software.amazon.lambda.powertools.logging.handlers; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; -public class AppStream implements RequestStreamHandler { - private static final ObjectMapper mapper = new ObjectMapper(); +public class PowertoolsLogResponseForStream implements RequestStreamHandler { + + private static final Logger logger = LoggerFactory.getLogger(PowertoolsLogResponseForStream.class); @Override - @Logging(logEvent = true) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) - public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { - Map map = mapper.readValue(input, Map.class); + @Logging(logResponse = true) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + byte[] buf = new byte[1024]; + int length; + while ((length = inputStream.read(buf)) != -1) { + outputStream.write(buf, 0, length); + } + } - System.out.println(map.size()); + public static Logger getLogger() { + return logger; } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogSamplingDisabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogSamplingDisabled.java new file mode 100644 index 000000000..5e2a7f148 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogSamplingDisabled.java @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogSamplingDisabled implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogSamplingDisabled.class); + + @Override + @Logging(samplingRate = 0.0) + public Boolean handleRequest(Object input, Context context) { + LOG.info("Test event"); + LOG.debug("Test debug event"); + return LOG.isDebugEnabled(); + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolSamplingEnabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogSamplingEnabled.java similarity index 74% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolSamplingEnabled.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogSamplingEnabled.java index 357520395..6a8c37896 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolSamplingEnabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogSamplingEnabled.java @@ -16,18 +16,18 @@ 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; -public class PowerLogToolSamplingEnabled implements RequestHandler { - private final Logger LOG = LogManager.getLogger(PowerLogToolSamplingEnabled.class); +public class PowertoolsLogSamplingEnabled implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogSamplingEnabled.class); @Override @Logging(samplingRate = 1.0) - public Object handleRequest(Object input, Context context) { + public Boolean handleRequest(Object input, Context context) { LOG.info("Test event"); LOG.debug("Test debug event"); - return null; + return LOG.isDebugEnabled(); } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/JsonSerializerTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/JsonSerializerTest.java new file mode 100644 index 000000000..8b34d32cb --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/JsonSerializerTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.lambda.powertools.logging.model.Basket; +import software.amazon.lambda.powertools.logging.model.Product; +import software.amazon.lambda.powertools.utilities.JsonConfig; + +class JsonSerializerTest { + private StringBuilder sb; + private JsonSerializer generator; + + @BeforeEach + void setUp() { + sb = new StringBuilder(); + generator = new JsonSerializer(sb); + } + + @Test + void writeString_shouldWriteStringWithQuotes() throws IOException { + generator.writeStringField("key", "StringValue"); + assertThat(sb.toString()).hasToString("\"key\":\"StringValue\""); + } + + @Test + void writeBoolean_shouldWriteBooleanValue() throws IOException { + generator.writeBooleanField("key", true); + assertThat(sb.toString()).hasToString("\"key\":true"); + } + + @Test + void writeNull_shouldWriteNullValue() throws IOException { + generator.writeNullField("key"); + assertThat(sb.toString()).hasToString("\"key\":null"); + } + + @Test + void writeInt_shouldWriteIntValue() throws IOException { + generator.writeNumberField("key", 1); + assertThat(sb.toString()).hasToString("\"key\":1"); + } + + @Test + void writeFloat_shouldWriteFloatValue() throws IOException { + generator.writeNumberField("key", 2.4f); + assertThat(sb.toString()).hasToString("\"key\":2.4"); + assertThat(sb.toString()).doesNotContain("F").doesNotContain("f"); // should not contain the F suffix for floats. + } + + @Test + void writeDouble_shouldWriteDoubleValue() throws IOException { + generator.writeNumberField("key", 4.3); + assertThat(sb.toString()).hasToString("\"key\":4.3"); + } + + @Test + void writeLong_shouldWriteLongValue() throws IOException { + generator.writeNumberField("key", 123456789L); + assertThat(sb.toString()).hasToString("\"key\":123456789"); + assertThat(sb.toString()).doesNotContain("L").doesNotContain("l"); // should not contain the L suffix for longs. + } + + @Test + void writeBigDecimal_shouldWriteBigDecimal() throws IOException { + generator.writeNumberField("key", BigDecimal.valueOf(432.1673254564546)); + assertThat(sb.toString()).hasToString("\"key\":432.1673254564546"); + } + + @Test + void writeStringObject_shouldWriteStringWithQuotes() throws IOException { + generator.writeObjectField("key","StringValue"); + assertThat(sb.toString()).hasToString("\"key\":\"StringValue\""); + } + + @Test + void writeMapObject_shouldWriteMapAsJson() throws IOException { + Map map = new HashMap<>(); + map.put("string","StringValue"); + map.put("number", BigInteger.valueOf(1234567890L)); + generator.writeObjectField("map", map); + assertThat(sb.toString()).hasToString("\"map\":{\"number\":1234567890,\"string\":\"StringValue\"}"); + } + + @Test + void writeListObject_shouldWriteListAsArray() throws IOException { + List list = Arrays.asList("val1", "val2", "val3"); + generator.writeObjectField("list", list); + assertThat(sb.toString()).hasToString("\"list\":[\"val1\",\"val2\",\"val3\"]"); + } + + @Test + void writeCustomObject_shouldWriteObjectAsJson() throws IOException { + Basket basket = new Basket(); + basket.add(new Product(42, "Nintendo DS", 299.45)); + basket.add(new Product(98, "Playstation 5", 499.99)); + generator.writeObjectField("basket", basket); + assertThat(sb.toString()).hasToString("\"basket\":{\"products\":[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]}"); + } + + @Test + void writeJsonNodeObject_shouldWriteObjectAsJson() throws IOException { + JsonNode jsonNode = JsonConfig.get().getObjectMapper().readTree( + "[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]"); + generator.writeObjectField("basket", jsonNode); + assertThat(sb.toString()).hasToString("\"basket\":[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]"); + } + + @Test + void writeTreeNodeArrayObject_shouldWriteObjectAsJson() throws IOException { + TreeNode treeNode = JsonConfig.get().getObjectMapper().readTree( + "[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]"); + generator.writeTree(treeNode); + assertThat(sb.toString()).hasToString("[{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45},{\"id\":98,\"name\":\"Playstation 5\",\"price\":499.99}]"); + } + + @Test + void writeTreeNodeObject_shouldWriteObjectAsJson() throws IOException { + TreeNode treeNode = JsonConfig.get().getObjectMapper().readTree( + "{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}"); + generator.writeTree(treeNode); + assertThat(sb.toString()).hasToString("{\"id\":42,\"name\":\"Nintendo DS\",\"price\":299.45}"); + } + +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java index 75cf22aa5..aba4664fe 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaLoggingAspectTest.java @@ -14,57 +14,87 @@ package software.amazon.lambda.powertools.logging.internal; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; -import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; -import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; -import com.amazonaws.services.lambda.runtime.tests.annotations.Event; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.ThreadContext; -import org.json.JSONException; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor; -import software.amazon.lambda.powertools.core.internal.SystemWrapper; -import software.amazon.lambda.powertools.logging.handlers.*; - -import java.io.*; +import static java.util.Collections.singletonList; +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_ARN; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_COLD_START; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_NAME; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_REQUEST_ID; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_TRACE_ID; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_VERSION; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.SERVICE; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonList; -import static java.util.stream.Collectors.joining; -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; -import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getProperty; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junitpioneer.jupiter.ClearEnvironmentVariable; +import org.junitpioneer.jupiter.SetEnvironmentVariable; +import org.junitpioneer.jupiter.SetSystemProperty; +import org.mockito.Mock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.slf4j.event.Level; +import org.slf4j.test.TestLogger; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.logging.argument.StructuredArgument; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAlbCorrelationId; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayHttpApiCorrelationId; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayRestApiCorrelationId; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAppSyncCorrelationId; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogClearState; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogDisabled; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogDisabledForStream; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabled; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabledForStream; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogError; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEvent; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventBridgeCorrelationId; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventEnvVar; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEventForStream; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponse; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponseForStream; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogSamplingDisabled; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogSamplingEnabled; class LambdaLoggingAspectTest { + private static final Logger LOG = LoggerFactory.getLogger(LambdaLoggingAspectTest.class); private static final int EXPECTED_CONTEXT_SIZE = 8; private RequestStreamHandler requestStreamHandler; private RequestHandler requestHandler; @@ -73,253 +103,555 @@ class LambdaLoggingAspectTest { private Context context; @BeforeEach - void setUp() throws IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException { + void setUp() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, IOException { openMocks(this); - ThreadContext.clearAll(); + MDC.clear(); writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); setupContext(); - requestHandler = new PowerLogToolEnabled(); - requestStreamHandler = new PowerLogToolEnabledForStream(); + requestHandler = new PowertoolsLogEnabled(); + requestStreamHandler = new PowertoolsLogEnabledForStream(); + resetLogLevel(Level.INFO); + writeStaticField(LoggingConstants.class, "LAMBDA_LOG_LEVEL", null, true); + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", null, true); + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_EVENT", false, true); + writeStaticField(LoggingConstants.class, "POWERTOOLS_SAMPLING_RATE", null, true); + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // may not be there in the first run + } + } + + @AfterEach + void cleanUp() throws IOException { //Make sure file is cleaned up before running full stack logging regression FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); - resetLogLevel(Level.INFO); + } + + @Test + void shouldLogDebugWhenPowertoolsLevelEnvVarIsDebug() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "DEBUG", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isTrue(); + } + + @Test + void shouldLogInfoWhenPowertoolsLevelEnvVarIsInfo() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "INFO", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isTrue(); + } + + @Test + void shouldLogInfoWhenPowertoolsLevelEnvVarIsInvalid() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "INVALID", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isTrue(); + } + + @Test + void shouldLogWarnWhenPowertoolsLevelEnvVarIsWarn() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "WARN", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isFalse(); + assertThat(LOG.isWarnEnabled()).isTrue(); + } + + @Test + void shouldLogErrorWhenPowertoolsLevelEnvVarIsError() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "ERROR", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isFalse(); + assertThat(LOG.isWarnEnabled()).isFalse(); + assertThat(LOG.isErrorEnabled()).isTrue(); + } + + @Test + void shouldLogErrorWhenPowertoolsLevelEnvVarIsFatal() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "FATAL", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isFalse(); + assertThat(LOG.isWarnEnabled()).isFalse(); + assertThat(LOG.isErrorEnabled()).isTrue(); + } + + @Test + void shouldLogWarnWhenPowertoolsLevelEnvVarIsWarnAndLambdaLevelVarIsInfo() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "WARN", true); + writeStaticField(LoggingConstants.class, "LAMBDA_LOG_LEVEL", "INFO", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isFalse(); + assertThat(LOG.isWarnEnabled()).isTrue(); + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).doesNotContain(" does not match AWS Lambda Advanced Logging Controls minimum log level"); + } + + @Test + void shouldLogInfoWhenPowertoolsLevelEnvVarIsInfoAndLambdaLevelVarIsWarn() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "INFO", true); + writeStaticField(LoggingConstants.class, "LAMBDA_LOG_LEVEL", "WARN", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isTrue(); + File logFile = new File("target/logfile.json"); + // should log a warning as powertools level is lower than lambda level + assertThat(contentOf(logFile)).contains("Current log level (INFO) does not match AWS Lambda Advanced Logging Controls minimum log level (WARN). This can lead to data loss, consider adjusting them."); + } + + @Test + void shouldLogWarnWhenPowertoolsLevelEnvVarINotSetAndLambdaLevelVarIsWarn() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", null, true); + writeStaticField(LoggingConstants.class, "LAMBDA_LOG_LEVEL", "WARN", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isFalse(); + assertThat(LOG.isWarnEnabled()).isTrue(); } @Test void shouldSetLambdaContextWhenEnabled() { requestHandler.handleRequest(new Object(), context); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE) - .containsEntry(DefaultLambdaFields.FUNCTION_ARN.getName(), "testArn") - .containsEntry(DefaultLambdaFields.FUNCTION_MEMORY_SIZE.getName(), "10") - .containsEntry(DefaultLambdaFields.FUNCTION_VERSION.getName(), "1") - .containsEntry(DefaultLambdaFields.FUNCTION_NAME.getName(), "testFunction") - .containsEntry(DefaultLambdaFields.FUNCTION_REQUEST_ID.getName(), "RequestId") - .containsKey("coldStart") - .containsKey("service"); + .containsEntry(FUNCTION_ARN.getName(), "testArn") + .containsEntry(FUNCTION_MEMORY_SIZE.getName(), "10") + .containsEntry(FUNCTION_VERSION.getName(), "1") + .containsEntry(FUNCTION_NAME.getName(), "testFunction") + .containsEntry(FUNCTION_REQUEST_ID.getName(), "RequestId") + .containsKey(FUNCTION_COLD_START.getName()) + .containsKey(SERVICE.getName()); } @Test void shouldSetLambdaContextForStreamHandlerWhenEnabled() throws IOException { - requestStreamHandler = new PowerLogToolEnabledForStream(); + requestStreamHandler = new PowertoolsLogEnabledForStream(); requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[]{}), new ByteArrayOutputStream(), context); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE) - .containsEntry(DefaultLambdaFields.FUNCTION_ARN.getName(), "testArn") - .containsEntry(DefaultLambdaFields.FUNCTION_MEMORY_SIZE.getName(), "10") - .containsEntry(DefaultLambdaFields.FUNCTION_VERSION.getName(), "1") - .containsEntry(DefaultLambdaFields.FUNCTION_NAME.getName(), "testFunction") - .containsEntry(DefaultLambdaFields.FUNCTION_REQUEST_ID.getName(), "RequestId") - .containsKey("coldStart") - .containsKey("service"); + .containsEntry(FUNCTION_ARN.getName(), "testArn") + .containsEntry(FUNCTION_MEMORY_SIZE.getName(), "10") + .containsEntry(FUNCTION_VERSION.getName(), "1") + .containsEntry(FUNCTION_NAME.getName(), "testFunction") + .containsEntry(FUNCTION_REQUEST_ID.getName(), "RequestId") + .containsKey(FUNCTION_COLD_START.getName()) + .containsKey(SERVICE.getName()); } @Test - void shouldSetColdStartFlag() throws IOException { + void shouldSetColdStartFlagOnFirstCallNotOnSecondCall() throws IOException { requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[]{}), new ByteArrayOutputStream(), context); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE) - .containsEntry("coldStart", "true"); + .containsEntry(FUNCTION_COLD_START.getName(), "true"); requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[]{}), new ByteArrayOutputStream(), context); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE) - .containsEntry("coldStart", "false"); + .containsEntry(FUNCTION_COLD_START.getName(), "false"); } @Test void shouldNotSetLambdaContextWhenDisabled() { - requestHandler = new PowerToolDisabled(); + requestHandler = new PowertoolsLogDisabled(); requestHandler.handleRequest(new Object(), context); - assertThat(ThreadContext.getImmutableContext()) - .isEmpty(); + assertThat(MDC.getCopyOfContextMap()).isNull(); } @Test void shouldNotSetLambdaContextForStreamHandlerWhenDisabled() throws IOException { - requestStreamHandler = new PowerToolDisabledForStream(); + requestStreamHandler = new PowertoolsLogDisabledForStream(); requestStreamHandler.handleRequest(null, null, context); - assertThat(ThreadContext.getImmutableContext()) - .isEmpty(); + assertThat(MDC.getCopyOfContextMap()).isNull(); } @Test - void shouldHaveNoEffectIfNotUsedOnLambdaHandler() { - PowerLogToolEnabled handler = new PowerLogToolEnabled(); + void shouldClearStateWhenClearStateIsTrue() { + PowertoolsLogClearState handler = new PowertoolsLogClearState(); - handler.anotherMethod(); + handler.handleRequest(Collections.singletonMap("mySuperSecret", "P@ssw0Rd"), context); - assertThat(ThreadContext.getImmutableContext()) - .isEmpty(); + assertThat(MDC.getCopyOfContextMap()).isNull(); } @Test - void shouldLogEventForHandler() throws IOException, JSONException { - requestHandler = new PowerToolLogEventEnabled(); - S3EventNotification s3EventNotification = s3EventNotification(); + void shouldLogDebugWhenSamplingEqualsOne() { + PowertoolsLogSamplingEnabled handler = new PowertoolsLogSamplingEnabled(); - requestHandler.handleRequest(s3EventNotification, context); + Boolean debugEnabled = handler.handleRequest(new Object(), context); - Map log = parseToMap(Files.lines(Paths.get("target/logfile.json")).collect(joining())); + assertThat(debugEnabled).isTrue(); + } - String event = (String) log.get("message"); + @Test + void shouldLogDebugWhenSamplingEnvVarEqualsOne() throws IllegalAccessException { + // GIVEN + LoggingConstants.POWERTOOLS_SAMPLING_RATE = "1"; + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); - String expectEvent = new BufferedReader( - new InputStreamReader(this.getClass().getResourceAsStream("/s3EventNotification.json"))) - .lines().collect(joining("\n")); + // WHEN + handler.handleRequest(new Object(), context); - assertEquals(expectEvent, event, false); + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("Test debug event"); } - /** - * If POWERTOOLS_LOGGER_LOG_EVENT was set to true, the handler should log, despite @Logging(logEvent=false) - * - * @throws IOException - */ @Test - void shouldLogEventForHandlerWhenEnvVariableSetToTrue() throws IOException, IllegalAccessException, JSONException { - try { - writeStaticField(LambdaLoggingAspect.class, "LOG_EVENT", Boolean.TRUE, true); + void shouldNotLogDebugWhenSamplingEnvVarIsTooBig() throws IllegalAccessException { + // GIVEN + LoggingConstants.POWERTOOLS_SAMPLING_RATE = "42"; - requestHandler = new PowerToolLogEventDisabled(); - S3EventNotification s3EventNotification = s3EventNotification(); + // WHEN + requestHandler.handleRequest(new Object(), context); - requestHandler.handleRequest(s3EventNotification, context); + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).doesNotContain("Test debug event"); + } - Map log = parseToMap(Files.lines(Paths.get("target/logfile.json")).collect(joining())); + @Test + void shouldNotLogDebugWhenSamplingEnvVarIsInvalid() { + // GIVEN + LoggingConstants.POWERTOOLS_SAMPLING_RATE = "NotANumber"; - String event = (String) log.get("message"); + // WHEN + requestHandler.handleRequest(new Object(), context); - String expectEvent = new BufferedReader( - new InputStreamReader(this.getClass().getResourceAsStream("/s3EventNotification.json"))) - .lines().collect(joining("\n")); + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).doesNotContain("Test debug event"); + assertThat(contentOf(logFile)).contains( + "Skipping sampling rate on environment variable configuration because of invalid value"); + } - assertEquals(expectEvent, event, false); - } finally { - writeStaticField(LambdaLoggingAspect.class, "LOG_EVENT", Boolean.FALSE, true); - } + @Test + void shouldNotLogDebugWhenSamplingEqualsZero() { + // GIVEN + LoggingConstants.POWERTOOLS_SAMPLING_RATE = "0"; + PowertoolsLogSamplingDisabled handler = new PowertoolsLogSamplingDisabled(); + + // WHEN + Boolean debugEnabled = handler.handleRequest(new Object(), context); + + // THEN + assertThat(debugEnabled).isFalse(); + } + + @Test + void shouldHaveNoEffectIfNotUsedOnLambdaHandler() { + // GIVEN + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); + + // WHEN + handler.anotherMethod(); + + // THEN + assertThat(MDC.getCopyOfContextMap()).isNull(); + } + + @Test + void shouldLogServiceNameWhenEnvVarSet() throws IllegalAccessException { + // GIVEN + writeStaticField(LambdaHandlerProcessor.class, "SERVICE_NAME", "testService", true); + + // WHEN + requestHandler.handleRequest(new Object(), context); + + // THEN + assertThat(MDC.getCopyOfContextMap()) + .hasSize(EXPECTED_CONTEXT_SIZE) + .containsEntry(SERVICE.getName(), "testService"); + } + + @Test + @ClearEnvironmentVariable(key = "_X_AMZN_TRACE_ID") + @SetSystemProperty(key = "com.amazonaws.xray.traceHeader", value = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1") + void shouldLogxRayTraceIdSystemPropertySet() { + String xRayTraceId = "1-5759e988-bd862e3fe1be46a994272793"; + + requestHandler.handleRequest(new Object(), context); + + assertThat(MDC.getCopyOfContextMap()) + .hasSize(EXPECTED_CONTEXT_SIZE + 1) + .containsEntry("xray_trace_id", xRayTraceId); } - /** - * If POWERTOOLS_LOGGER_LOG_EVENT was set to false and @Logging(logEvent=false), the handler shouldn't log - * - * @throws IOException - */ @Test - void shouldNotLogEventForHandlerWhenEnvVariableSetToFalse() throws IOException { - requestHandler = new PowerToolLogEventDisabled(); - S3EventNotification s3EventNotification = s3EventNotification(); + @SetEnvironmentVariable(key = "_X_AMZN_TRACE_ID", value = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1") + void shouldLogxRayTraceIdEnvVarSet() { + // GIVEN + String xRayTraceId = "1-5759e988-bd862e3fe1be46a994272793"; - requestHandler.handleRequest(s3EventNotification, context); + // WHEN + requestHandler.handleRequest(new Object(), context); - Assertions.assertEquals(0, - Files.lines(Paths.get("target/logfile.json")).collect(joining()).length()); + // THEN + assertThat(MDC.getCopyOfContextMap()) + .hasSize(EXPECTED_CONTEXT_SIZE + 1) + .containsEntry(FUNCTION_TRACE_ID.getName(), xRayTraceId); } @Test - void shouldLogEventForHandlerWithOverriddenObjectMapper() throws IOException, JSONException { - RequestHandler handler = new PowerToolLogEventEnabledWithCustomMapper(); - S3EventNotification s3EventNotification = s3EventNotification(); + void shouldLogEventForHandlerWithLogEventAnnotation() { + // GIVEN + requestHandler = new PowertoolsLogEvent(); + List listOfOneElement = singletonList("ListOfOneElement"); + + // WHEN + requestHandler.handleRequest(listOfOneElement, context); + + // THEN + TestLogger logger = (TestLogger) PowertoolsLogEvent.getLogger(); + assertThat(logger.getArguments()).hasSize(1); + StructuredArgument argument = (StructuredArgument) logger.getArguments()[0]; + assertThat(argument.toString()).hasToString("event=" + listOfOneElement.toString()); + } - handler.handleRequest(s3EventNotification, context); + @Test + void shouldLogEventForHandlerWhenEnvVariableSetToTrue() { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_EVENT = true; - Map log = parseToMap(Files.lines(Paths.get("target/logfile.json")).collect(joining())); + requestHandler = new PowertoolsLogEventEnvVar(); - String event = (String) log.get("message"); + SQSEvent.SQSMessage message = new SQSEvent.SQSMessage(); + message.setBody("body"); + message.setMessageId("1234abcd"); + message.setAwsRegion("eu-west-1"); - String expectEvent = new BufferedReader( - new InputStreamReader(this.getClass().getResourceAsStream("/customizedLogEvent.json"))) - .lines().collect(joining("\n")); + // WHEN + requestHandler.handleRequest(message, context); - assertEquals(expectEvent, event, false); + // THEN + TestLogger logger = (TestLogger) ((PowertoolsLogEventEnvVar) requestHandler).getLogger(); + try { + assertThat(logger.getArguments()).hasSize(1); + StructuredArgument argument = (StructuredArgument) logger.getArguments()[0]; + assertThat(argument.toString()).hasToString("event={messageId: 1234abcd,awsRegion: eu-west-1,body: body,}"); + } finally { + LoggingConstants.POWERTOOLS_LOG_EVENT = false; + if (logger != null) { + logger.clearArguments(); + } + } } @Test - void shouldLogEventForStreamAndLambdaStreamIsValid() throws IOException, JSONException { - requestStreamHandler = new PowerToolLogEventEnabledForStream(); + void shouldNotLogEventForHandlerWhenEnvVariableSetToFalse() { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_EVENT = false; + + // WHEN + requestHandler = new PowertoolsLogEventEnvVar(); + requestHandler.handleRequest(singletonList("ListOfOneElement"), context); + + // THEN + TestLogger logger = (TestLogger) ((PowertoolsLogEventEnvVar)requestHandler).getLogger(); + assertThat(logger.getArguments()).isNull(); + } + + @Test + void shouldLogEventForStreamHandler() throws IOException { + // GIVEN + requestStreamHandler = new PowertoolsLogEventForStream(); ByteArrayOutputStream output = new ByteArrayOutputStream(); - S3EventNotification s3EventNotification = s3EventNotification(); + Map map = Collections.singletonMap("key", "value"); - requestStreamHandler.handleRequest( - new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(s3EventNotification)), output, context); + // WHEN + requestStreamHandler.handleRequest(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(map)), output, context); + // THEN assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) .isNotEmpty(); - Map log = parseToMap(Files.lines(Paths.get("target/logfile.json")).collect(joining())); + TestLogger logger = (TestLogger) PowertoolsLogEventForStream.getLogger(); + try { + assertThat(logger.getArguments()).hasSize(1); + StructuredArgument argument = (StructuredArgument) logger.getArguments()[0]; + assertThat(argument.toString()).hasToString("event={\"key\":\"value\"}"); + } finally { + logger.clearArguments(); + } + } - String event = (String) log.get("message"); + @Test + void shouldLogResponseForHandlerWithLogResponseAnnotation() { + // GIVEN + requestHandler = new PowertoolsLogResponse(); - String expectEvent = new BufferedReader( - new InputStreamReader(this.getClass().getResourceAsStream("/s3EventNotification.json"))) - .lines().collect(joining("\n")); + // WHEN + requestHandler.handleRequest("input", context); - assertEquals(expectEvent, event, false); + // THEN + TestLogger logger = (TestLogger) PowertoolsLogResponse.getLogger(); + try { + assertThat(logger.getArguments()).hasSize(1); + StructuredArgument argument = (StructuredArgument) logger.getArguments()[0]; + assertThat(argument.toString()).hasToString("response=Hola mundo"); + } finally { + logger.clearArguments(); + } } @Test - void shouldLogServiceNameWhenEnvVarSet() throws IllegalAccessException { - writeStaticField(LambdaHandlerProcessor.class, "SERVICE_NAME", "testService", true); - requestHandler.handleRequest(new Object(), context); + void shouldLogResponseForHandlerWhenEnvVariableSetToTrue() { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_RESPONSE = true; - assertThat(ThreadContext.getImmutableContext()) - .hasSize(EXPECTED_CONTEXT_SIZE) - .containsEntry("service", "testService"); + requestHandler = new PowertoolsLogEnabled(); + + // WHEN + requestHandler.handleRequest("input", context); + + // THEN + TestLogger logger = (TestLogger) PowertoolsLogEnabled.getLogger(); + try { + assertThat(logger.getArguments()).hasSize(1); + StructuredArgument argument = (StructuredArgument) logger.getArguments()[0]; + assertThat(argument.toString()).hasToString("response=Bonjour le monde"); + } finally { + LoggingConstants.POWERTOOLS_LOG_RESPONSE = false; + logger.clearArguments(); + } } @Test - void shouldLogxRayTraceIdSystemPropertySet() { - String xRayTraceId = "1-5759e988-bd862e3fe1be46a994272793"; + void shouldLogResponseForStreamHandler() throws IOException { + // GIVEN + requestStreamHandler = new PowertoolsLogResponseForStream(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + String input = "BobThe Sponge"; - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> getenv("_X_AMZN_TRACE_ID")) - .thenReturn(null); - mocked.when(() -> getProperty("com.amazonaws.xray.traceHeader")) - .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"); + // WHEN + requestStreamHandler.handleRequest(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)), output, context); - requestHandler.handleRequest(new Object(), context); + // THEN + assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) + .isEqualTo(input); - assertThat(ThreadContext.getImmutableContext()) - .hasSize(EXPECTED_CONTEXT_SIZE + 1) - .containsEntry("xray_trace_id", xRayTraceId); + TestLogger logger = (TestLogger) PowertoolsLogResponseForStream.getLogger(); + try { + assertThat(logger.getArguments()).hasSize(1); + StructuredArgument argument = (StructuredArgument) logger.getArguments()[0]; + assertThat(argument.toString()).hasToString("response=" + input); + } finally { + logger.clearArguments(); } } @Test - void shouldLogxRayTraceIdEnvVarSet() { - String xRayTraceId = "1-5759e988-bd862e3fe1be46a994272793"; + void shouldLogErrorForHandlerWithLogErrorAnnotation() { + // GIVEN + requestHandler = new PowertoolsLogError(); - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> getenv("_X_AMZN_TRACE_ID")) - .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"); + // WHEN + try { + requestHandler.handleRequest("input", context); + } catch (Exception e) { + // ignore + } - requestHandler.handleRequest(new Object(), context); + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("This is an error"); + } - assertThat(ThreadContext.getImmutableContext()) - .hasSize(EXPECTED_CONTEXT_SIZE + 1) - .containsEntry("xray_trace_id", xRayTraceId); + @Test + void shouldLogErrorForHandlerWhenEnvVariableSetToTrue() { + try { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_ERROR = true; + + requestHandler = new PowertoolsLogEnabled(true); + + // WHEN + try { + requestHandler.handleRequest("input", context); + } catch (Exception e) { + // ignore + } + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("Something went wrong"); + } finally { + LoggingConstants.POWERTOOLS_LOG_ERROR = false; } } @ParameterizedTest @Event(value = "apiGatewayProxyEventV1.json", type = APIGatewayProxyRequestEvent.class) void shouldLogCorrelationIdOnAPIGatewayProxyRequestEvent(APIGatewayProxyRequestEvent event) { - RequestHandler handler = new PowerLogToolApiGatewayRestApiCorrelationId(); + // GIVEN + RequestHandler handler = new PowertoolsLogApiGatewayRestApiCorrelationId(); + + // WHEN handler.handleRequest(event, context); - assertThat(ThreadContext.getImmutableContext()) + // THEN + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE + 1) .containsEntry("correlation_id", event.getRequestContext().getRequestId()); } @@ -327,10 +659,14 @@ void shouldLogCorrelationIdOnAPIGatewayProxyRequestEvent(APIGatewayProxyRequestE @ParameterizedTest @Event(value = "apiGatewayProxyEventV2.json", type = APIGatewayV2HTTPEvent.class) void shouldLogCorrelationIdOnAPIGatewayV2HTTPEvent(APIGatewayV2HTTPEvent event) { - RequestHandler handler = new PowerLogToolApiGatewayHttpApiCorrelationId(); + // GIVEN + RequestHandler handler = new PowertoolsLogApiGatewayHttpApiCorrelationId(); + + // WHEN handler.handleRequest(event, context); - assertThat(ThreadContext.getImmutableContext()) + // THEN + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE + 1) .containsEntry("correlation_id", event.getRequestContext().getRequestId()); } @@ -338,46 +674,92 @@ void shouldLogCorrelationIdOnAPIGatewayV2HTTPEvent(APIGatewayV2HTTPEvent event) @ParameterizedTest @Event(value = "albEvent.json", type = ApplicationLoadBalancerRequestEvent.class) void shouldLogCorrelationIdOnALBEvent(ApplicationLoadBalancerRequestEvent event) { + // GIVEN RequestHandler handler = new PowertoolsLogAlbCorrelationId(); + + // WHEN handler.handleRequest(event, context); - assertThat(ThreadContext.getImmutableContext()) + // THEN + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE + 1) .containsEntry("correlation_id", event.getHeaders().get("x-amzn-trace-id")); } @Test void shouldLogCorrelationIdOnStreamHandler() throws IOException { + // GIVEN RequestStreamHandler handler = new PowertoolsLogEventBridgeCorrelationId(); String eventId = "3"; - String event = "{\"id\":" + eventId + "}"; // CorrelationIdPathConstants.EVENT_BRIDGE + String event = "{\"id\":" + eventId + "}"; // CorrelationIdPath.EVENT_BRIDGE ByteArrayInputStream inputStream = new ByteArrayInputStream(event.getBytes()); + + // WHEN handler.handleRequest(inputStream, new ByteArrayOutputStream(), context); + // THEN + assertThat(MDC.getCopyOfContextMap()) + .hasSize(EXPECTED_CONTEXT_SIZE + 1) + .containsEntry("correlation_id", eventId); + } + + @Test + void shouldLogCorrelationIdOnAppSyncEvent() throws IOException { + // GIVEN + RequestStreamHandler handler = new PowertoolsLogAppSyncCorrelationId(); + String eventId = "456"; + String event = "{\"request\":{\"headers\":{\"x-amzn-trace-id\":" + eventId + "}}}"; // CorrelationIdPath.APPSYNC_RESOLVER + ByteArrayInputStream inputStream = new ByteArrayInputStream(event.getBytes()); + + // WHEN + handler.handleRequest(inputStream, new ByteArrayOutputStream(), context); - assertThat(ThreadContext.getImmutableContext()) + // THEN + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE + 1) .containsEntry("correlation_id", eventId); } @Test - void shouldLogAndClearLogContextOnEachRequest() throws IOException { - requestHandler = new PowertoolsLogEnabledWithClearState(); - S3EventNotification s3EventNotification = s3EventNotification(); + void testMultipleLoggingManagers_shouldWarnAndSelectFirstOne() throws UnsupportedEncodingException { + // GIVEN + List list = new ArrayList<>(); + list.add(new TestLoggingManager()); + list.add(new DefautlLoggingManager()); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream stream = new PrintStream(outputStream); + + // WHEN + LambdaLoggingAspect.getLoggingManager(list, stream); + + // THEN + String output = outputStream.toString("UTF-8"); + assertThat(output) + .contains("WARN. Multiple LoggingManagers were found on the classpath") + .contains("WARN. Make sure to have only one of powertools-logging-log4j OR powertools-logging-logback to your dependencies") + .contains("WARN. Using the first LoggingManager found on the classpath: [" + list.get(0) + "]"); + } - requestHandler.handleRequest(s3EventNotification, context); - requestHandler.handleRequest(s3EventNotification, context); + @Test + void testNoLoggingManagers_shouldWarnAndCreateDefault() throws UnsupportedEncodingException { + // GIVEN + List list = new ArrayList<>(); - List logLines = Files.lines(Paths.get("target/logfile.json")).collect(Collectors.toList()); - Map invokeLog = parseToMap(logLines.get(0)); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream stream = new PrintStream(outputStream); - assertThat(invokeLog) - .containsEntry("TestKey", "TestValue"); + // WHEN + LoggingManager loggingManager = LambdaLoggingAspect.getLoggingManager(list, stream); - invokeLog = parseToMap(logLines.get(1)); + // THEN + String output = outputStream.toString("UTF-8"); + assertThat(output) + .contains("ERROR. No LoggingManager was found on the classpath") + .contains("ERROR. Applying default LoggingManager: POWERTOOLS_LOG_LEVEL variable is ignored") + .contains("ERROR. Make sure to add either powertools-logging-log4j or powertools-logging-logback to your dependencies"); - assertThat(invokeLog) - .doesNotContainKey("TestKey"); + assertThat(loggingManager).isExactlyInstanceOf(DefautlLoggingManager.class); } private void setupContext() { @@ -390,44 +772,9 @@ private void setupContext() { private void resetLogLevel(Level level) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { - Method resetLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("resetLogLevels", Level.class); - resetLogLevels.setAccessible(true); - resetLogLevels.invoke(null, level); + Method setLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("setLogLevels", Level.class); + setLogLevels.setAccessible(true); + setLogLevels.invoke(null, level); writeStaticField(LambdaLoggingAspect.class, "LEVEL_AT_INITIALISATION", level, true); } - - private S3EventNotification s3EventNotification() { - S3EventNotification.S3EventNotificationRecord record = - new S3EventNotification.S3EventNotificationRecord("us-west-2", - "ObjectCreated:Put", - "aws:s3", - null, - "2.1", - new S3EventNotification.RequestParametersEntity("127.0.0.1"), - new S3EventNotification.ResponseElementsEntity("C3D13FE58DE4C810", - "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"), - new S3EventNotification.S3Entity("testConfigRule", - new S3EventNotification.S3BucketEntity("mybucket", - new S3EventNotification.UserIdentityEntity("A3NL1KOZZKExample"), - "arn:aws:s3:::mybucket"), - new S3EventNotification.S3ObjectEntity("HappyFace.jpg", - 1024L, - "d41d8cd98f00b204e9800998ecf8427e", - "096fKKXTRTtl3on89fVO.nfljtsv6qko", - "0055AED6DCD90281E5"), - "1.0"), - new S3EventNotification.UserIdentityEntity("AIDAJDPLRKLG7UEXAMPLE") - ); - - return new S3EventNotification(singletonList(record)); - } - - private Map parseToMap(String stringAsJson) { - try { - return new ObjectMapper().readValue(stringAsJson, Map.class); - } catch (JsonProcessingException e) { - fail("Failed parsing logger line " + stringAsJson); - return emptyMap(); - } - } -} \ No newline at end of file +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/TestLoggingManager.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/TestLoggingManager.java new file mode 100644 index 000000000..0958e0d3b --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/TestLoggingManager.java @@ -0,0 +1,32 @@ +package software.amazon.lambda.powertools.logging.internal; + +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import org.slf4j.test.TestLogger; +import org.slf4j.test.TestLoggerFactory; + +public class TestLoggingManager implements LoggingManager { + + private final TestLoggerFactory loggerFactory; + + public TestLoggingManager() { + ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); + if (!(loggerFactory instanceof TestLoggerFactory)) { + throw new RuntimeException( + "LoggerFactory does not match required type: " + TestLoggerFactory.class.getName()); + } + this.loggerFactory = (TestLoggerFactory) loggerFactory; + } + + @Override + public void setLogLevel(Level logLevel) { + loggerFactory.getLoggers().forEach((key, logger) -> ((TestLogger) logger).setLogLevel(logLevel.toString())); + } + + @Override + public Level getLogLevel(Logger logger) { + return org.slf4j.event.Level.intToLevel(((TestLogger) logger).getLogLevel()); + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/model/Basket.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/model/Basket.java new file mode 100644 index 000000000..0fa544cf1 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/model/Basket.java @@ -0,0 +1,67 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class Basket { + private List products = new ArrayList<>(); + + public Basket() { + } + + public Basket(Product... p) { + products.addAll(Arrays.asList(p)); + } + + public List getProducts() { + return products; + } + + public void setProducts(List products) { + this.products = products; + } + + public void add(Product product) { + products.add(product); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Basket basket = (Basket) o; + return products.equals(basket.products); + } + + @Override + public int hashCode() { + return Objects.hash(products); + } + + @Override + public String toString() { + return "Basket{" + + "products=" + products + + '}'; + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/model/Product.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/model/Product.java new file mode 100644 index 000000000..fee5bc20d --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/model/Product.java @@ -0,0 +1,84 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.logging.model; + +import java.util.Objects; + +public class Product { + private long id; + + private String name; + + private double price; + + public Product() { + } + + public Product(long id, String name, double price) { + this.id = id; + this.name = name; + this.price = price; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Product product = (Product) o; + return id == product.id && Double.compare(product.price, price) == 0 && Objects.equals(name, product.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, price); + } + + @Override + public String toString() { + return "Product{" + + "id=" + id + + ", name='" + name + '\'' + + ", price=" + price + + '}'; + } +} diff --git a/powertools-logging/src/test/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider b/powertools-logging/src/test/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider new file mode 100644 index 000000000..ade4bb1e2 --- /dev/null +++ b/powertools-logging/src/test/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider @@ -0,0 +1 @@ +org.slf4j.test.TestServiceProvider \ No newline at end of file diff --git a/powertools-logging/src/test/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager b/powertools-logging/src/test/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager new file mode 100644 index 000000000..adbf7ae69 --- /dev/null +++ b/powertools-logging/src/test/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager @@ -0,0 +1,15 @@ +# +# Copyright 2023 Amazon.com, Inc. or its affiliates. +# 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 +# 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. +# +# + +software.amazon.lambda.powertools.logging.internal.TestLoggingManager \ No newline at end of file diff --git a/powertools-logging/src/test/resources/s3EventNotification.json b/powertools-logging/src/test/resources/s3EventNotification.json deleted file mode 100644 index feb88ec02..000000000 --- a/powertools-logging/src/test/resources/s3EventNotification.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "records":[ - { - "eventVersion":"2.1", - "eventSource":"aws:s3", - "awsRegion":"us-west-2", - "eventName":"ObjectCreated:Put", - "userIdentity":{ - "principalId":"AIDAJDPLRKLG7UEXAMPLE" - }, - "requestParameters":{ - "sourceIPAddress":"127.0.0.1" - }, - "responseElements":{ - "xAmzId2":"C3D13FE58DE4C810", - "xAmzRequestId":"FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD" - }, - "s3":{ - "s3SchemaVersion":"1.0", - "configurationId":"testConfigRule", - "bucket":{ - "name":"mybucket", - "ownerIdentity":{ - "principalId":"A3NL1KOZZKExample" - }, - "arn":"arn:aws:s3:::mybucket" - }, - "object":{ - "key":"HappyFace.jpg", - "size":1024, - "eTag":"d41d8cd98f00b204e9800998ecf8427e", - "versionId":"096fKKXTRTtl3on89fVO.nfljtsv6qko", - "sequencer":"0055AED6DCD90281E5" - } - } - } - ] -} \ No newline at end of file diff --git a/powertools-logging/src/test/resources/testlogger.properties b/powertools-logging/src/test/resources/testlogger.properties new file mode 100644 index 000000000..84b7beaae --- /dev/null +++ b/powertools-logging/src/test/resources/testlogger.properties @@ -0,0 +1,3 @@ +org.slf4j.simpleLogger.defaultLogLevel=warn +org.slf4j.simpleLogger.log.software.amazon.lambda.powertools=info +org.slf4j.simpleLogger.logFile=target/logfile.json \ No newline at end of file diff --git a/powertools-metrics/pom.xml b/powertools-metrics/pom.xml index 53df4cf57..8ae52d43d 100644 --- a/powertools-metrics/pom.xml +++ b/powertools-metrics/pom.xml @@ -14,8 +14,8 @@ --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 powertools-metrics @@ -24,41 +24,24 @@ powertools-parent software.amazon.lambda - 1.19.0-SNAPSHOT + 2.0.0-RC1 - Powertools for AWS Lambda (Java) library Metrics + Powertools for AWS Lambda (Java) - Metrics A suite of utilities for AWS Lambda Functions that make creating custom metrics via AWS Embedded Metric Format asynchronously easier. - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - + + org.aspectj + aspectjrt + provided + software.amazon.lambda - powertools-core + powertools-common com.amazonaws @@ -82,10 +65,9 @@ com.fasterxml.jackson.core jackson-databind - - org.aspectj - aspectjrt + org.apache.commons + commons-lang3 @@ -100,8 +82,23 @@ test - org.apache.commons - commons-lang3 + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + org.slf4j + slf4j-simple + test + + + org.junit-pioneer + junit-pioneer test @@ -116,12 +113,122 @@ + + + generate-graalvm-files + + + org.mockito + mockito-subclass + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent + -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics,experimental-class-define-support + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.6 + true + + + test-native + + test + + test + + + + powertools-metrics + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + + + + + + + + + + + + src/main/resources + + org.apache.maven.plugins - maven-checkstyle-plugin + maven-surefire-plugin + + + Lambda + + - \ No newline at end of file + diff --git a/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java b/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java deleted file mode 100644 index e2d886fe5..000000000 --- a/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.cloudwatchlogs.emf.model; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import java.lang.reflect.Field; - -public final class MetricsLoggerHelper { - private MetricsLoggerHelper() { - } - - public static boolean hasNoMetrics() { - return metricsContext().getRootNode().getAws().isEmpty(); - } - - public static long dimensionsCount() { - return metricsContext().getDimensions().size(); - } - - public static MetricsContext metricsContext() { - try { - Field f = metricsLogger().getClass().getDeclaredField("context"); - f.setAccessible(true); - return (MetricsContext) f.get(metricsLogger()); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } -} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/FlushMetrics.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/FlushMetrics.java new file mode 100644 index 000000000..952625f5b --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/FlushMetrics.java @@ -0,0 +1,71 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code FlushMetrics} is used to signal that the annotated Lambda handler method should be + * extended with Metrics flushing functionality. Will have no effect when used on a method that is not a Lambda handler. + * + *

{@code FlushMetrics} allows users to asynchronously create Amazon + * CloudWatch metrics by using the CloudWatch Embedded Metrics Format. + * {@code FlushMetrics} manages the life-cycle and configuration of Metrics to simplify the user experience when used with AWS Lambda. + * + *

{@code FlushMetrics} should be used with the handleRequest method of a class + * which implements either + * {@code com.amazonaws.services.lambda.runtime.RequestHandler} or + * {@code com.amazonaws.services.lambda.runtime.RequestStreamHandler}.

+ * + *

{@code FlushMetrics} creates Amazon CloudWatch custom metrics. You can find + * pricing information on the CloudWatch pricing documentation page.

+ * + *

To enable creation of custom metrics for cold starts you can add {@code @FlushMetrics(captureColdStart = true)}. + *
This will create a metric with the key {@code "ColdStart"} and the unit type {@code COUNT}. + *

+ * + *

To raise exception if no metrics are emitted, use {@code @FlushMetrics(raiseOnEmptyMetrics = true)}. + *
This will create an exception if no metrics are emitted. By default its value is set to false. + *

+ * + *

By default the service name associated with metrics created will be + * "service_undefined". This can be overridden with the environment variable {@code POWERTOOLS_SERVICE_NAME} + * or the annotation variable {@code @FlushMetrics(service = "Service Name")}. + * If both are specified then the value of the annotation variable will be used.

+ * + *

A namespace must be specified for metrics. This can be set with the environment variable {@code POWERTOOLS_METRICS_NAMESPACE} + * or the annotation variable {@code @FlushMetrics(namespace = "Namespace")}. If not specified, an IllegalStateException will be thrown. + * If both are specified then the value of the annotation variable will be used.

+ * + *

You can specify a custom function name with {@code @FlushMetrics(functionName = "MyFunction")}. + * If specified, this will be used instead of the function name from the Lambda context. + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface FlushMetrics { + String namespace() default ""; + + String service() default ""; + + String functionName() default ""; + + boolean captureColdStart() default false; + + boolean raiseOnEmptyMetrics() default false; +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java index fb92c900d..d21fe163e 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java @@ -14,54 +14,175 @@ package software.amazon.lambda.powertools.metrics; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import com.amazonaws.services.lambda.runtime.Context; +import java.time.Instant; + +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; /** - * {@code Metrics} is used to signal that the annotated method should be - * extended with Metrics functionality. - * - *

{@code Metrics} allows users to asynchronously create Amazon - * CloudWatch metrics by using the CloudWatch Embedded Metrics Format. - * {@code Metrics} manages the life-cycle of the MetricsLogger class, - * to simplify the user experience when used with AWS Lambda. - * - *

{@code Metrics} should be used with the handleRequest method of a class - * which implements either - * {@code com.amazonaws.services.lambda.runtime.RequestHandler} or - * {@code com.amazonaws.services.lambda.runtime.RequestStreamHandler}.

- * - *

{@code Metrics} creates Amazon CloudWatch custom metrics. You can find - * pricing information on the CloudWatch pricing documentation page.

- * - *

To enable creation of custom metrics for cold starts you can add {@code @Metrics(captureColdStart = true)}. - *
This will create a metric with the key {@code "ColdStart"} and the unit type {@code COUNT}. - *

- * - *

To raise exception if no metrics are emitted, use {@code @Metrics(raiseOnEmptyMetrics = true)}. - *
This will create a create a exception of type {@link ValidationException}. By default its value is set to false. - *

- * - *

By default the service name associated with metrics created will be - * "service_undefined". This can be overridden with the environment variable {@code POWERTOOLS_SERVICE_NAME} - * or the annotation variable {@code @Metrics(service = "Service Name")}. - * If both are specified then the value of the annotation variable will be used.

- * - *

By default the namespace associated with metrics created will be "aws-embedded-metrics". - * This can be overridden with the environment variable {@code POWERTOOLS_METRICS_NAMESPACE} - * or the annotation variable {@code @Metrics(namespace = "Namespace")}. - * If both are specified then the value of the annotation variable will be used.

+ * Interface for metrics implementations. + * This interface is used to collect metrics in the Lambda function. + * It provides methods to add metrics, dimensions, and metadata. */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface Metrics { - String namespace() default ""; +public interface Metrics { + + /** + * Add a metric + * + * @param key the name of the metric + * @param value the value of the metric + * @param unit the unit of the metric + * @param resolution the resolution of the metric + */ + void addMetric(String key, double value, MetricUnit unit, MetricResolution resolution); + + /** + * Add a metric with default resolution + * + * @param key the name of the metric + * @param value the value of the metric + * @param unit the unit of the metric + */ + default void addMetric(String key, double value, MetricUnit unit) { + addMetric(key, value, unit, MetricResolution.STANDARD); + } + + /** + * Add a metric with default unit and resolution + * + * @param key the name of the metric + * @param value the value of the metric + */ + default void addMetric(String key, double value) { + addMetric(key, value, MetricUnit.NONE, MetricResolution.STANDARD); + } + + /** + * Add a dimension + * This is equivalent to calling {@code addDimension(DimensionSet.of(key, value))} + * + * @param key the name of the dimension + * @param value the value of the dimension + */ + default void addDimension(String key, String value) { + addDimension(DimensionSet.of(key, value)); + } + + /** + * Add a dimension set + * + * @param dimensionSet the dimension set to add + */ + void addDimension(DimensionSet dimensionSet); + + /** + * Set a custom timestamp for the metrics + * + * @param timestamp the timestamp to use for the metrics + */ + void setTimestamp(Instant timestamp); + + /** + * Add metadata + * + * @param key the name of the metadata + * @param value the value of the metadata + */ + void addMetadata(String key, Object value); + + /** + * Set default dimensions + * + * @param dimensionSet the dimension set to use as default dimensions + */ + void setDefaultDimensions(DimensionSet dimensionSet); + + /** + * Get the default dimensions + * + * @return the default dimensions as a DimensionSet + */ + DimensionSet getDefaultDimensions(); + + /** + * Set the namespace + * + * @param namespace the namespace + */ + void setNamespace(String namespace); + + /** + * Set whether to raise an exception if no metrics are emitted + * + * @param raiseOnEmptyMetrics true to raise an exception, false otherwise + */ + void setRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics); + + /** + * Clear default dimensions + */ + void clearDefaultDimensions(); + + /** + * Flush metrics to the configured sink + */ + void flush(); + + /** + * Capture cold start metric and flush immediately + * + * @param context Lambda context + * @param dimensions custom dimensions for this metric (optional) + */ + void captureColdStartMetric(Context context, DimensionSet dimensions); + + /** + * Capture cold start metric and flush immediately + * + * @param context Lambda context + */ + default void captureColdStartMetric(Context context) { + captureColdStartMetric(context, null); + } + + /** + * Capture cold start metric without Lambda context and flush immediately + * + * @param dimensions custom dimensions for this metric (optional) + */ + void captureColdStartMetric(DimensionSet dimensions); - String service() default ""; + /** + * Capture cold start metric without Lambda context and flush immediately + */ + default void captureColdStartMetric() { + captureColdStartMetric((DimensionSet) null); + } - boolean captureColdStart() default false; + /** + * Flush a single metric with custom dimensions. This creates a separate metrics context + * that doesn't affect the default metrics context. + * + * @param name the name of the metric + * @param value the value of the metric + * @param unit the unit of the metric + * @param namespace the namespace for the metric + * @param dimensions custom dimensions for this metric (optional) + */ + void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, DimensionSet dimensions); - boolean raiseOnEmptyMetrics() default false; + /** + * Flush a single metric with custom dimensions. This creates a separate metrics context + * that doesn't affect the default metrics context. + * + * @param name the name of the metric + * @param value the value of the metric + * @param unit the unit of the metric + * @param namespace the namespace for the metric + */ + default void flushSingleMetric(String name, double value, MetricUnit unit, String namespace) { + flushSingleMetric(name, value, unit, namespace, null); + } } diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsBuilder.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsBuilder.java new file mode 100644 index 000000000..2eb127b00 --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsBuilder.java @@ -0,0 +1,144 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics; + +import java.util.LinkedHashMap; +import java.util.Map; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; + +import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; + +/** + * Builder for configuring the singleton Metrics instance + */ +public class MetricsBuilder { + private MetricsProvider provider; + private String namespace; + private String service; + private boolean raiseOnEmptyMetrics = false; + private final Map defaultDimensions = new LinkedHashMap<>(); + + private MetricsBuilder() { + } + + /** + * Create a new builder instance + * + * @return a new builder instance + */ + public static MetricsBuilder builder() { + return new MetricsBuilder(); + } + + /** + * Set the metrics provider + * + * @param provider the metrics provider + * @return this builder + */ + public MetricsBuilder withMetricsProvider(MetricsProvider provider) { + this.provider = provider; + return this; + } + + /** + * Set the namespace + * + * @param namespace the namespace + * @return this builder + */ + public MetricsBuilder withNamespace(String namespace) { + this.namespace = namespace; + return this; + } + + /** + * Set the service name. Does not apply if used in combination with default dimensions. If you would like to use a + * service name with default dimensions, use {@link #withDefaultDimension(String, String)} instead. + * + * @param service the service name + * @return this builder + */ + public MetricsBuilder withService(String service) { + this.service = service; + return this; + } + + /** + * Set whether to raise an exception if no metrics are emitted + * + * @param raiseOnEmptyMetrics true to raise an exception, false otherwise + * @return this builder + */ + public MetricsBuilder withRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics) { + this.raiseOnEmptyMetrics = raiseOnEmptyMetrics; + return this; + } + + /** + * Add a default dimension. + * + * @param key the dimension key + * @param value the dimension value + * @return this builder + */ + public MetricsBuilder withDefaultDimension(String key, String value) { + this.defaultDimensions.put(key, value); + return this; + } + + /** + * Add default dimensions + * + * @param dimensionSet the dimension set to add + * @return this builder + */ + public MetricsBuilder withDefaultDimensions(DimensionSet dimensionSet) { + if (dimensionSet != null) { + this.defaultDimensions.putAll(dimensionSet.getDimensions()); + } + return this; + } + + /** + * Configure and return the singleton Metrics instance + * + * @return the configured singleton Metrics instance + */ + public Metrics build() { + if (provider != null) { + MetricsFactory.setMetricsProvider(provider); + } + + Metrics metrics = MetricsFactory.getMetricsInstance(); + + if (namespace != null) { + metrics.setNamespace(namespace); + } + + metrics.setRaiseOnEmptyMetrics(raiseOnEmptyMetrics); + + if (service != null) { + metrics.setDefaultDimensions(DimensionSet.of("Service", service)); + } + + // If the user provided default dimension, we overwrite the default Service dimension again + if (!defaultDimensions.isEmpty()) { + metrics.setDefaultDimensions(DimensionSet.of(defaultDimensions)); + } + + return metrics; + } +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java new file mode 100644 index 000000000..1fd5f88ca --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java @@ -0,0 +1,71 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics; + +import software.amazon.lambda.powertools.common.internal.LambdaConstants; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider; +import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; + +/** + * Factory for accessing the singleton Metrics instance + */ +public final class MetricsFactory { + private static MetricsProvider provider = new EmfMetricsProvider(); + private static Metrics metrics; + + private MetricsFactory() { + } + + /** + * Get the singleton instance of the Metrics + * + * @return the singleton Metrics instance + */ + public static synchronized Metrics getMetricsInstance() { + if (metrics == null) { + metrics = provider.getMetricsInstance(); + + // Apply default configuration from environment variables + String envNamespace = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); + if (envNamespace != null) { + metrics.setNamespace(envNamespace); + } + + // Only set Service dimension if it's not the default undefined value + String serviceName = LambdaHandlerProcessor.serviceName(); + if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) { + metrics.setDefaultDimensions(DimensionSet.of("Service", serviceName)); + } + } + + return metrics; + } + + /** + * Set the metrics provider + * + * @param metricsProvider the metrics provider + */ + public static synchronized void setMetricsProvider(MetricsProvider metricsProvider) { + if (metricsProvider == null) { + throw new IllegalArgumentException("Metrics provider cannot be null"); + } + provider = metricsProvider; + // Reset the metrics instance so it will be recreated with the new provider + metrics = null; + } +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsUtils.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsUtils.java deleted file mode 100644 index 7891b22ec..000000000 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsUtils.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics; - -import static java.util.Objects.requireNonNull; -import static java.util.Optional.ofNullable; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.getXrayTraceId; -import static software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspect.REQUEST_ID_PROPERTY; -import static software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspect.TRACE_ID_PROPERTY; - -import java.util.Arrays; -import java.util.Optional; -import java.util.function.Consumer; -import software.amazon.cloudwatchlogs.emf.config.SystemWrapper; -import software.amazon.cloudwatchlogs.emf.environment.EnvironmentProvider; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.MetricsContext; -import software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper; -import software.amazon.cloudwatchlogs.emf.model.Unit; - -/** - * A class used to retrieve the instance of the {@code MetricsLogger} used by - * {@code Metrics}. - *

- * {@see Metrics} - */ -public final class MetricsUtils { - private static final MetricsLogger metricsLogger = new MetricsLogger(); - private static DimensionSet[] defaultDimensions; - - private MetricsUtils() { - } - - /** - * The instance of the {@code MetricsLogger} used by {@code Metrics}. - * - * @return The instance of the MetricsLogger used by Metrics. - */ - public static MetricsLogger metricsLogger() { - return metricsLogger; - } - - /** - * Configure default dimension to be used by logger. - * By default, @{@link Metrics} annotation captures configured service as a dimension Service - * - * @param dimensionSets Default value of dimensions set for logger - */ - public static void defaultDimensions(final DimensionSet... dimensionSets) { - MetricsUtils.defaultDimensions = dimensionSets; - } - - /** - * Configure default dimension to be used by logger. - * By default, @{@link Metrics} annotation captures configured service as a dimension Service - * - * @param dimensionSet Default value of dimension set for logger - * @deprecated use {@link #defaultDimensions(DimensionSet...)} instead - */ - @Deprecated - public static void defaultDimensionSet(final DimensionSet dimensionSet) { - requireNonNull(dimensionSet, "Null dimension set not allowed"); - - if (dimensionSet.getDimensionKeys().size() > 0) { - defaultDimensions(dimensionSet); - } - } - - - /** - * Add and immediately flush a single metric. It will use the default namespace - * specified either on {@link Metrics} annotation or via POWERTOOLS_METRICS_NAMESPACE env var. - * It by default captures function_request_id as property if used together with {@link Metrics} annotation. It will also - * capture xray_trace_id as property if tracing is enabled. - * - * @param name the name of the metric - * @param value the value of the metric - * @param unit the unit type of the metric - * @param logger the MetricsLogger - */ - public static void withSingleMetric(final String name, - final double value, - final Unit unit, - final Consumer logger) { - withMetricsLogger(metricsLogger -> - { - metricsLogger.putMetric(name, value, unit); - logger.accept(metricsLogger); - }); - } - - /** - * Add and immediately flush a single metric. - * It by default captures function_request_id as property if used together with {@link Metrics} annotation. It will also - * capture xray_trace_id as property if tracing is enabled. - * - * @param name the name of the metric - * @param value the value of the metric - * @param unit the unit type of the metric - * @param namespace the namespace associated with the metric - * @param logger the MetricsLogger - */ - public static void withSingleMetric(final String name, - final double value, - final Unit unit, - final String namespace, - final Consumer logger) { - withMetricsLogger(metricsLogger -> - { - metricsLogger.setNamespace(namespace); - metricsLogger.putMetric(name, value, unit); - logger.accept(metricsLogger); - }); - } - - /** - * Provide and immediately flush a {@link MetricsLogger}. It uses the default namespace - * specified either on {@link Metrics} annotation or via POWERTOOLS_METRICS_NAMESPACE env var. - * It by default captures function_request_id as property if used together with {@link Metrics} annotation. It will also - * capture xray_trace_id as property if tracing is enabled. - * - * @param logger the MetricsLogger - */ - public static void withMetricsLogger(final Consumer logger) { - MetricsLogger metricsLogger = logger(); - - try { - metricsLogger.setNamespace(defaultNameSpace()); - captureRequestAndTraceId(metricsLogger); - logger.accept(metricsLogger); - } finally { - metricsLogger.flush(); - } - } - - /** - * Provide and immediately flush a {@link MetricsLogger}. It uses the default namespace - * specified either on {@link Metrics} annotation or via POWERTOOLS_METRICS_NAMESPACE env var. - * It by default captures function_request_id as property if used together with {@link Metrics} annotation. It will also - * capture xray_trace_id as property if tracing is enabled. - * - * @param logger the MetricsLogger - * @deprecated use {@link MetricsUtils#withMetricsLogger} instead - */ - @Deprecated - public static void withMetricLogger(final Consumer logger) { - withMetricsLogger(logger); - } - - public static DimensionSet[] getDefaultDimensions() { - return Arrays.copyOf(defaultDimensions, defaultDimensions.length); - } - - public static boolean hasDefaultDimension() { - return null != defaultDimensions; - } - - private static void captureRequestAndTraceId(MetricsLogger metricsLogger) { - awsRequestId(). - ifPresent(requestId -> metricsLogger.putProperty(REQUEST_ID_PROPERTY, requestId)); - - getXrayTraceId() - .ifPresent(traceId -> metricsLogger.putProperty(TRACE_ID_PROPERTY, traceId)); - } - - private static String defaultNameSpace() { - MetricsContext context = MetricsLoggerHelper.metricsContext(); - if ("aws-embedded-metrics".equals(context.getNamespace())) { - String namespace = SystemWrapper.getenv("POWERTOOLS_METRICS_NAMESPACE"); - return namespace != null ? namespace : "aws-embedded-metrics"; - } else { - return context.getNamespace(); - } - } - - private static Optional awsRequestId() { - MetricsContext context = MetricsLoggerHelper.metricsContext(); - return ofNullable(context.getProperty(REQUEST_ID_PROPERTY)) - .map(Object::toString); - } - - private static MetricsLogger logger() { - MetricsContext metricsContext = new MetricsContext(); - - if (hasDefaultDimension()) { - metricsContext.setDimensions(defaultDimensions); - } - - return new MetricsLogger(new EnvironmentProvider(), metricsContext); - } -} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/ValidationException.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/ValidationException.java deleted file mode 100644 index a553abbbd..000000000 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/ValidationException.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics; - -public class ValidationException extends RuntimeException { - - public ValidationException(String message) { - super(message); - } -} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java new file mode 100644 index 000000000..a55e1da5a --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java @@ -0,0 +1,326 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics.internal; + +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.getXrayTraceId; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isColdStart; + +import java.time.Instant; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.amazonaws.services.lambda.runtime.Context; + +import software.amazon.cloudwatchlogs.emf.environment.EnvironmentProvider; +import software.amazon.cloudwatchlogs.emf.model.DimensionSet; +import software.amazon.cloudwatchlogs.emf.model.MetricsContext; +import software.amazon.cloudwatchlogs.emf.model.StorageResolution; +import software.amazon.cloudwatchlogs.emf.model.Unit; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; + +/** + * Implementation of Metrics that uses the EMF library. Proxies Metrics interface calls to underlying + * library {@link software.amazon.cloudwatchlogs.emf.logger.MetricsLogger}. + */ +public class EmfMetricsLogger implements Metrics { + private static final Logger LOGGER = LoggerFactory.getLogger(EmfMetricsLogger.class); + private static final String TRACE_ID_PROPERTY = "xray_trace_id"; + private static final String REQUEST_ID_PROPERTY = "function_request_id"; + private static final String COLD_START_METRIC = "ColdStart"; + private static final String METRICS_DISABLED_ENV_VAR = "POWERTOOLS_METRICS_DISABLED"; + + private final software.amazon.cloudwatchlogs.emf.logger.MetricsLogger emfLogger; + private final EnvironmentProvider environmentProvider; + private boolean raiseOnEmptyMetrics = false; + private String namespace; + private Map defaultDimensions = new HashMap<>(); + private final AtomicBoolean hasMetrics = new AtomicBoolean(false); + + public EmfMetricsLogger(EnvironmentProvider environmentProvider, MetricsContext metricsContext) { + this.emfLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger(environmentProvider, + metricsContext); + this.environmentProvider = environmentProvider; + } + + @Override + public void addMetric(String key, double value, MetricUnit unit, MetricResolution resolution) { + StorageResolution storageResolution = resolution == MetricResolution.HIGH ? StorageResolution.HIGH + : StorageResolution.STANDARD; + emfLogger.putMetric(key, value, convertUnit(unit), storageResolution); + hasMetrics.set(true); + } + + @Override + public void addDimension(software.amazon.lambda.powertools.metrics.model.DimensionSet dimensionSet) { + if (dimensionSet == null) { + throw new IllegalArgumentException("DimensionSet cannot be null"); + } + + DimensionSet emfDimensionSet = new DimensionSet(); + dimensionSet.getDimensions().forEach((key, val) -> { + try { + emfDimensionSet.addDimension(key, val); + // Update our local copy of default dimensions + defaultDimensions.put(key, val); + } catch (Exception e) { + // Ignore dimension errors + } + }); + + emfLogger.putDimensions(emfDimensionSet); + } + + @Override + public void addMetadata(String key, Object value) { + emfLogger.putMetadata(key, value); + } + + @Override + public void setDefaultDimensions(software.amazon.lambda.powertools.metrics.model.DimensionSet dimensionSet) { + if (dimensionSet == null) { + throw new IllegalArgumentException("DimensionSet cannot be null"); + } + + DimensionSet emfDimensionSet = new DimensionSet(); + Map dimensions = dimensionSet.getDimensions(); + dimensions.forEach((key, value) -> { + try { + emfDimensionSet.addDimension(key, value); + } catch (Exception e) { + // Ignore dimension errors + } + }); + emfLogger.setDimensions(emfDimensionSet); + // Store a copy of the default dimensions + this.defaultDimensions = new LinkedHashMap<>(dimensions); + } + + @Override + public software.amazon.lambda.powertools.metrics.model.DimensionSet getDefaultDimensions() { + return software.amazon.lambda.powertools.metrics.model.DimensionSet.of(defaultDimensions); + } + + @Override + public void setNamespace(String namespace) { + Validator.validateNamespace(namespace); + + this.namespace = namespace; + try { + emfLogger.setNamespace(namespace); + } catch (Exception e) { + LOGGER.error("Namespace cannot be set due to an error in EMF", e); + } + } + + @Override + public void setRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics) { + this.raiseOnEmptyMetrics = raiseOnEmptyMetrics; + } + + @Override + public void setTimestamp(Instant timestamp) { + Validator.validateTimestamp(timestamp); + + emfLogger.setTimestamp(timestamp); + } + + @Override + public void clearDefaultDimensions() { + emfLogger.resetDimensions(false); + defaultDimensions.clear(); + } + + @Override + public void flush() { + if (isMetricsDisabled()) { + LOGGER.debug("Metrics are disabled, skipping flush"); + return; + } + + Validator.validateNamespace(namespace); + + if (!hasMetrics.get()) { + if (raiseOnEmptyMetrics) { + throw new IllegalStateException("No metrics were emitted"); + } else { + LOGGER.warn("No metrics were emitted"); + } + } + emfLogger.flush(); + } + + @Override + public void captureColdStartMetric(Context context, + software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) { + if (isColdStart()) { + if (isMetricsDisabled()) { + LOGGER.debug("Metrics are disabled, skipping cold start metric capture"); + return; + } + + Validator.validateNamespace(namespace); + + software.amazon.cloudwatchlogs.emf.logger.MetricsLogger coldStartLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger(); + + try { + coldStartLogger.setNamespace(namespace); + } catch (Exception e) { + LOGGER.error("Namespace cannot be set for cold start metrics due to an error in EMF", e); + } + + coldStartLogger.putMetric(COLD_START_METRIC, 1, Unit.COUNT); + + // Set dimensions if provided + if (dimensions != null) { + DimensionSet emfDimensionSet = new DimensionSet(); + dimensions.getDimensions().forEach((key, val) -> { + try { + emfDimensionSet.addDimension(key, val); + } catch (Exception e) { + // Ignore dimension errors + } + }); + coldStartLogger.setDimensions(emfDimensionSet); + } + + // Add request ID from context if available + if (context != null) { + coldStartLogger.putProperty(REQUEST_ID_PROPERTY, context.getAwsRequestId()); + } + + // Add trace ID using the standard logic + getXrayTraceId().ifPresent(traceId -> coldStartLogger.putProperty(TRACE_ID_PROPERTY, traceId)); + + coldStartLogger.flush(); + } + } + + @Override + public void captureColdStartMetric(software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) { + captureColdStartMetric(null, dimensions); + } + + @Override + public void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, + software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) { + if (isMetricsDisabled()) { + LOGGER.debug("Metrics are disabled, skipping single metric flush"); + return; + } + + Validator.validateNamespace(namespace); + + // Create a new logger for this single metric + software.amazon.cloudwatchlogs.emf.logger.MetricsLogger singleMetricLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger( + environmentProvider); + + try { + singleMetricLogger.setNamespace(namespace); + } catch (Exception e) { + LOGGER.error("Namespace cannot be set for single metric due to an error in EMF", e); + } + + // Add the metric + singleMetricLogger.putMetric(name, value, convertUnit(unit)); + + // Set dimensions if provided + if (dimensions != null) { + DimensionSet emfDimensionSet = new DimensionSet(); + dimensions.getDimensions().forEach((key, val) -> { + try { + emfDimensionSet.addDimension(key, val); + } catch (Exception e) { + // Ignore dimension errors + } + }); + singleMetricLogger.setDimensions(emfDimensionSet); + } + + // Flush the metric + singleMetricLogger.flush(); + } + + private boolean isMetricsDisabled() { + String disabledValue = System.getenv(METRICS_DISABLED_ENV_VAR); + return "true".equalsIgnoreCase(disabledValue); + } + + private Unit convertUnit(MetricUnit unit) { + switch (unit) { + case SECONDS: + return Unit.SECONDS; + case MICROSECONDS: + return Unit.MICROSECONDS; + case MILLISECONDS: + return Unit.MILLISECONDS; + case BYTES: + return Unit.BYTES; + case KILOBYTES: + return Unit.KILOBYTES; + case MEGABYTES: + return Unit.MEGABYTES; + case GIGABYTES: + return Unit.GIGABYTES; + case TERABYTES: + return Unit.TERABYTES; + case BITS: + return Unit.BITS; + case KILOBITS: + return Unit.KILOBITS; + case MEGABITS: + return Unit.MEGABITS; + case GIGABITS: + return Unit.GIGABITS; + case TERABITS: + return Unit.TERABITS; + case PERCENT: + return Unit.PERCENT; + case COUNT: + return Unit.COUNT; + case BYTES_SECOND: + return Unit.BYTES_SECOND; + case KILOBYTES_SECOND: + return Unit.KILOBYTES_SECOND; + case MEGABYTES_SECOND: + return Unit.MEGABYTES_SECOND; + case GIGABYTES_SECOND: + return Unit.GIGABYTES_SECOND; + case TERABYTES_SECOND: + return Unit.TERABYTES_SECOND; + case BITS_SECOND: + return Unit.BITS_SECOND; + case KILOBITS_SECOND: + return Unit.KILOBITS_SECOND; + case MEGABITS_SECOND: + return Unit.MEGABITS_SECOND; + case GIGABITS_SECOND: + return Unit.GIGABITS_SECOND; + case TERABITS_SECOND: + return Unit.TERABITS_SECOND; + case COUNT_SECOND: + return Unit.COUNT_SECOND; + case NONE: + default: + return Unit.NONE; + } + } +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java index 8ca069b01..b214d7c52 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java @@ -14,138 +14,129 @@ package software.amazon.lambda.powertools.metrics.internal; -import static software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper.dimensionsCount; -import static software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper.hasNoMetrics; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.coldStartDone; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.extractContext; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isColdStart; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.hasDefaultDimension; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.coldStartDone; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.extractContext; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isHandlerMethod; -import com.amazonaws.services.lambda.runtime.Context; -import java.lang.reflect.Field; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.MetricsContext; -import software.amazon.cloudwatchlogs.emf.model.Unit; -import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor; + +import com.amazonaws.services.lambda.runtime.Context; + +import software.amazon.lambda.powertools.common.internal.LambdaConstants; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; -import software.amazon.lambda.powertools.metrics.MetricsUtils; -import software.amazon.lambda.powertools.metrics.ValidationException; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; @Aspect public class LambdaMetricsAspect { public static final String TRACE_ID_PROPERTY = "xray_trace_id"; public static final String REQUEST_ID_PROPERTY = "function_request_id"; - private static final String NAMESPACE = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); - - private static String service(Metrics metrics) { - return !"".equals(metrics.service()) ? metrics.service() : serviceName(); - } + private static final String SERVICE_DIMENSION = "Service"; + private static final String FUNCTION_NAME_ENV_VAR = "POWERTOOLS_METRICS_FUNCTION_NAME"; - // This can be simplified after this issues https://github.com/awslabs/aws-embedded-metrics-java/issues/35 is fixed - public static void refreshMetricsContext(Metrics metrics) { - try { - Field f = metricsLogger().getClass().getDeclaredField("context"); - f.setAccessible(true); - MetricsContext context = new MetricsContext(); + private String functionName(FlushMetrics metrics, Context context) { + if (!"".equals(metrics.functionName())) { + return metrics.functionName(); + } - DimensionSet[] defaultDimensions = hasDefaultDimension() ? MetricsUtils.getDefaultDimensions() - : new DimensionSet[] {DimensionSet.of("Service", service(metrics))}; + String envFunctionName = System.getenv(FUNCTION_NAME_ENV_VAR); + if (envFunctionName != null && !envFunctionName.isEmpty()) { + return envFunctionName; + } - context.setDimensions(defaultDimensions); + return context != null ? context.getFunctionName() : null; + } - f.set(metricsLogger(), context); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); + private String serviceNameWithFallback(FlushMetrics metrics) { + if (!"".equals(metrics.service())) { + return metrics.service(); } + return LambdaHandlerProcessor.serviceName(); } - @SuppressWarnings({"EmptyMethod"}) + @SuppressWarnings({ "EmptyMethod" }) @Pointcut("@annotation(metrics)") - public void callAt(Metrics metrics) { + public void callAt(FlushMetrics metrics) { + // AspectJ point cut referenced in around() method } - @Around(value = "callAt(metrics) && execution(@Metrics * *.*(..))", argNames = "pjp,metrics") + @Around(value = "callAt(metrics) && execution(@FlushMetrics * *.*(..))", argNames = "pjp,metrics") public Object around(ProceedingJoinPoint pjp, - Metrics metrics) throws Throwable { + FlushMetrics metrics) throws Throwable { Object[] proceedArgs = pjp.getArgs(); if (isHandlerMethod(pjp)) { + Metrics metricsInstance = MetricsFactory.getMetricsInstance(); - MetricsLogger logger = metricsLogger(); - - refreshMetricsContext(metrics); - - logger.setNamespace(namespace(metrics)); - - Context extractedContext = extractContext(pjp); + // The MetricsFactory applies default settings from the environment or can be configured by the + // MetricsBuilder. We only overwrite settings if they are explicitly set in the @FlushMetrics + // annotation. + if (!"".equals(metrics.namespace())) { + metricsInstance.setNamespace(metrics.namespace()); + } - if (null != extractedContext) { - coldStartSingleMetricIfApplicable(extractedContext.getAwsRequestId(), - extractedContext.getFunctionName(), metrics); - logger.putProperty(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); + // We only overwrite the default dimensions if the user didn't overwrite them previously. This means that + // they are either empty or only contain the default "Service" dimension. + if (!"".equals(metrics.service().trim()) + && (metricsInstance.getDefaultDimensions().getDimensionKeys().size() <= 1 + || metricsInstance.getDefaultDimensions().getDimensionKeys().contains(SERVICE_DIMENSION))) { + metricsInstance.setDefaultDimensions(DimensionSet.of(SERVICE_DIMENSION, metrics.service())); } + metricsInstance.setRaiseOnEmptyMetrics(metrics.raiseOnEmptyMetrics()); + + // Add trace ID metadata if available LambdaHandlerProcessor.getXrayTraceId() - .ifPresent(traceId -> logger.putProperty(TRACE_ID_PROPERTY, traceId)); + .ifPresent(traceId -> metricsInstance.addMetadata(TRACE_ID_PROPERTY, traceId)); + + captureColdStartMetricIfEnabled(extractContext(pjp), metrics); try { return pjp.proceed(proceedArgs); - } finally { coldStartDone(); - validateMetricsAndRefreshOnFailure(metrics); - logger.flush(); - refreshMetricsContext(metrics); + metricsInstance.flush(); } } return pjp.proceed(proceedArgs); } - private void coldStartSingleMetricIfApplicable(final String awsRequestId, - final String functionName, - final Metrics metrics) { - if (metrics.captureColdStart() - && isColdStart()) { - MetricsLogger metricsLogger = new MetricsLogger(); - metricsLogger.setNamespace(namespace(metrics)); - metricsLogger.putMetric("ColdStart", 1, Unit.COUNT); - metricsLogger.setDimensions(DimensionSet.of("Service", service(metrics), "FunctionName", functionName)); - metricsLogger.putProperty(REQUEST_ID_PROPERTY, awsRequestId); - metricsLogger.flush(); + private void captureColdStartMetricIfEnabled(Context extractedContext, FlushMetrics metrics) { + if (extractedContext == null) { + return; } - } + Metrics metricsInstance = MetricsFactory.getMetricsInstance(); + metricsInstance.addMetadata(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); - private void validateBeforeFlushingMetrics(Metrics metrics) { - if (metrics.raiseOnEmptyMetrics() && hasNoMetrics()) { - throw new ValidationException("No metrics captured, at least one metrics must be emitted"); - } + // Only capture cold start metrics if enabled on annotation + if (metrics.captureColdStart()) { + // Get function name from annotation or context + String funcName = functionName(metrics, extractedContext); - if (dimensionsCount() > 9) { - throw new ValidationException(String.format("Number of Dimensions must be in range of 0-9." + - " Actual size: %d.", dimensionsCount())); - } - } + DimensionSet coldStartDimensions = new DimensionSet(); - private String namespace(Metrics metrics) { - return !"".equals(metrics.namespace()) ? metrics.namespace() : NAMESPACE; - } + // Get service name from metrics instance default dimensions or fallback + String serviceName = metricsInstance.getDefaultDimensions().getDimensions().getOrDefault( + SERVICE_DIMENSION, + serviceNameWithFallback(metrics)); + + // Only add service if it is not undefined + if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) { + coldStartDimensions.addDimension(SERVICE_DIMENSION, serviceName); + } + + // Add function name + coldStartDimensions.addDimension("FunctionName", + funcName != null ? funcName : extractedContext.getFunctionName()); - private void validateMetricsAndRefreshOnFailure(Metrics metrics) { - try { - validateBeforeFlushingMetrics(metrics); - } catch (ValidationException e) { - refreshMetricsContext(metrics); - throw e; + metricsInstance.captureColdStartMetric(extractedContext, coldStartDimensions); } } } diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/Validator.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/Validator.java new file mode 100644 index 000000000..eebb54739 --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/Validator.java @@ -0,0 +1,135 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics.internal; + +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang3.StringUtils; + +/** + * Utility class for validating metrics-related parameters. + */ +public class Validator { + private static final int MAX_DIMENSION_NAME_LENGTH = 250; + private static final int MAX_DIMENSION_VALUE_LENGTH = 1024; + private static final int MAX_NAMESPACE_LENGTH = 255; + private static final String NAMESPACE_REGEX = "^[a-zA-Z0-9._#:/-]+$"; + public static final long MAX_TIMESTAMP_PAST_AGE_SECONDS = TimeUnit.DAYS.toSeconds(14); + public static final long MAX_TIMESTAMP_FUTURE_AGE_SECONDS = TimeUnit.HOURS.toSeconds(2); + + private Validator() { + // Private constructor to prevent instantiation + } + + /** + * Validates that a namespace is properly specified. + * + * @param namespace The namespace to validate + * @throws IllegalArgumentException if the namespace is invalid + */ + public static void validateNamespace(String namespace) { + if (namespace == null || namespace.trim().isEmpty()) { + throw new IllegalArgumentException("Namespace must be specified before flushing metrics"); + } + + if (namespace.length() > MAX_NAMESPACE_LENGTH) { + throw new IllegalArgumentException( + "Namespace exceeds maximum length of " + MAX_NAMESPACE_LENGTH + ": " + namespace); + } + + if (!namespace.matches(NAMESPACE_REGEX)) { + throw new IllegalArgumentException("Namespace contains invalid characters: " + namespace); + } + } + + /** + * Validates Timestamp. + * + * @see CloudWatch + * Timestamp + * @param timestamp Timestamp + * @throws IllegalArgumentException if timestamp is invalid + */ + public static void validateTimestamp(Instant timestamp) { + if (timestamp == null) { + throw new IllegalArgumentException("Timestamp cannot be null"); + } + + if (timestamp.isAfter( + Instant.now().plusSeconds(MAX_TIMESTAMP_FUTURE_AGE_SECONDS))) { + throw new IllegalArgumentException( + "Timestamp cannot be more than " + + MAX_TIMESTAMP_FUTURE_AGE_SECONDS + + " seconds in the future"); + } + + if (timestamp.isBefore( + Instant.now().minusSeconds(MAX_TIMESTAMP_PAST_AGE_SECONDS))) { + throw new IllegalArgumentException( + "Timestamp cannot be more than " + + MAX_TIMESTAMP_PAST_AGE_SECONDS + + " seconds in the past"); + } + } + + /** + * Validates a dimension key-value pair. + * + * @param key The dimension key to validate + * @param value The dimension value to validate + * @throws IllegalArgumentException if the key or value is invalid + */ + public static void validateDimension(String key, String value) { + if (key == null || key.trim().isEmpty()) { + throw new IllegalArgumentException("Dimension key cannot be null or empty"); + } + + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException("Dimension value cannot be null or empty"); + } + + if (StringUtils.containsWhitespace(key)) { + throw new IllegalArgumentException("Dimension key cannot contain whitespaces: " + key); + } + + if (StringUtils.containsWhitespace(value)) { + throw new IllegalArgumentException("Dimension value cannot contain whitespaces: " + value); + } + + if (key.startsWith(":")) { + throw new IllegalArgumentException("Dimension key cannot start with colon: " + key); + } + + if (key.length() > MAX_DIMENSION_NAME_LENGTH) { + throw new IllegalArgumentException( + "Dimension name exceeds maximum length of " + MAX_DIMENSION_NAME_LENGTH + ": " + key); + } + + if (value.length() > MAX_DIMENSION_VALUE_LENGTH) { + throw new IllegalArgumentException( + "Dimension value exceeds maximum length of " + MAX_DIMENSION_VALUE_LENGTH + ": " + value); + } + + if (!StringUtils.isAsciiPrintable(key)) { + throw new IllegalArgumentException("Dimension name has invalid characters: " + key); + } + + if (!StringUtils.isAsciiPrintable(value)) { + throw new IllegalArgumentException("Dimension value has invalid characters: " + value); + } + } +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/DimensionSet.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/DimensionSet.java new file mode 100644 index 000000000..e93f34237 --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/DimensionSet.java @@ -0,0 +1,193 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics.model; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import software.amazon.lambda.powertools.metrics.internal.Validator; + +/** + * Represents a set of dimensions for CloudWatch metrics + */ +public class DimensionSet { + private static final int MAX_DIMENSION_SET_SIZE = 30; + + private final Map dimensions = new LinkedHashMap<>(); + + /** + * Create a dimension set with a single key-value pair + * + * @param key dimension key + * @param value dimension value + * @return a new DimensionSet + */ + public static DimensionSet of(String key, String value) { + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension(key, value); + return dimensionSet; + } + + /** + * Create a dimension set with two key-value pairs + * + * @param key1 first dimension key + * @param value1 first dimension value + * @param key2 second dimension key + * @param value2 second dimension value + * @return a new DimensionSet + */ + public static DimensionSet of(String key1, String value1, String key2, String value2) { + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension(key1, value1); + dimensionSet.addDimension(key2, value2); + return dimensionSet; + } + + /** + * Create a dimension set with three key-value pairs + * + * @param key1 first dimension key + * @param value1 first dimension value + * @param key2 second dimension key + * @param value2 second dimension value + * @param key3 third dimension key + * @param value3 third dimension value + * @return a new DimensionSet + */ + public static DimensionSet of(String key1, String value1, String key2, String value2, String key3, String value3) { + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension(key1, value1); + dimensionSet.addDimension(key2, value2); + dimensionSet.addDimension(key3, value3); + return dimensionSet; + } + + /** + * Create a dimension set with four key-value pairs + * + * @param key1 first dimension key + * @param value1 first dimension value + * @param key2 second dimension key + * @param value2 second dimension value + * @param key3 third dimension key + * @param value3 third dimension value + * @param key4 fourth dimension key + * @param value4 fourth dimension value + * @return a new DimensionSet + */ + public static DimensionSet of(String key1, String value1, String key2, String value2, + String key3, String value3, String key4, String value4) { + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension(key1, value1); + dimensionSet.addDimension(key2, value2); + dimensionSet.addDimension(key3, value3); + dimensionSet.addDimension(key4, value4); + return dimensionSet; + } + + /** + * Create a dimension set with five key-value pairs + * + * @param key1 first dimension key + * @param value1 first dimension value + * @param key2 second dimension key + * @param value2 second dimension value + * @param key3 third dimension key + * @param value3 third dimension value + * @param key4 fourth dimension key + * @param value4 fourth dimension value + * @param key5 fifth dimension key + * @param value5 fifth dimension value + * @return a new DimensionSet + */ + public static DimensionSet of(String key1, String value1, String key2, String value2, + String key3, String value3, String key4, String value4, + String key5, String value5) { + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension(key1, value1); + dimensionSet.addDimension(key2, value2); + dimensionSet.addDimension(key3, value3); + dimensionSet.addDimension(key4, value4); + dimensionSet.addDimension(key5, value5); + return dimensionSet; + } + + /** + * Create a dimension set from a map of key-value pairs + * + * @param dimensions map of dimension key-value pairs + * @return a new DimensionSet + */ + public static DimensionSet of(Map dimensions) { + DimensionSet dimensionSet = new DimensionSet(); + dimensions.forEach(dimensionSet::addDimension); + return dimensionSet; + } + + /** + * Add a dimension to this dimension set + * + * @param key dimension key + * @param value dimension value + * @return this dimension set for chaining + * @throws IllegalArgumentException if key or value is invalid + * @throws IllegalStateException if adding would exceed the maximum number of dimensions + */ + public DimensionSet addDimension(String key, String value) { + validateDimension(key, value); + + if (dimensions.size() >= MAX_DIMENSION_SET_SIZE) { + throw new IllegalStateException( + "Cannot exceed " + MAX_DIMENSION_SET_SIZE + " dimensions per dimension set"); + } + + dimensions.put(key, value); + return this; + } + + /** + * Get the dimension keys in this dimension set + * + * @return set of dimension keys + */ + public Set getDimensionKeys() { + return dimensions.keySet(); + } + + /** + * Get the value for a dimension key + * + * @param key dimension key + * @return dimension value or null if not found + */ + public String getDimensionValue(String key) { + return dimensions.get(key); + } + + /** + * Get the dimensions as a map. Creates a shallow copy + * + * @return map of dimensions + */ + public Map getDimensions() { + return new LinkedHashMap<>(dimensions); + } + + private void validateDimension(String key, String value) { + Validator.validateDimension(key, value); + } +} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/exception/SkippedMessageDueToFailedBatchException.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricResolution.java similarity index 67% rename from powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/exception/SkippedMessageDueToFailedBatchException.java rename to powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricResolution.java index fbb4289d8..db514c8b7 100644 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/exception/SkippedMessageDueToFailedBatchException.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricResolution.java @@ -12,15 +12,21 @@ * */ -package software.amazon.lambda.powertools.sqs.exception; +package software.amazon.lambda.powertools.metrics.model; /** - * Indicates that a message has been skipped due to the batch it is - * within failing. + * Resolution for metrics */ -public class SkippedMessageDueToFailedBatchException extends Exception { +public enum MetricResolution { + STANDARD(60), HIGH(1); - public SkippedMessageDueToFailedBatchException() { + private final int seconds; + + MetricResolution(int seconds) { + this.seconds = seconds; } + public int getSeconds() { + return seconds; + } } diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricUnit.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricUnit.java new file mode 100644 index 000000000..445d950b2 --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricUnit.java @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics.model; + +/** + * Metric units supported by CloudWatch + */ +public enum MetricUnit { + SECONDS("Seconds"), + MICROSECONDS("Microseconds"), + MILLISECONDS("Milliseconds"), + BYTES("Bytes"), + KILOBYTES("Kilobytes"), + MEGABYTES("Megabytes"), + GIGABYTES("Gigabytes"), + TERABYTES("Terabytes"), + BITS("Bits"), + KILOBITS("Kilobits"), + MEGABITS("Megabits"), + GIGABITS("Gigabits"), + TERABITS("Terabits"), + PERCENT("Percent"), + COUNT("Count"), + BYTES_SECOND("Bytes/Second"), + KILOBYTES_SECOND("Kilobytes/Second"), + MEGABYTES_SECOND("Megabytes/Second"), + GIGABYTES_SECOND("Gigabytes/Second"), + TERABYTES_SECOND("Terabytes/Second"), + BITS_SECOND("Bits/Second"), + KILOBITS_SECOND("Kilobits/Second"), + MEGABITS_SECOND("Megabits/Second"), + GIGABITS_SECOND("Gigabits/Second"), + TERABITS_SECOND("Terabits/Second"), + COUNT_SECOND("Count/Second"), + NONE("None"); + + private final String name; + + MetricUnit(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProvider.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProvider.java new file mode 100644 index 000000000..12c99b18f --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics.provider; + +import software.amazon.cloudwatchlogs.emf.environment.EnvironmentProvider; +import software.amazon.cloudwatchlogs.emf.model.MetricsContext; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.internal.EmfMetricsLogger; + +/** + * Provider implementation for EMF metrics + */ +public class EmfMetricsProvider implements MetricsProvider { + + @Override + public Metrics getMetricsInstance() { + return new EmfMetricsLogger(new EnvironmentProvider(), new MetricsContext()); + } +} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SampleSqsHandler.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/MetricsProvider.java similarity index 63% rename from powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SampleSqsHandler.java rename to powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/MetricsProvider.java index 557aa214d..e6c79e000 100644 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SampleSqsHandler.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/MetricsProvider.java @@ -12,15 +12,19 @@ * */ -package software.amazon.lambda.powertools.sqs; +package software.amazon.lambda.powertools.metrics.provider; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import software.amazon.lambda.powertools.metrics.Metrics; -public class SampleSqsHandler implements SqsMessageHandler { - private int counter; +/** + * Interface for metrics provider implementations + */ +public interface MetricsProvider { - @Override - public String process(SQSEvent.SQSMessage message) { - return String.valueOf(counter++); - } + /** + * Get a new instance of a metrics implementation + * + * @return a new metrics instance + */ + Metrics getMetricsInstance(); } diff --git a/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/jni-config.json b/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/jni-config.json new file mode 100644 index 000000000..75ee9e5f5 --- /dev/null +++ b/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/jni-config.json @@ -0,0 +1,33 @@ +[ + { + "name": "java.lang.Boolean", + "methods": [{ "name": "getBoolean", "parameterTypes": ["java.lang.String"] }] + }, + { + "name": "java.lang.String", + "methods": [ + { "name": "lastIndexOf", "parameterTypes": ["int"] }, + { "name": "substring", "parameterTypes": ["int"] } + ] + }, + { + "name": "java.lang.System", + "methods": [ + { "name": "getProperty", "parameterTypes": ["java.lang.String"] }, + { "name": "setProperty", "parameterTypes": ["java.lang.String", "java.lang.String"] } + ] + }, + { + "name": "sun.management.VMManagementImpl", + "fields": [ + { "name": "compTimeMonitoringSupport" }, + { "name": "currentThreadCpuTimeSupport" }, + { "name": "objectMonitorUsageSupport" }, + { "name": "otherThreadCpuTimeSupport" }, + { "name": "remoteDiagnosticCommandsSupport" }, + { "name": "synchronizerUsageSupport" }, + { "name": "threadAllocatedMemorySupport" }, + { "name": "threadContentionMonitoringSupport" } + ] + } +] diff --git a/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/reflect-config.json b/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/reflect-config.json new file mode 100644 index 000000000..43b2822d6 --- /dev/null +++ b/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/reflect-config.json @@ -0,0 +1,152 @@ +[ + { + "name": "com.amazonaws.services.lambda.runtime.Context", + "allDeclaredClasses": true, + "queryAllPublicMethods": true + }, + { + "name": "com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "java.io.Serializable", + "queryAllDeclaredMethods": true + }, + { + "name": "java.lang.Comparable", + "queryAllDeclaredMethods": true + }, + { + "name": "java.lang.Double", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "java.lang.Number", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "java.lang.ProcessEnvironment", + "fields": [{ "name": "theCaseInsensitiveEnvironment" }, { "name": "theEnvironment" }] + }, + { + "name": "java.lang.String" + }, + { + "name": "java.lang.constant.Constable", + "queryAllDeclaredMethods": true + }, + { + "name": "java.lang.constant.ConstantDesc", + "queryAllDeclaredMethods": true + }, + { + "name": "java.util.Collections$UnmodifiableMap", + "fields": [{ "name": "m" }] + }, + { + "name": "java.util.Map" + }, + { + "name": "java.util.concurrent.atomic.AtomicBoolean", + "fields": [{ "name": "value" }] + }, + { + "name": "java.util.concurrent.atomic.AtomicReference", + "fields": [{ "name": "value" }] + }, + { + "name": "java.util.function.Consumer", + "queryAllPublicMethods": true + }, + { + "name": "org.apiguardian.api.API", + "queryAllPublicMethods": true + }, + { + "name": "software.amazon.cloudwatchlogs.emf.model.Metadata", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "getCloudWatchMetrics", "parameterTypes": [] }, + { "name": "getCustomMetadata", "parameterTypes": [] }, + { "name": "getTimestamp", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.model.MetricDefinition", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "getName", "parameterTypes": [] }, + { "name": "getStorageResolution", "parameterTypes": [] }, + { "name": "getUnit", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.model.MetricDirective", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "getAllDimensionKeys", "parameterTypes": [] }, + { "name": "getAllMetrics", "parameterTypes": [] }, + { "name": "getNamespace", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.model.RootNode", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "getAws", "parameterTypes": [] }, + { "name": "getTargetMembers", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.serializers.InstantSerializer", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.serializers.StorageResolutionFilter", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.serializers.StorageResolutionSerializer", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.serializers.UnitSerializer", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor", + "fields": [{ "name": "IS_COLD_START" }], + "methods": [{ "name": "resetServiceName", "parameterTypes": [] }] + }, + { + "name": "software.amazon.lambda.powertools.metrics.Metrics", + "allDeclaredClasses": true, + "queryAllPublicMethods": true + }, + { + "name": "software.amazon.lambda.powertools.metrics.MetricsFactory", + "fields": [{ "name": "metrics" }, { "name": "provider" }] + }, + { + "name": "software.amazon.lambda.powertools.metrics.internal.EmfMetricsLogger", + "methods": [ + { "name": "convertUnit", "parameterTypes": ["software.amazon.lambda.powertools.metrics.model.MetricUnit"] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.provider.MetricsProvider", + "allDeclaredClasses": true, + "queryAllPublicMethods": true + } +] diff --git a/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/resource-config.json b/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/resource-config.json new file mode 100644 index 000000000..d47298855 --- /dev/null +++ b/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/resource-config.json @@ -0,0 +1,13 @@ +{ + "resources": { + "includes": [ + { + "pattern": "\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, + { + "pattern": "\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + } + ] + }, + "bundles": [] +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/ConfigurationPrecedenceTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/ConfigurationPrecedenceTest.java new file mode 100644 index 000000000..1bf3b6a69 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/ConfigurationPrecedenceTest.java @@ -0,0 +1,206 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.SetEnvironmentVariable; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.metrics.testutils.TestContext; + +/** + * Tests to verify the hierarchy of precedence for configuration: + * 1. @FlushMetrics annotation + * 2. MetricsBuilder + * 3. Environment variables + */ +class ConfigurationPrecedenceTest { + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() throws Exception { + System.setOut(new PrintStream(outputStreamCaptor)); + + // Reset LambdaHandlerProcessor's SERVICE_NAME + Method resetServiceName = LambdaHandlerProcessor.class.getDeclaredMethod("resetServiceName"); + resetServiceName.setAccessible(true); + resetServiceName.invoke(null); + + // Reset IS_COLD_START + java.lang.reflect.Field coldStartField = LambdaHandlerProcessor.class.getDeclaredField("IS_COLD_START"); + coldStartField.setAccessible(true); + coldStartField.set(null, null); + } + + @AfterEach + void tearDown() throws Exception { + System.setOut(standardOut); + + // Reset the singleton state between tests + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); + field.setAccessible(true); + field.set(null, null); + + field = MetricsFactory.class.getDeclaredField("provider"); + field.setAccessible(true); + field.set(null, new software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider()); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "EnvNamespace") + @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = "EnvService") + void annotationShouldOverrideBuilderAndEnvironment() throws Exception { + // Given + // Configure with builder first + MetricsBuilder.builder() + .withNamespace("BuilderNamespace") + .withService("BuilderService") + .build(); + + RequestHandler, String> handler = new HandlerWithMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // Annotation values should take precedence + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("AnnotationNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("AnnotationService"); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "EnvNamespace") + @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = "EnvService") + void builderShouldOverrideEnvironment() throws Exception { + // Given + // Configure with builder + MetricsBuilder.builder() + .withNamespace("BuilderNamespace") + .withService("BuilderService") + .build(); + + RequestHandler, String> handler = new HandlerWithDefaultMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // Builder values should take precedence over environment variables + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("BuilderNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("BuilderService"); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "EnvNamespace") + @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = "EnvService") + void environmentVariablesShouldBeUsedWhenNoOverrides() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithDefaultMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // Environment variable values should be used + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("EnvNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("EnvService"); + } + + @Test + void shouldUseDefaultsWhenNoConfiguration() throws Exception { + // Given + MetricsBuilder.builder() + .withNamespace("TestNamespace") + .build(); + + RequestHandler, String> handler = new HandlerWithDefaultMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // Default values should be used + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("TestNamespace"); + // Service dimension should not be present when service is undefined + assertThat(rootNode.has("Service")).isFalse(); + } + + private static class HandlerWithMetricsAnnotation implements RequestHandler, String> { + @Override + @FlushMetrics(namespace = "AnnotationNamespace", service = "AnnotationService") + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } + + private static class HandlerWithDefaultMetricsAnnotation implements RequestHandler, String> { + @Override + @FlushMetrics + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } + +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsBuilderTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsBuilderTest.java new file mode 100644 index 000000000..bd300fb6b --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsBuilderTest.java @@ -0,0 +1,186 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; +import software.amazon.lambda.powertools.metrics.testutils.TestMetrics; +import software.amazon.lambda.powertools.metrics.testutils.TestMetricsProvider; + +class MetricsBuilderTest { + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @AfterEach + void tearDown() throws Exception { + System.setOut(standardOut); + + // Reset the singleton state between tests + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); + field.setAccessible(true); + field.set(null, null); + + field = MetricsFactory.class.getDeclaredField("provider"); + field.setAccessible(true); + field.set(null, new software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider()); + } + + @Test + void shouldBuildWithCustomNamespace() throws Exception { + // When + Metrics metrics = MetricsBuilder.builder() + .withNamespace("CustomNamespace") + .build(); + + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("CustomNamespace"); + } + + @Test + void shouldBuildWithCustomService() throws Exception { + // When + Metrics metrics = MetricsBuilder.builder() + .withService("CustomService") + .withNamespace("TestNamespace") + .build(); + + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("CustomService"); + } + + @Test + void shouldBuildWithRaiseOnEmptyMetrics() { + // When + Metrics metrics = MetricsBuilder.builder() + .withRaiseOnEmptyMetrics(true) + .withNamespace("TestNamespace") + .build(); + + // Then + assertThat(metrics).isNotNull(); + assertThatThrownBy(metrics::flush) + .isInstanceOf(IllegalStateException.class) + .hasMessage("No metrics were emitted"); + } + + @Test + void shouldBuildWithDefaultDimension() throws Exception { + // When + Metrics metrics = MetricsBuilder.builder() + .withDefaultDimension("Environment", "Test") + .withNamespace("TestNamespace") + .build(); + + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Environment")).isTrue(); + assertThat(rootNode.get("Environment").asText()).isEqualTo("Test"); + } + + @Test + void shouldBuildWithMultipleDefaultDimensions() throws Exception { + // When + Metrics metrics = MetricsBuilder.builder() + .withDefaultDimensions(DimensionSet.of("Environment", "Test", "Region", "us-west-2")) + .withNamespace("TestNamespace") + .build(); + + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Environment")).isTrue(); + assertThat(rootNode.get("Environment").asText()).isEqualTo("Test"); + assertThat(rootNode.has("Region")).isTrue(); + assertThat(rootNode.get("Region").asText()).isEqualTo("us-west-2"); + } + + @Test + void shouldBuildWithCustomMetricsProvider() { + // Given + MetricsProvider testProvider = new TestMetricsProvider(); + + // When + Metrics metrics = MetricsBuilder.builder() + .withMetricsProvider(testProvider) + .build(); + + // Then + assertThat(metrics).isInstanceOf(TestMetrics.class); + } + + @Test + void shouldOverrideServiceWithDefaultDimensions() throws Exception { + // When + Metrics metrics = MetricsBuilder.builder() + .withService("OriginalService") + .withDefaultDimensions(DimensionSet.of("Service", "OverriddenService")) + .withNamespace("TestNamespace") + .build(); + + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("OverriddenService"); + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsFactoryTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsFactoryTest.java new file mode 100644 index 000000000..962f2c2d7 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsFactoryTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.SetEnvironmentVariable; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; +import software.amazon.lambda.powertools.metrics.testutils.TestMetrics; +import software.amazon.lambda.powertools.metrics.testutils.TestMetricsProvider; + +class MetricsFactoryTest { + + private static final String TEST_NAMESPACE = "TestNamespace"; + private static final String TEST_SERVICE = "TestService"; + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() throws Exception { + System.setOut(new PrintStream(outputStreamCaptor)); + + // Reset LambdaHandlerProcessor's SERVICE_NAME + Method resetServiceName = LambdaHandlerProcessor.class.getDeclaredMethod("resetServiceName"); + resetServiceName.setAccessible(true); + resetServiceName.invoke(null); + + // Reset IS_COLD_START + java.lang.reflect.Field coldStartField = LambdaHandlerProcessor.class.getDeclaredField("IS_COLD_START"); + coldStartField.setAccessible(true); + coldStartField.set(null, null); + } + + @AfterEach + void tearDown() throws Exception { + System.setOut(standardOut); + + // Reset the singleton state between tests + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); + field.setAccessible(true); + field.set(null, null); + + field = MetricsFactory.class.getDeclaredField("provider"); + field.setAccessible(true); + field.set(null, new software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider()); + } + + @Test + void shouldGetMetricsInstance() { + // When + Metrics metrics = MetricsFactory.getMetricsInstance(); + + // Then + assertThat(metrics).isNotNull(); + } + + @Test + void shouldReturnSameInstanceOnMultipleCalls() { + // When + Metrics firstInstance = MetricsFactory.getMetricsInstance(); + Metrics secondInstance = MetricsFactory.getMetricsInstance(); + + // Then + assertThat(firstInstance).isSameAs(secondInstance); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = TEST_NAMESPACE) + void shouldUseNamespaceFromEnvironmentVariable() throws Exception { + // When + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo(TEST_NAMESPACE); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = TEST_SERVICE) + void shouldUseServiceNameFromEnvironmentVariable() throws Exception { + // When + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.setNamespace("TestNamespace"); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo(TEST_SERVICE); + } + + @Test + void shouldSetCustomMetricsProvider() { + // Given + MetricsProvider testProvider = new TestMetricsProvider(); + + // When + MetricsFactory.setMetricsProvider(testProvider); + Metrics metrics = MetricsFactory.getMetricsInstance(); + + // Then + assertThat(metrics).isInstanceOf(TestMetrics.class); + } + + @Test + void shouldThrowExceptionWhenSettingNullProvider() { + // When/Then + assertThatThrownBy(() -> MetricsFactory.setMetricsProvider(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metrics provider cannot be null"); + } + + @Test + void shouldNotSetServiceDimensionWhenServiceUndefined() throws Exception { + // Given - no POWERTOOLS_SERVICE_NAME set, so it will use the default undefined value + + // When + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.setNamespace("TestNamespace"); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // Service dimension should not be present + assertThat(rootNode.has("Service")).isFalse(); + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java deleted file mode 100644 index ce9d63cfd..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics; - -import static java.util.Collections.emptyMap; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; -import static org.mockito.Mockito.mockStatic; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getProperty; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; -import java.util.Map; -import java.util.function.Consumer; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import software.amazon.cloudwatchlogs.emf.config.SystemWrapper; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; - -class MetricsLoggerTest { - - private final ByteArrayOutputStream out = new ByteArrayOutputStream(); - private final PrintStream originalOut = System.out; - private final ObjectMapper mapper = new ObjectMapper(); - - @BeforeAll - static void beforeAll() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - } - } - - @BeforeEach - void setUp() { - System.setOut(new PrintStream(out)); - } - - @AfterEach - void tearDown() { - System.setOut(originalOut); - } - - @Test - void singleMetricsCaptureUtilityWithDefaultDimension() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic( - software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) - .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"); - - MetricsUtils.defaultDimensions(DimensionSet.of("Service", "Booking")); - - MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, "test", - metricsLogger -> - { - }); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "Booking") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - }); - } - } - - @Test - void singleMetricsCaptureUtility() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic( - software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) - .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"); - - MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, "test", - metricsLogger -> metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1"))); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=test"); - }); - } - } - - @Test - void singleMetricsCaptureUtilityWithNullNamespace() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic( - software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - // POWERTOOLS_METRICS_NAMESPACE is not defined - - MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, - metricsLogger -> metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1"))); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=aws-embedded-metrics"); - }); - } - } - - @Test - void singleMetricsCaptureUtilityWithDefaultNameSpace() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic( - software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_METRICS_NAMESPACE")).thenReturn("GlobalName"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) - .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"); - - MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, - metricsLogger -> metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1"))); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=GlobalName"); - }); - } - } - - @Test - void metricsLoggerCaptureUtilityWithDefaultNameSpace() { - testLogger(MetricsUtils::withMetricsLogger); - } - - @Test - void deprecatedMetricLoggerCaptureUtilityWithDefaultNameSpace() { - testLogger(MetricsUtils::withMetricLogger); - } - - @Test - void shouldThrowExceptionWhenDefaultDimensionIsNull() { - assertThatNullPointerException() - .isThrownBy(() -> MetricsUtils.defaultDimensionSet(null)) - .withMessage("Null dimension set not allowed"); - } - - @Test - void shouldUseTraceIdFromSystemPropertyIfEnvVarNotPresent() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic( - software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_METRICS_NAMESPACE")).thenReturn("GlobalName"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) - .thenReturn(null); - internalWrapper.when(() -> getProperty("com.amazonaws.xray.traceHeader")) - .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"); - - MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, - metricsLogger -> metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1"))); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=GlobalName"); - }); - } - } - - private void testLogger(Consumer> methodToTest) { - try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic( - software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_METRICS_NAMESPACE")).thenReturn("GlobalName"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) - .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"); - - methodToTest.accept(metricsLogger -> - { - metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1")); - metricsLogger.putMetric("Metric1", 1, Unit.COUNT); - }); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=GlobalName"); - }); - } - } - - private Map readAsJson(String s) { - try { - return mapper.readValue(s, Map.class); - } catch (JsonProcessingException e) { - e.printStackTrace(); - } - return emptyMap(); - } -} \ No newline at end of file diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java deleted file mode 100644 index e3a0fa22e..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.Unit; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsColdStartEnabledHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking", captureColdStart = true) - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("Metric1", 1, Unit.BYTES); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultDimensionHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultDimensionHandler.java deleted file mode 100644 index 761c20caa..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultDimensionHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.defaultDimensions; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsEnabledDefaultDimensionHandler implements RequestHandler { - - static { - defaultDimensions(DimensionSet.of("CustomDimension", "booking")); - } - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("Metric1", 1, Unit.BYTES); - - withSingleMetric("Metric2", 1, Unit.COUNT, log -> - { - }); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultNoDimensionHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultNoDimensionHandler.java deleted file mode 100644 index d968f94f5..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultNoDimensionHandler.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.Unit; -import software.amazon.lambda.powertools.metrics.Metrics; -import software.amazon.lambda.powertools.metrics.MetricsUtils; - -public class PowertoolsMetricsEnabledDefaultNoDimensionHandler implements RequestHandler { - - static { - MetricsUtils.defaultDimensions(); - } - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("Metric1", 1, Unit.BYTES); - - withSingleMetric("Metric2", 1, Unit.COUNT, log -> - { - }); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java deleted file mode 100644 index 7cfee533d..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsEnabledHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("Metric1", 1, Unit.BYTES); - - - withSingleMetric("Metric2", 1, Unit.COUNT, - log -> log.setDimensions(DimensionSet.of("Dimension1", "Value1"))); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java deleted file mode 100644 index 1600f4a64..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import java.io.InputStream; -import java.io.OutputStream; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.Unit; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsEnabledStreamHandler implements RequestStreamHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public void handleRequest(InputStream input, OutputStream output, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("Metric1", 1, Unit.BYTES); - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java deleted file mode 100644 index 42e0b3ad4..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsExceptionWhenNoMetricsHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking", raiseOnEmptyMetrics = true) - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetadata("MetaData", "MetaDataValue"); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java deleted file mode 100644 index 04b02e166..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsNoDimensionsHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("CoolMetric", 1); - metricsLogger.setDimensions(new DimensionSet()); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java deleted file mode 100644 index c08ce2f86..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsNoExceptionWhenNoMetricsHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetadata("MetaData", "MetaDataValue"); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java deleted file mode 100644 index bc8a6e949..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import java.util.stream.IntStream; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsTooManyDimensionsHandler implements RequestHandler { - - @Override - @Metrics - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - - metricsLogger.setDimensions(IntStream.range(1, 15) - .mapToObj(value -> DimensionSet.of("Dimension" + value, "DimensionValue" + value)) - .toArray(DimensionSet[]::new)); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsWithExceptionInHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsWithExceptionInHandler.java deleted file mode 100644 index da9028a70..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsWithExceptionInHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsWithExceptionInHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("CoolMetric", 1); - throw new IllegalStateException("Whoops, unexpected exception"); - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLoggerTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLoggerTest.java new file mode 100644 index 000000000..1b7106ece --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLoggerTest.java @@ -0,0 +1,521 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.Instant; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junitpioneer.jupiter.SetEnvironmentVariable; + +import com.amazonaws.services.lambda.runtime.Context; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.cloudwatchlogs.emf.model.Unit; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.metrics.testutils.TestContext; + +class EmfMetricsLoggerTest { + + private Metrics metrics; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + + @BeforeEach + void setUp() throws Exception { + // Reset LambdaHandlerProcessor's SERVICE_NAME + Method resetServiceName = LambdaHandlerProcessor.class.getDeclaredMethod("resetServiceName"); + resetServiceName.setAccessible(true); + resetServiceName.invoke(null); + + // Reset IS_COLD_START + java.lang.reflect.Field coldStartField = LambdaHandlerProcessor.class.getDeclaredField("IS_COLD_START"); + coldStartField.setAccessible(true); + coldStartField.set(null, null); + + metrics = MetricsFactory.getMetricsInstance(); + metrics.setNamespace("TestNamespace"); + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @AfterEach + void tearDown() throws Exception { + System.setOut(standardOut); + + // Reset the singleton state between tests + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); + field.setAccessible(true); + field.set(null, null); + } + + @ParameterizedTest + @MethodSource("unitConversionTestCases") + void shouldConvertMetricUnits(MetricUnit inputUnit, Unit expectedUnit) throws Exception { + // Given + // We access using reflection here for simplicity (even though this is not best practice) + Method convertUnitMethod = EmfMetricsLogger.class.getDeclaredMethod("convertUnit", MetricUnit.class); + convertUnitMethod.setAccessible(true); + + // When + Unit actualUnit = (Unit) convertUnitMethod.invoke(metrics, inputUnit); + + // Then + assertThat(actualUnit).isEqualTo(expectedUnit); + } + + private static Stream unitConversionTestCases() { + return Stream.of( + Arguments.of(MetricUnit.SECONDS, Unit.SECONDS), + Arguments.of(MetricUnit.MICROSECONDS, Unit.MICROSECONDS), + Arguments.of(MetricUnit.MILLISECONDS, Unit.MILLISECONDS), + Arguments.of(MetricUnit.BYTES, Unit.BYTES), + Arguments.of(MetricUnit.KILOBYTES, Unit.KILOBYTES), + Arguments.of(MetricUnit.MEGABYTES, Unit.MEGABYTES), + Arguments.of(MetricUnit.GIGABYTES, Unit.GIGABYTES), + Arguments.of(MetricUnit.TERABYTES, Unit.TERABYTES), + Arguments.of(MetricUnit.BITS, Unit.BITS), + Arguments.of(MetricUnit.KILOBITS, Unit.KILOBITS), + Arguments.of(MetricUnit.MEGABITS, Unit.MEGABITS), + Arguments.of(MetricUnit.GIGABITS, Unit.GIGABITS), + Arguments.of(MetricUnit.TERABITS, Unit.TERABITS), + Arguments.of(MetricUnit.PERCENT, Unit.PERCENT), + Arguments.of(MetricUnit.COUNT, Unit.COUNT), + Arguments.of(MetricUnit.BYTES_SECOND, Unit.BYTES_SECOND), + Arguments.of(MetricUnit.KILOBYTES_SECOND, Unit.KILOBYTES_SECOND), + Arguments.of(MetricUnit.MEGABYTES_SECOND, Unit.MEGABYTES_SECOND), + Arguments.of(MetricUnit.GIGABYTES_SECOND, Unit.GIGABYTES_SECOND), + Arguments.of(MetricUnit.TERABYTES_SECOND, Unit.TERABYTES_SECOND), + Arguments.of(MetricUnit.BITS_SECOND, Unit.BITS_SECOND), + Arguments.of(MetricUnit.KILOBITS_SECOND, Unit.KILOBITS_SECOND), + Arguments.of(MetricUnit.MEGABITS_SECOND, Unit.MEGABITS_SECOND), + Arguments.of(MetricUnit.GIGABITS_SECOND, Unit.GIGABITS_SECOND), + Arguments.of(MetricUnit.TERABITS_SECOND, Unit.TERABITS_SECOND), + Arguments.of(MetricUnit.COUNT_SECOND, Unit.COUNT_SECOND), + Arguments.of(MetricUnit.NONE, Unit.NONE)); + } + + @Test + void shouldCreateMetricWithDefaultResolution() throws Exception { + // When + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("_aws")).isTrue(); + assertThat(rootNode.get("test-metric").asDouble()).isEqualTo(100); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Metrics").get(0).get("Unit").asText()) + .isEqualTo("Count"); + } + + @Test + void shouldCreateMetricWithHighResolution() throws Exception { + // When + metrics.addMetric("test-metric", 100, MetricUnit.COUNT, MetricResolution.HIGH); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("_aws")).isTrue(); + assertThat(rootNode.get("test-metric").asDouble()).isEqualTo(100); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Metrics").get(0).get("Unit").asText()) + .isEqualTo("Count"); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Metrics").get(0).get("StorageResolution") + .asInt()).isEqualTo(1); + } + + @Test + void shouldAddDimension() throws Exception { + // When + metrics.clearDefaultDimensions(); // Clear default Service dimension first for easier assertions + metrics.addDimension("CustomDimension", "CustomValue"); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("CustomDimension")).isTrue(); + assertThat(rootNode.get("CustomDimension").asText()).isEqualTo("CustomValue"); + + // Check that the dimension is in the CloudWatchMetrics section + JsonNode dimensions = rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Dimensions").get(0); + boolean hasDimension = false; + for (JsonNode dimension : dimensions) { + if (dimension.asText().equals("CustomDimension")) { + hasDimension = true; + break; + } + } + assertThat(hasDimension).isTrue(); + } + + @Test + void shouldSetCustomTimestamp() throws Exception { + // Given + Instant customTimestamp = Instant.now(); + + // When + metrics.setTimestamp(customTimestamp); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("_aws")).isTrue(); + assertThat(rootNode.get("_aws").has("Timestamp")).isTrue(); + assertThat(rootNode.get("_aws").get("Timestamp").asLong()).isEqualTo(customTimestamp.toEpochMilli()); + } + + @Test + void shouldAddDimensionSet() throws Exception { + // Given + DimensionSet dimensionSet = DimensionSet.of("Dim1", "Value1", "Dim2", "Value2"); + + // When + metrics.clearDefaultDimensions(); // Clear default Service dimension first for easier assertions + metrics.addDimension(dimensionSet); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Dim1")).isTrue(); + assertThat(rootNode.get("Dim1").asText()).isEqualTo("Value1"); + assertThat(rootNode.has("Dim2")).isTrue(); + assertThat(rootNode.get("Dim2").asText()).isEqualTo("Value2"); + + // Check that the dimensions are in the CloudWatchMetrics section + JsonNode dimensions = rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Dimensions").get(0); + boolean hasDim1 = false; + boolean hasDim2 = false; + for (JsonNode dimension : dimensions) { + String dimName = dimension.asText(); + if (dimName.equals("Dim1")) { + hasDim1 = true; + } else if (dimName.equals("Dim2")) { + hasDim2 = true; + } + } + assertThat(hasDim1).isTrue(); + assertThat(hasDim2).isTrue(); + } + + @Test + void shouldThrowExceptionWhenDimensionSetIsNull() { + // When/Then + assertThatThrownBy(() -> metrics.addDimension((DimensionSet) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("DimensionSet cannot be null"); + } + + @Test + void shouldAddMetadata() throws Exception { + // When + metrics.addMetadata("CustomMetadata", "MetadataValue"); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // The metadata is added to the _aws section in the EMF output + assertThat(rootNode.get("_aws").has("CustomMetadata")).isTrue(); + assertThat(rootNode.get("_aws").get("CustomMetadata").asText()).isEqualTo("MetadataValue"); + } + + @Test + void shouldSetDefaultDimensions() throws Exception { + // Given + DimensionSet dimensionSet = DimensionSet.of("Service", "TestService", "Environment", "Test"); + + // When + metrics.setDefaultDimensions(dimensionSet); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("TestService"); + assertThat(rootNode.has("Environment")).isTrue(); + assertThat(rootNode.get("Environment").asText()).isEqualTo("Test"); + } + + @Test + void shouldGetDefaultDimensions() { + // Given + DimensionSet dimensionSet = DimensionSet.of("Service", "TestService", "Environment", "Test"); + + // When + metrics.setDefaultDimensions(dimensionSet); + DimensionSet dimensions = metrics.getDefaultDimensions(); + + // Then + assertThat(dimensions.getDimensions()).containsEntry("Service", "TestService"); + assertThat(dimensions.getDimensions()).containsEntry("Environment", "Test"); + } + + @Test + void shouldThrowExceptionWhenDefaultDimensionSetIsNull() { + // When/Then + assertThatThrownBy(() -> metrics.setDefaultDimensions((DimensionSet) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("DimensionSet cannot be null"); + } + + @Test + void shouldSetNamespace() throws Exception { + // When + metrics.setNamespace("CustomNamespace"); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("CustomNamespace"); + } + + @Test + void shouldRaiseExceptionOnEmptyMetrics() { + // When + metrics.setRaiseOnEmptyMetrics(true); + + // Then + assertThatThrownBy(() -> metrics.flush()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("No metrics were emitted"); + } + + @Test + void shouldLogWarningOnEmptyMetrics() throws Exception { + // Given + File logFile = new File("target/metrics-test.log"); + + // When + // Flushing without adding metrics + metrics.flush(); + + // Then + // Read the log file and check for the warning + String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8); + assertThat(logContent).contains("No metrics were emitted"); + } + + @Test + void shouldClearDefaultDimensions() throws Exception { + // Given + metrics.setDefaultDimensions(DimensionSet.of("Service", "TestService", "Environment", "Test")); + + // When + metrics.clearDefaultDimensions(); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Service")).isFalse(); + assertThat(rootNode.has("Environment")).isFalse(); + } + + @Test + void shouldCaptureColdStartMetric() throws Exception { + // Given + Context testContext = new TestContext(); + + // When + metrics.captureColdStartMetric(testContext); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("ColdStart")).isTrue(); + assertThat(rootNode.get("ColdStart").asDouble()).isEqualTo(1.0); + assertThat(rootNode.has("function_request_id")).isTrue(); + assertThat(rootNode.get("function_request_id").asText()).isEqualTo(testContext.getAwsRequestId()); + } + + @Test + void shouldCaptureColdStartMetricWithDimensions() throws Exception { + // Given + DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); + + // When + metrics.captureColdStartMetric(dimensions); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("ColdStart")).isTrue(); + assertThat(rootNode.get("ColdStart").asDouble()).isEqualTo(1.0); + assertThat(rootNode.has("CustomDim")).isTrue(); + assertThat(rootNode.get("CustomDim").asText()).isEqualTo("CustomValue"); + } + + @Test + void shouldCaptureColdStartMetricWithoutDimensions() throws Exception { + // When + metrics.captureColdStartMetric(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("ColdStart")).isTrue(); + assertThat(rootNode.get("ColdStart").asDouble()).isEqualTo(1.0); + } + + @Test + void shouldReuseNamespaceForColdStartMetric() throws Exception { + // Given + String customNamespace = "CustomNamespace"; + metrics.setNamespace(customNamespace); + + Context testContext = new TestContext(); + + DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); + + // When + metrics.captureColdStartMetric(testContext, dimensions); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("ColdStart")).isTrue(); + assertThat(rootNode.get("ColdStart").asDouble()).isEqualTo(1.0); + assertThat(rootNode.has("CustomDim")).isTrue(); + assertThat(rootNode.get("CustomDim").asText()).isEqualTo("CustomValue"); + assertThat(rootNode.has("function_request_id")).isTrue(); + assertThat(rootNode.get("function_request_id").asText()).isEqualTo(testContext.getAwsRequestId()); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo(customNamespace); + } + + @Test + void shouldFlushSingleMetric() throws Exception { + // Given + DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); + + // When + metrics.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace", dimensions); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("single-metric")).isTrue(); + assertThat(rootNode.get("single-metric").asDouble()).isEqualTo(200.0); + assertThat(rootNode.has("CustomDim")).isTrue(); + assertThat(rootNode.get("CustomDim").asText()).isEqualTo("CustomValue"); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("SingleNamespace"); + } + + @Test + void shouldFlushSingleMetricWithoutDimensions() throws Exception { + // When + metrics.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace"); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("single-metric")).isTrue(); + assertThat(rootNode.get("single-metric").asDouble()).isEqualTo(200.0); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("SingleNamespace"); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_DISABLED", value = "true") + void shouldNotFlushMetricsWhenDisabled() { + // When + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + assertThat(emfOutput).isEmpty(); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_DISABLED", value = "true") + void shouldNotCaptureColdStartMetricWhenDisabled() { + // Given + Context testContext = new TestContext(); + + // When + metrics.captureColdStartMetric(testContext); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + assertThat(emfOutput).isEmpty(); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_DISABLED", value = "true") + void shouldNotFlushSingleMetricWhenDisabled() { + // Given + DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); + + // When + metrics.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace", dimensions); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + assertThat(emfOutput).isEmpty(); + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java index 0a735c75e..068d19ccb 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java @@ -14,426 +14,315 @@ package software.amazon.lambda.powertools.metrics.internal; -import static java.util.Collections.emptyMap; -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; -import static software.amazon.lambda.powertools.core.internal.SystemWrapper.getenv; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.HashMap; import java.util.Map; + import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import software.amazon.cloudwatchlogs.emf.config.SystemWrapper; -import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor; -import software.amazon.lambda.powertools.metrics.MetricsUtils; -import software.amazon.lambda.powertools.metrics.ValidationException; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsColdStartEnabledHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledDefaultDimensionHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledDefaultNoDimensionHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledStreamHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsExceptionWhenNoMetricsHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsNoDimensionsHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsNoExceptionWhenNoMetricsHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsTooManyDimensionsHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsWithExceptionInHandler; - -public class LambdaMetricsAspectTest { - private final ByteArrayOutputStream out = new ByteArrayOutputStream(); - private final PrintStream originalOut = System.out; - private final ObjectMapper mapper = new ObjectMapper(); - @Mock - private Context context; - private RequestHandler requestHandler; - - - @BeforeAll - static void beforeAll() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - } - } +import org.junitpioneer.jupiter.SetEnvironmentVariable; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.metrics.FlushMetrics; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.metrics.testutils.TestContext; + +class LambdaMetricsAspectTest { + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach - void setUp() throws IllegalAccessException { - openMocks(this); - setupContext(); - writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); - System.setOut(new PrintStream(out)); + void setUp() throws Exception { + System.setOut(new PrintStream(outputStreamCaptor)); + + // Reset LambdaHandlerProcessor's SERVICE_NAME + Method resetServiceName = LambdaHandlerProcessor.class.getDeclaredMethod("resetServiceName"); + resetServiceName.setAccessible(true); + resetServiceName.invoke(null); + + // Reset IS_COLD_START + java.lang.reflect.Field coldStartField = LambdaHandlerProcessor.class.getDeclaredField("IS_COLD_START"); + coldStartField.setAccessible(true); + coldStartField.set(null, null); } @AfterEach - void tearDown() { - System.setOut(originalOut); + void tearDown() throws Exception { + System.setOut(standardOut); + + // Reset the singleton state between tests + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); + field.setAccessible(true); + field.set(null, null); } @Test - public void metricsWithoutColdStart() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic( - software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { - - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) - .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"); - - MetricsUtils.defaultDimensions(null); - requestHandler = new PowertoolsMetricsEnabledHandler(); - requestHandler.handleRequest("input", context); - - assertThat(out.toString().split("\n")) - .hasSize(2) - .satisfies(s -> - { - Map logAsJson = readAsJson(s[0]); - - assertThat(logAsJson) - .containsEntry("Metric2", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793") - .containsEntry("function_request_id", "123ABC"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=ExampleApplication"); - - logAsJson = readAsJson(s[1]); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); - } + void shouldCaptureMetricsFromAnnotatedHandler() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("test-metric")).isTrue(); + assertThat(rootNode.get("test-metric").asDouble()).isEqualTo(100.0); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("CustomNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("CustomService"); } @Test - public void metricsWithDefaultDimensionSpecified() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic( - software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { - - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) - .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"); - - requestHandler = new PowertoolsMetricsEnabledDefaultDimensionHandler(); - - requestHandler.handleRequest("input", context); - - assertThat(out.toString().split("\n")) - .hasSize(2) - .satisfies(s -> - { - Map logAsJson = readAsJson(s[0]); - - assertThat(logAsJson) - .containsEntry("Metric2", 1.0) - .containsEntry("CustomDimension", "booking") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793") - .containsEntry("function_request_id", "123ABC"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=ExampleApplication"); - - logAsJson = readAsJson(s[1]); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("CustomDimension", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); - } + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "EnvNamespace") + @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = "EnvService") + void shouldOverrideEnvironmentVariablesWithAnnotation() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("CustomNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("CustomService"); } @Test - public void metricsWithDefaultNoDimensionSpecified() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class); - MockedStatic internalWrapper = mockStatic( - software.amazon.lambda.powertools.core.internal.SystemWrapper.class)) { - - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - internalWrapper.when(() -> getenv("_X_AMZN_TRACE_ID")) - .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"); - - requestHandler = new PowertoolsMetricsEnabledDefaultNoDimensionHandler(); - - requestHandler.handleRequest("input", context); + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "EnvNamespace") + @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = "EnvService") + void shouldUseEnvironmentVariablesWhenNoAnnotationOverrides() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithDefaultMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("EnvNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("EnvService"); + } - assertThat(out.toString().split("\n")) - .hasSize(2) - .satisfies(s -> - { - Map logAsJson = readAsJson(s[0]); + @Test + void shouldCaptureColdStartMetricWhenConfigured() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithColdStartMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); - assertThat(logAsJson) - .containsEntry("Metric2", 1.0) - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793") - .containsEntry("function_request_id", "123ABC"); + // When + handler.handleRequest(input, context); - Map aws = (Map) logAsJson.get("_aws"); + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + String[] emfOutputs = emfOutput.split("\\n"); - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=ExampleApplication"); + // There should be two EMF outputs - one for cold start and one for the handler metrics + assertThat(emfOutputs).hasSize(2); - logAsJson = readAsJson(s[1]); + JsonNode coldStartNode = objectMapper.readTree(emfOutputs[0]); + assertThat(coldStartNode.has("ColdStart")).isTrue(); + assertThat(coldStartNode.get("ColdStart").asDouble()).isEqualTo(1.0); - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); - } + JsonNode metricsNode = objectMapper.readTree(emfOutputs[1]); + assertThat(metricsNode.has("test-metric")).isTrue(); } @Test - public void metricsWithColdStart() { - - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_FUNCTION_NAME", value = "EnvFunctionName") + void shouldNotIncludeServiceDimensionInColdStartMetricWhenServiceUndefined() throws Exception { + // Given - no service name set, so it will use the default undefined value + RequestHandler, String> handler = new HandlerWithColdStartMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); - MetricsUtils.defaultDimensions(null); - requestHandler = new PowertoolsMetricsColdStartEnabledHandler(); + // When + handler.handleRequest(input, context); - requestHandler.handleRequest("input", context); + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + String[] emfOutputs = emfOutput.split("\\n"); - assertThat(out.toString().split("\n")) - .hasSize(2) - .satisfies(s -> - { - Map logAsJson = readAsJson(s[0]); + // There should be two EMF outputs - one for cold start and one for the handler metrics + assertThat(emfOutputs).hasSize(2); - assertThat(logAsJson) - .doesNotContainKey("Metric1") - .containsEntry("ColdStart", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); + JsonNode coldStartNode = objectMapper.readTree(emfOutputs[0]); + assertThat(coldStartNode.has("ColdStart")).isTrue(); + assertThat(coldStartNode.get("ColdStart").asDouble()).isEqualTo(1.0); - logAsJson = readAsJson(s[1]); + // Service dimension should not be present in cold start metrics + assertThat(coldStartNode.has("Service")).isFalse(); - assertThat(logAsJson) - .doesNotContainKey("ColdStart") - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); - } + // FunctionName dimension should be present + assertThat(coldStartNode.has("FunctionName")).isTrue(); + assertThat(coldStartNode.get("FunctionName").asText()).isEqualTo("EnvFunctionName"); } @Test - public void noColdStartMetricsWhenColdStartDone() { - - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - - MetricsUtils.defaultDimensions(null); - requestHandler = new PowertoolsMetricsColdStartEnabledHandler(); - - requestHandler.handleRequest("input", context); - requestHandler.handleRequest("input", context); - - assertThat(out.toString().split("\n")) - .hasSize(3) - .satisfies(s -> - { - Map logAsJson = readAsJson(s[0]); - - assertThat(logAsJson) - .doesNotContainKey("Metric1") - .containsEntry("ColdStart", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - - logAsJson = readAsJson(s[1]); - - assertThat(logAsJson) - .doesNotContainKey("ColdStart") - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - - logAsJson = readAsJson(s[2]); - - assertThat(logAsJson) - .doesNotContainKey("ColdStart") - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + void shouldUseCustomFunctionNameWhenProvidedForColdStartMetric() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithCustomFunctionName(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + String[] emfOutputs = emfOutput.split("\\n"); + + // There should be two EMF outputs - one for cold start and one for the handler metrics + assertThat(emfOutputs).hasSize(2); + + JsonNode coldStartNode = objectMapper.readTree(emfOutputs[0]); + assertThat(coldStartNode.has("FunctionName")).isTrue(); + assertThat(coldStartNode.get("FunctionName").asText()).isEqualTo("CustomFunction"); + + // Check that FunctionName is in the dimensions + JsonNode dimensions = coldStartNode.get("_aws").get("CloudWatchMetrics").get(0).get("Dimensions").get(0); + boolean hasFunctionName = false; + for (JsonNode dimension : dimensions) { + if (dimension.asText().equals("FunctionName")) { + hasFunctionName = true; + break; + } } + assertThat(hasFunctionName).isTrue(); } @Test - public void metricsWithStreamHandler() throws IOException { - - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); + void shouldUseServiceNameWhenProvidedForColdStartMetric() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithServiceNameAndColdStart(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // Should use the service name from annotation + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("CustomService"); + } - MetricsUtils.defaultDimensions(null); - RequestStreamHandler streamHandler = new PowertoolsMetricsEnabledStreamHandler(); + @Test + void shouldHaveNoEffectOnNonHandlerMethod() { + // Given + RequestHandler, String> handler = new HandlerWithAnnotationOnWrongMethod(); + Context context = new TestContext(); + Map input = new HashMap<>(); - streamHandler.handleRequest(new ByteArrayInputStream(new byte[] {}), new ByteArrayOutputStream(), context); + // When + handler.handleRequest(input, context); - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); + // Then + String emfOutput = outputStreamCaptor.toString().trim(); - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); - } + // Should be empty because we do not flush any metrics manually + assertThat(emfOutput).isEmpty(); } - @Test - public void exceptionWhenNoMetricsEmitted() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - - MetricsUtils.defaultDimensions(null); - requestHandler = new PowertoolsMetricsExceptionWhenNoMetricsHandler(); - - assertThatExceptionOfType(ValidationException.class) - .isThrownBy(() -> requestHandler.handleRequest("input", context)) - .withMessage("No metrics captured, at least one metrics must be emitted"); + static class HandlerWithMetricsAnnotation implements RequestHandler, String> { + @Override + @FlushMetrics(namespace = "CustomNamespace", service = "CustomService") + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; } } - @Test - public void noExceptionWhenNoMetricsEmitted() { - - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - - MetricsUtils.defaultDimensions(null); - requestHandler = new PowertoolsMetricsNoExceptionWhenNoMetricsHandler(); - - requestHandler.handleRequest("input", context); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Service", "booking") - .doesNotContainKey("_aws"); - }); + static class HandlerWithDefaultMetricsAnnotation implements RequestHandler, String> { + @Override + @FlushMetrics + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; } } - @Test - public void allowWhenNoDimensionsSet() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - MetricsUtils.defaultDimensions(null); - - requestHandler = new PowertoolsMetricsNoDimensionsHandler(); - requestHandler.handleRequest("input", context); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - assertThat(logAsJson) - .containsEntry("CoolMetric", 1.0) - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + static class HandlerWithColdStartMetricsAnnotation implements RequestHandler, String> { + @Override + @FlushMetrics(captureColdStart = true, namespace = "TestNamespace") + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; } } - @Test - public void exceptionWhenTooManyDimensionsSet() { - - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - - MetricsUtils.defaultDimensions(null); - - requestHandler = new PowertoolsMetricsTooManyDimensionsHandler(); - - assertThatExceptionOfType(ValidationException.class) - .isThrownBy(() -> requestHandler.handleRequest("input", context)) - .withMessage("Number of Dimensions must be in range of 0-9. Actual size: 14."); + static class HandlerWithCustomFunctionName implements RequestHandler, String> { + @Override + @FlushMetrics(captureColdStart = true, functionName = "CustomFunction", namespace = "TestNamespace") + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; } } - @Test - public void metricsPublishedEvenHandlerThrowsException() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda"); - - MetricsUtils.defaultDimensions(null); - requestHandler = new PowertoolsMetricsWithExceptionInHandler(); - - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> requestHandler.handleRequest("input", context)) - .withMessage("Whoops, unexpected exception"); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - assertThat(logAsJson) - .containsEntry("CoolMetric", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + static class HandlerWithServiceNameAndColdStart implements RequestHandler, String> { + @Override + @FlushMetrics(service = "CustomService", captureColdStart = true, namespace = "TestNamespace") + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; } } - private void setupContext() { - when(context.getFunctionName()).thenReturn("testFunction"); - when(context.getInvokedFunctionArn()).thenReturn("testArn"); - when(context.getFunctionVersion()).thenReturn("1"); - when(context.getMemoryLimitInMB()).thenReturn(10); - when(context.getAwsRequestId()).thenReturn("123ABC"); - } + static class HandlerWithAnnotationOnWrongMethod implements RequestHandler, String> { + @Override + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + someOtherMethod(); + return "OK"; + } - private Map readAsJson(String s) { - try { - return mapper.readValue(s, Map.class); - } catch (JsonProcessingException e) { - e.printStackTrace(); + @FlushMetrics + public void someOtherMethod() { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); } - return emptyMap(); } } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/ValidatorTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/ValidatorTest.java new file mode 100644 index 000000000..e5d780f9f --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/ValidatorTest.java @@ -0,0 +1,224 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics.internal; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.Instant; + +import org.junit.jupiter.api.Test; + +class ValidatorTest { + + @Test + void shouldThrowExceptionWhenNamespaceIsNull() { + // When/Then + assertThatThrownBy(() -> Validator.validateNamespace(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Namespace must be specified before flushing metrics"); + } + + @Test + void shouldThrowExceptionWhenNamespaceIsEmpty() { + // When/Then + assertThatThrownBy(() -> Validator.validateNamespace("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Namespace must be specified before flushing metrics"); + } + + @Test + void shouldThrowExceptionWhenNamespaceIsBlank() { + // When/Then + assertThatThrownBy(() -> Validator.validateNamespace(" ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Namespace must be specified before flushing metrics"); + } + + @Test + void shouldThrowExceptionWhenNamespaceExceedsMaxLength() { + // Given + String tooLongNamespace = "a".repeat(256); + + // When/Then + assertThatThrownBy(() -> Validator.validateNamespace(tooLongNamespace)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Namespace exceeds maximum length of 255"); + } + + @Test + void shouldThrowExceptionWhenNamespaceContainsInvalidCharacters() { + // When/Then + assertThatThrownBy(() -> Validator.validateNamespace("Invalid Namespace")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Namespace contains invalid characters"); + } + + @Test + void shouldAcceptValidNamespace() { + // When/Then + assertThatCode(() -> Validator.validateNamespace("Valid.Namespace_123#/")) + .doesNotThrowAnyException(); + } + + @Test + void shouldThrowExceptionWhenDimensionKeyIsNull() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension(null, "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension key cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenDimensionKeyIsEmpty() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("", "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension key cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenDimensionValueIsNull() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("Key", null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenDimensionValueIsEmpty() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("Key", "")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenDimensionKeyContainsWhitespace() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("Key With Space", "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension key cannot contain whitespaces: Key With Space"); + } + + @Test + void shouldThrowExceptionWhenDimensionValueContainsWhitespace() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("Key", "Value With Space")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value cannot contain whitespaces: Value With Space"); + } + + @Test + void shouldThrowExceptionWhenDimensionKeyStartsWithColon() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension(":Key", "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension key cannot start with colon: :Key"); + } + + @Test + void shouldThrowExceptionWhenDimensionKeyExceedsMaxLength() { + // Given + String longKey = "a".repeat(251); // MAX_DIMENSION_NAME_LENGTH + 1 + + // When/Then + assertThatThrownBy(() -> Validator.validateDimension(longKey, "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension name exceeds maximum length of 250: " + longKey); + } + + @Test + void shouldThrowExceptionWhenDimensionValueExceedsMaxLength() { + // Given + String longValue = "a".repeat(1025); // MAX_DIMENSION_VALUE_LENGTH + 1 + + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("Key", longValue)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value exceeds maximum length of 1024: " + longValue); + } + + @Test + void shouldThrowExceptionWhenDimensionKeyContainsNonAsciiCharacters() { + // Given + String keyWithNonAscii = "Key\u0080"; // Non-ASCII character + + // When/Then + assertThatThrownBy(() -> Validator.validateDimension(keyWithNonAscii, "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension name has invalid characters: " + keyWithNonAscii); + } + + @Test + void shouldThrowExceptionWhenDimensionValueContainsNonAsciiCharacters() { + // Given + String valueWithNonAscii = "Value\u0080"; // Non-ASCII character + + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("Key", valueWithNonAscii)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value has invalid characters: " + valueWithNonAscii); + } + + @Test + void shouldAcceptValidDimension() { + // When/Then + assertThatCode(() -> Validator.validateDimension("ValidKey", "ValidValue")) + .doesNotThrowAnyException(); + } + + @Test + void shouldThrowExceptionWhenTimestampIsNull() { + // When/Then + assertThatThrownBy(() -> Validator.validateTimestamp(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Timestamp cannot be null"); + } + + @Test + void shouldThrowExceptionWhenTimestampIsTooFarInFuture() { + // Given + Instant futureTooFar = Instant.now().plusSeconds(Validator.MAX_TIMESTAMP_FUTURE_AGE_SECONDS + 1); + + // When/Then + assertThatThrownBy(() -> Validator.validateTimestamp(futureTooFar)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Timestamp cannot be more than " + Validator.MAX_TIMESTAMP_FUTURE_AGE_SECONDS + + " seconds in the future"); + } + + @Test + void shouldThrowExceptionWhenTimestampIsTooFarInPast() { + // Given + Instant pastTooFar = Instant.now().minusSeconds(Validator.MAX_TIMESTAMP_PAST_AGE_SECONDS + 1); + + // When/Then + assertThatThrownBy(() -> Validator.validateTimestamp(pastTooFar)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Timestamp cannot be more than " + Validator.MAX_TIMESTAMP_PAST_AGE_SECONDS + + " seconds in the past"); + } + + @Test + void shouldAcceptValidTimestamp() { + // Given + Instant validTimestamp = Instant.now(); + + // When/Then + assertThatCode(() -> Validator.validateTimestamp(validTimestamp)) + .doesNotThrowAnyException(); + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/model/DimensionSetTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/model/DimensionSetTest.java new file mode 100644 index 000000000..ac059f117 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/model/DimensionSetTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class DimensionSetTest { + + @Test + void shouldCreateEmptyDimensionSet() { + // When + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + + // Then + assertThat(dimensionSet.getDimensions()).isEmpty(); + assertThat(dimensionSet.getDimensionKeys()).isEmpty(); + } + + @Test + void shouldCreateDimensionSetWithSingleKeyValue() { + // When + DimensionSet dimensionSet = DimensionSet.of("Key", "Value"); + + // Then + assertThat(dimensionSet.getDimensions()).containsExactly(Map.entry("Key", "Value")); + assertThat(dimensionSet.getDimensionKeys()).containsExactly("Key"); + } + + @Test + void shouldCreateDimensionSetWithTwoKeyValues() { + // When + DimensionSet dimensionSet = DimensionSet.of("Key1", "Value1", "Key2", "Value2"); + + // Then + assertThat(dimensionSet.getDimensions()) + .containsEntry("Key1", "Value1") + .containsEntry("Key2", "Value2"); + assertThat(dimensionSet.getDimensionKeys()).containsExactly("Key1", "Key2"); + } + + @Test + void shouldCreateDimensionSetWithThreeKeyValues() { + // When + DimensionSet dimensionSet = DimensionSet.of( + "Key1", "Value1", + "Key2", "Value2", + "Key3", "Value3"); + + // Then + assertThat(dimensionSet.getDimensions()) + .containsEntry("Key1", "Value1") + .containsEntry("Key2", "Value2") + .containsEntry("Key3", "Value3"); + assertThat(dimensionSet.getDimensionKeys()).containsExactly("Key1", "Key2", "Key3"); + } + + @Test + void shouldCreateDimensionSetWithFourKeyValues() { + // When + DimensionSet dimensionSet = DimensionSet.of( + "Key1", "Value1", + "Key2", "Value2", + "Key3", "Value3", + "Key4", "Value4"); + + // Then + assertThat(dimensionSet.getDimensions()) + .containsEntry("Key1", "Value1") + .containsEntry("Key2", "Value2") + .containsEntry("Key3", "Value3") + .containsEntry("Key4", "Value4"); + assertThat(dimensionSet.getDimensionKeys()).containsExactly("Key1", "Key2", "Key3", "Key4"); + } + + @Test + void shouldCreateDimensionSetWithFiveKeyValues() { + // When + DimensionSet dimensionSet = DimensionSet.of( + "Key1", "Value1", + "Key2", "Value2", + "Key3", "Value3", + "Key4", "Value4", + "Key5", "Value5"); + + // Then + assertThat(dimensionSet.getDimensions()) + .containsEntry("Key1", "Value1") + .containsEntry("Key2", "Value2") + .containsEntry("Key3", "Value3") + .containsEntry("Key4", "Value4") + .containsEntry("Key5", "Value5"); + assertThat(dimensionSet.getDimensionKeys()).containsExactly("Key1", "Key2", "Key3", "Key4", "Key5"); + } + + @Test + void shouldCreateDimensionSetFromMap() { + // Given + Map dimensions = Map.of( + "Key1", "Value1", + "Key2", "Value2"); + + // When + DimensionSet dimensionSet = DimensionSet.of(dimensions); + + // Then + assertThat(dimensionSet.getDimensions()).isEqualTo(dimensions); + assertThat(dimensionSet.getDimensionKeys()).containsExactlyInAnyOrder("Key1", "Key2"); + } + + @Test + void shouldGetDimensionValue() { + // Given + DimensionSet dimensionSet = DimensionSet.of("Key1", "Value1", "Key2", "Value2"); + + // When + String value = dimensionSet.getDimensionValue("Key1"); + + // Then + assertThat(value).isEqualTo("Value1"); + } + + @Test + void shouldReturnNullForNonExistentDimension() { + // Given + DimensionSet dimensionSet = DimensionSet.of("Key1", "Value1"); + + // When + String value = dimensionSet.getDimensionValue("NonExistentKey"); + + // Then + assertThat(value).isNull(); + } + + @Test + void shouldThrowExceptionWhenExceedingMaxDimensions() { + // Given + // Create a dimension set with 30 dimensions (30 is maximum) + DimensionSet dimensionSet = new DimensionSet(); + for (int i = 1; i <= 30; i++) { + dimensionSet.addDimension("Key" + i, "Value" + i); + } + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension("Key31", "Value31")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Cannot exceed 30 dimensions per dimension set"); + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProviderTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProviderTest.java new file mode 100644 index 000000000..2b2268ea8 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProviderTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics.provider; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.internal.EmfMetricsLogger; + +class EmfMetricsProviderTest { + + @Test + void shouldCreateEmfMetricsLogger() { + // Given + EmfMetricsProvider provider = new EmfMetricsProvider(); + + // When + Metrics metrics = provider.getMetricsInstance(); + + // Then + assertThat(metrics) + .isNotNull() + .isInstanceOf(EmfMetricsLogger.class); + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestContext.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestContext.java new file mode 100644 index 000000000..c4f5e4455 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestContext.java @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.metrics.testutils; + +import com.amazonaws.services.lambda.runtime.Context; + +/** + * Simple Lambda context implementation for unit tests + */ +public class TestContext implements Context { + @Override + public String getAwsRequestId() { + return "test-request-id"; + } + + @Override + public String getLogGroupName() { + return "test-log-group"; + } + + @Override + public String getLogStreamName() { + return "test-log-stream"; + } + + @Override + public String getFunctionName() { + return "test-function"; + } + + @Override + public String getFunctionVersion() { + return "test-version"; + } + + @Override + public String getInvokedFunctionArn() { + return "test-arn"; + } + + @Override + public com.amazonaws.services.lambda.runtime.CognitoIdentity getIdentity() { + return null; + } + + @Override + public com.amazonaws.services.lambda.runtime.ClientContext getClientContext() { + return null; + } + + @Override + public int getRemainingTimeInMillis() { + return 1000; + } + + @Override + public int getMemoryLimitInMB() { + return 128; + } + + @Override + public com.amazonaws.services.lambda.runtime.LambdaLogger getLogger() { + return null; + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetrics.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetrics.java new file mode 100644 index 000000000..949828a13 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetrics.java @@ -0,0 +1,85 @@ +package software.amazon.lambda.powertools.metrics.testutils; + +import java.time.Instant; +import java.util.Collections; + +import com.amazonaws.services.lambda.runtime.Context; + +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; + +public class TestMetrics implements Metrics { + @Override + public void addMetric(String name, double value, MetricUnit unit) { + // Test placeholder + } + + @Override + public void flush() { + // Test placeholder + } + + @Override + public void addMetric(String key, double value, MetricUnit unit, MetricResolution resolution) { + // Test placeholder + } + + @Override + public void addDimension(DimensionSet dimensionSet) { + // Test placeholder + } + + @Override + public void addMetadata(String key, Object value) { + // Test placeholder + } + + @Override + public void setDefaultDimensions(DimensionSet dimensionSet) { + // Test placeholder + } + + @Override + public DimensionSet getDefaultDimensions() { + // Test placeholder + return DimensionSet.of(Collections.emptyMap()); + } + + @Override + public void setNamespace(String namespace) { + // Test placeholder + } + + @Override + public void setRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics) { + // Test placeholder + } + + @Override + public void clearDefaultDimensions() { + // Test placeholder + } + + @Override + public void setTimestamp(Instant timestamp) { + // Test placeholder + } + + @Override + public void captureColdStartMetric(Context context, DimensionSet dimensions) { + // Test placeholder + } + + @Override + public void captureColdStartMetric(DimensionSet dimensions) { + // Test placeholder + } + + @Override + public void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, + DimensionSet dimensions) { + // Test placeholder + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsProvider.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsProvider.java new file mode 100644 index 000000000..4a5fc23dd --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsProvider.java @@ -0,0 +1,11 @@ +package software.amazon.lambda.powertools.metrics.testutils; + +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; + +public class TestMetricsProvider implements MetricsProvider { + @Override + public Metrics getMetricsInstance() { + return new TestMetrics(); + } +} diff --git a/powertools-metrics/src/test/resources/simplelogger.properties b/powertools-metrics/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..6c626ab4d --- /dev/null +++ b/powertools-metrics/src/test/resources/simplelogger.properties @@ -0,0 +1,7 @@ +org.slf4j.simpleLogger.logFile=target/metrics-test.log +org.slf4j.simpleLogger.defaultLogLevel=warn +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS +org.slf4j.simpleLogger.showThreadName=false +org.slf4j.simpleLogger.showLogName=true +org.slf4j.simpleLogger.showShortLogName=false diff --git a/powertools-parameters/pom.xml b/powertools-parameters/pom.xml index 49f0cd80c..ad8f14e36 100644 --- a/powertools-parameters/pom.xml +++ b/powertools-parameters/pom.xml @@ -21,94 +21,33 @@ powertools-parent software.amazon.lambda - 1.19.0-SNAPSHOT + 2.0.0-RC1 powertools-parameters - Powertools for AWS Lambda (Java) library Parameters - - - Set of utilities to retrieve parameters from Secrets Manager or SSM Parameter Store - - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - + Powertools for AWS Lambda (Java) - Parameters + Set of utilities to retrieve parameters - common interface - software.amazon.lambda - powertools-core - - - software.amazon.awssdk - ssm - - - software.amazon.awssdk - apache-client - - - software.amazon.awssdk - netty-nio-client - - + org.aspectj + aspectjrt + provided - software.amazon.awssdk - secretsmanager - - - software.amazon.awssdk - apache-client - - - software.amazon.awssdk - netty-nio-client - - + software.amazon.lambda + powertools-common software.amazon.awssdk url-connection-client - - software.amazon.awssdk - dynamodb - - - software.amazon.awssdk - appconfigdata - com.fasterxml.jackson.core jackson-databind - - org.aspectj - aspectjrt - compile - org.junit.jupiter @@ -148,10 +87,100 @@ - - org.apache.maven.plugins - maven-checkstyle-plugin - + + + + generate-graalvm-files + + + org.mockito + mockito-subclass + 5.6.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters,experimental-class-define-support + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + 5.6.0 + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.2 + true + + + test-native + + test + + test + + + + powertools-parameters + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + --native-image-info + -H:+UnlockExperimentalVMOptions + -H:Log=registerResource:5 + + + + + + + diff --git a/powertools-parameters/powertools-parameters-appconfig/pom.xml b/powertools-parameters/powertools-parameters-appconfig/pom.xml new file mode 100644 index 000000000..840bb5159 --- /dev/null +++ b/powertools-parameters/powertools-parameters-appconfig/pom.xml @@ -0,0 +1,189 @@ + + + 4.0.0 + + + software.amazon.lambda + powertools-parent + 2.0.0-RC1 + ../../pom.xml + + + powertools-parameters-appconfig + Powertools for AWS Lambda (Java) library Parameters - AppConfig + AppConfig implementation for the Parameters module + + + + software.amazon.lambda + powertools-parameters + ${project.version} + + + software.amazon.awssdk + appconfigdata + + + software.amazon.awssdk + apache-client + + + software.amazon.awssdk + netty-nio-client + + + + + org.aspectj + aspectjrt + provided + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + + + org.apache.commons + commons-lang3 + test + + + org.assertj + assertj-core + test + + + org.aspectj + aspectjweaver + test + + + + + + generate-graalvm-files + + + org.mockito + mockito-subclass + 5.6.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-appconfig,experimental-class-define-support + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + 5.6.0 + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.2 + true + + + test-native + + test + + test + + + + powertools-parameters-appconfig + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + --native-image-info + -H:+UnlockExperimentalVMOptions + -H:Log=registerResource:5 + + + + + + + + + + + + maven-surefire-plugin + 3.1.2 + + + eu-central-1 + + + + + + diff --git a/powertools-parameters/powertools-parameters-appconfig/src/main/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigParam.java b/powertools-parameters/powertools-parameters-appconfig/src/main/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigParam.java new file mode 100644 index 000000000..eb959be76 --- /dev/null +++ b/powertools-parameters/powertools-parameters-appconfig/src/main/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigParam.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.appconfig; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import software.amazon.lambda.powertools.parameters.transform.Transformer; + +/** + * Use this annotation to inject AWS AppConfig parameters into fields in your application. You + * can also use {@code AppConfigProviderBuilder} to obtain AppConfig values directly, rather than + * injecting them implicitly. + * Both {@code environment} and {@code application} fields are necessary. + * + * @see AppConfigProviderBuilder + * @see Powertools for AWS Lambda (Java) parameters documentation + * + *
+ * @AppConfigParam(key = "my-param", environment = "my-env", application = "my-app")
+ * String appConfigParam;
+ * 
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface AppConfigParam { + String key(); + + /** + * Mandatory. Provide an environment to the {@link AppConfigProvider} + */ + String environment(); + + /** + * Mandatory. Provide an application to the {@link AppConfigProvider} + */ + String application(); + + /** + * Optional Provide a Transformer to transform the returned parameter values. + */ + Class transformer() default Transformer.class; +} diff --git a/powertools-parameters/powertools-parameters-appconfig/src/main/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigParametersAspect.java b/powertools-parameters/powertools-parameters-appconfig/src/main/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigParametersAspect.java new file mode 100644 index 000000000..7ab4cf23e --- /dev/null +++ b/powertools-parameters/powertools-parameters-appconfig/src/main/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigParametersAspect.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.appconfig; + +import java.util.function.BiFunction; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.FieldSignature; +import software.amazon.lambda.powertools.parameters.BaseParamAspect; + +/** + * Provides the AppConfig parameter aspect. This aspect is responsible for injecting + * parameters from AWS AppConfig into fields annotated with @AppConfigParam. See the + * README and Powertools for Lambda (Java) documentation for information on using this feature. + */ +@Aspect +public class AppConfigParametersAspect extends BaseParamAspect { + + private static BiFunction providerBuilder = + (String env, String app) -> AppConfigProvider.builder() + .withEnvironment(env) + .withApplication(app) + .build(); + + + @Pointcut("get(* *) && @annotation(appConfigParamAnnotation)") + public void getParam(AppConfigParam appConfigParamAnnotation) { + } + + @Around("getParam(appConfigParamAnnotation)") + public Object injectParam(final ProceedingJoinPoint joinPoint, final AppConfigParam appConfigParamAnnotation) { + + AppConfigProvider provider = providerBuilder.apply + (appConfigParamAnnotation.environment(), appConfigParamAnnotation.application()); + + return getAndTransform(appConfigParamAnnotation.key(), appConfigParamAnnotation.transformer(), provider, + (FieldSignature) joinPoint.getSignature()); + } + +} diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java b/powertools-parameters/powertools-parameters-appconfig/src/main/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigProvider.java similarity index 52% rename from powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java rename to powertools-parameters/powertools-parameters-appconfig/src/main/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigProvider.java index f2e4faebb..5fd272c9b 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/AppConfigProvider.java +++ b/powertools-parameters/powertools-parameters-appconfig/src/main/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigProvider.java @@ -12,20 +12,16 @@ * */ -package software.amazon.lambda.powertools.parameters; +package software.amazon.lambda.powertools.parameters.appconfig; import java.util.HashMap; import java.util.Map; -import software.amazon.awssdk.core.SdkSystemSetting; -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; -import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; -import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; -import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationRequest; import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse; import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest; -import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator; +import software.amazon.lambda.powertools.parameters.BaseProvider; +import software.amazon.lambda.powertools.parameters.ParamProvider; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; @@ -50,8 +46,9 @@ public class AppConfigProvider extends BaseProvider { private final String environment; private final HashMap establishedSessions = new HashMap<>(); - AppConfigProvider(CacheManager cacheManager, AppConfigDataClient client, String environment, String application) { - super(cacheManager); + AppConfigProvider(CacheManager cacheManager, TransformationManager transformationManager, + AppConfigDataClient client, String environment, String application) { + super(cacheManager, transformationManager); this.client = client; this.application = application; this.environment = environment; @@ -60,12 +57,13 @@ public class AppConfigProvider extends BaseProvider { /** * Create a builder that can be used to configure and create a {@link AppConfigProvider}. * - * @return a new instance of {@link AppConfigProvider.Builder} + * @return a new instance of {@link AppConfigProviderBuilder} */ - public static AppConfigProvider.Builder builder() { - return new AppConfigProvider.Builder(); + public static AppConfigProviderBuilder builder() { + return new AppConfigProviderBuilder(); } + /** * Retrieve the parameter value from the AppConfig parameter store.
* @@ -128,102 +126,4 @@ private EstablishedSession(String nextSessionToken, String value) { } } - static class Builder { - private AppConfigDataClient client; - private CacheManager cacheManager; - private TransformationManager transformationManager; - private String environment; - private String application; - - /** - * Create a {@link AppConfigProvider} instance. - * - * @return a {@link AppConfigProvider} - */ - public AppConfigProvider build() { - if (cacheManager == null) { - throw new IllegalStateException("No CacheManager provided; please provide one"); - } - if (environment == null) { - throw new IllegalStateException("No environment provided; please provide one"); - } - if (application == null) { - throw new IllegalStateException("No application provided; please provide one"); - } - - // Create a AppConfigDataClient if we haven't been given one - if (client == null) { - client = AppConfigDataClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) - .overrideConfiguration(ClientOverrideConfiguration.builder() - .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, - UserAgentConfigurator.getUserAgent(PARAMETERS)).build()) - .build(); - } - - AppConfigProvider provider = new AppConfigProvider(cacheManager, client, environment, application); - - if (transformationManager != null) { - provider.setTransformationManager(transformationManager); - } - return provider; - } - - /** - * Set custom {@link AppConfigProvider} to pass to the {@link AppConfigDataClient}.
- * Use it if you want to customize the region or any other part of the client. - * - * @param client Custom client - * @return the builder to chain calls (eg.
builder.withClient().build()
) - */ - public AppConfigProvider.Builder withClient(AppConfigDataClient client) { - this.client = client; - return this; - } - - /** - * Mandatory. Provide an environment to the {@link AppConfigProvider} - * - * @param environment the AppConfig environment - * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) - */ - public AppConfigProvider.Builder withEnvironment(String environment) { - this.environment = environment; - return this; - } - - /** - * Mandatory. Provide an application to the {@link AppConfigProvider} - * - * @param application the application to pull configuration from - * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) - */ - public AppConfigProvider.Builder withApplication(String application) { - this.application = application; - return this; - } - - /** - * Mandatory. Provide a CacheManager to the {@link AppConfigProvider} - * - * @param cacheManager the manager that will handle the cache of parameters - * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) - */ - public AppConfigProvider.Builder withCacheManager(CacheManager cacheManager) { - this.cacheManager = cacheManager; - return this; - } - - /** - * Provide a transformationManager to the {@link AppConfigProvider} - * - * @param transformationManager the manager that will handle transformation of parameters - * @return the builder to chain calls (eg.
builder.withTransformationManager().build()
) - */ - public AppConfigProvider.Builder withTransformationManager(TransformationManager transformationManager) { - this.transformationManager = transformationManager; - return this; - } - } } diff --git a/powertools-parameters/powertools-parameters-appconfig/src/main/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigProviderBuilder.java b/powertools-parameters/powertools-parameters-appconfig/src/main/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigProviderBuilder.java new file mode 100644 index 000000000..b6f6cd809 --- /dev/null +++ b/powertools-parameters/powertools-parameters-appconfig/src/main/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigProviderBuilder.java @@ -0,0 +1,126 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.appconfig; + +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; +import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; +import software.amazon.lambda.powertools.common.internal.UserAgentConfigurator; +import software.amazon.lambda.powertools.parameters.BaseProvider; +import software.amazon.lambda.powertools.parameters.ParamProvider; +import software.amazon.lambda.powertools.parameters.cache.CacheManager; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; + +/** + * Implements a {@link ParamProvider} on top of the AppConfig service. AppConfig provides + */ +public class AppConfigProviderBuilder { + private AppConfigDataClient client; + private CacheManager cacheManager; + private TransformationManager transformationManager; + private String environment; + private String application; + + /** + * Create a {@link AppConfigProvider} instance. + * + * @return a {@link AppConfigProvider} + */ + public AppConfigProvider build() { + if (cacheManager == null) { + cacheManager = new CacheManager(); + } + if (environment == null) { + throw new IllegalStateException("No environment provided; please provide one"); + } + if (application == null) { + throw new IllegalStateException("No application provided; please provide one"); + } + if (transformationManager == null) { + transformationManager = new TransformationManager(); + } + // Create a AppConfigDataClient if we haven't been given one + if (client == null) { + client = AppConfigDataClient.builder() + .httpClientBuilder(UrlConnectionHttpClient.builder()) + .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) + .overrideConfiguration(ClientOverrideConfiguration.builder() + .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, + UserAgentConfigurator.getUserAgent(BaseProvider.PARAMETERS)).build()) + .build(); + } + + return new AppConfigProvider(cacheManager, transformationManager, client, environment, application); + } + + /** + * Set custom {@link AppConfigDataClient} to pass to the {@link AppConfigProvider}.
+ * Use it if you want to customize the region or any other part of the client. + * + * @param client Custom client + * @return the builder to chain calls (eg.
builder.withClient().build()
) + */ + public AppConfigProviderBuilder withClient(AppConfigDataClient client) { + this.client = client; + return this; + } + + /** + * Mandatory. Provide an environment to the {@link AppConfigProvider} + * + * @param environment the AppConfig environment + * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) + */ + public AppConfigProviderBuilder withEnvironment(String environment) { + this.environment = environment; + return this; + } + + /** + * Mandatory. Provide an application to the {@link AppConfigProvider} + * + * @param application the application to pull configuration from + * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) + */ + public AppConfigProviderBuilder withApplication(String application) { + this.application = application; + return this; + } + + /** + * Provide a CacheManager to the {@link AppConfigProvider} + * + * @param cacheManager the manager that will handle the cache of parameters + * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) + */ + public AppConfigProviderBuilder withCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; + return this; + } + + /** + * Provide a transformationManager to the {@link AppConfigProvider} + * + * @param transformationManager the manager that will handle transformation of parameters + * @return the builder to chain calls (eg.
builder.withTransformationManager().build()
) + */ + public AppConfigProviderBuilder withTransformationManager(TransformationManager transformationManager) { + this.transformationManager = transformationManager; + return this; + } +} diff --git a/powertools-parameters/powertools-parameters-appconfig/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-appconfig/jni-config.json b/powertools-parameters/powertools-parameters-appconfig/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-appconfig/jni-config.json new file mode 100644 index 000000000..2c4de0562 --- /dev/null +++ b/powertools-parameters/powertools-parameters-appconfig/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-appconfig/jni-config.json @@ -0,0 +1,26 @@ +[ +{ + "name":"java.lang.Boolean", + "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.String", + "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"org.apache.maven.surefire.booter.ForkedBooter", + "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"sun.instrument.InstrumentationImpl", + "methods":[{"name":"","parameterTypes":["long","boolean","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] +}, +{ + "name":"sun.management.VMManagementImpl", + "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] +} +] diff --git a/powertools-parameters/powertools-parameters-appconfig/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-appconfig/reflect-config.json b/powertools-parameters/powertools-parameters-appconfig/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-appconfig/reflect-config.json new file mode 100644 index 000000000..7e7b40197 --- /dev/null +++ b/powertools-parameters/powertools-parameters-appconfig/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-appconfig/reflect-config.json @@ -0,0 +1,365 @@ +[ +{ + "name":"com.sun.crypto.provider.AESCipher$General", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ARCFOURCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESedeCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.tools.attach.VirtualMachine" +}, +{ + "name":"java.io.Serializable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.AutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.Class", + "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] +}, +{ + "name":"java.lang.ClassLoader", + "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] +}, +{ + "name":"java.lang.Module", + "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] +}, +{ + "name":"java.lang.Object", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] +}, +{ + "name":"java.lang.ProcessHandle", + "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime", + "methods":[{"name":"version","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime$Version", + "methods":[{"name":"feature","parameterTypes":[] }] +}, +{ + "name":"java.lang.StackWalker" +}, +{ + "name":"java.lang.String" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.annotation.Retention", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.annotation.Target", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.invoke.MethodHandle", + "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles", + "methods":[{"name":"lookup","parameterTypes":[] }] +}, +{ + "name":"java.lang.invoke.MethodHandles$Lookup", + "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] +}, +{ + "name":"java.lang.invoke.MethodType", + "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] +}, +{ + "name":"java.lang.reflect.AccessibleObject", + "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] +}, +{ + "name":"java.lang.reflect.AnnotatedArrayType", + "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedParameterizedType", + "methods":[{"name":"getAnnotatedActualTypeArguments","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedType", + "methods":[{"name":"getType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedTypeVariable", + "methods":[{"name":"getAnnotatedBounds","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedWildcardType", + "methods":[{"name":"getAnnotatedUpperBounds","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Executable", + "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Parameter", + "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.TypeVariable", + "methods":[{"name":"getAnnotatedBounds","parameterTypes":[] }] +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.security.AlgorithmParametersSpi" +}, +{ + "name":"java.security.KeyStoreSpi" +}, +{ + "name":"java.security.SecureRandomParameters" +}, +{ + "name":"java.util.concurrent.ForkJoinTask", + "fields":[{"name":"aux"}, {"name":"status"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"javax.security.auth.x500.X500Principal", + "fields":[{"name":"thisX500Name"}], + "methods":[{"name":"","parameterTypes":["sun.security.x509.X500Name"] }] +}, +{ + "name":"jdk.internal.misc.Unsafe" +}, +{ + "name":"kotlin.Unit" +}, +{ + "name":"kotlin.jvm.JvmInline" +}, +{ + "name":"org.apache.maven.surefire.junitplatform.JUnitPlatformProvider", + "methods":[{"name":"","parameterTypes":["org.apache.maven.surefire.api.provider.ProviderParameters"] }] +}, +{ + "name":"org.apiguardian.api.API", + "queryAllPublicMethods":true +}, +{ + "name":"org.junit.internal.AssumptionViolatedException" +}, +{ + "name":"org.junit.jupiter.api.Test", + "queryAllPublicMethods":true +}, +{ + "name":"org.junit.platform.commons.annotation.Testable", + "queryAllPublicMethods":true +}, +{ + "name":"org.junit.platform.launcher.LauncherSession", + "methods":[{"name":"getLauncher","parameterTypes":[] }] +}, +{ + "name":"org.junit.platform.launcher.core.LauncherFactory", + "methods":[{"name":"openSession","parameterTypes":[] }] +}, +{ + "name":"scala.util.Properties" +}, +{ + "name":"software.amazon.awssdk.awscore.AwsClient", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"software.amazon.awssdk.core.SdkClient", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"serviceName","parameterTypes":[] }] +}, +{ + "name":"software.amazon.awssdk.services.appconfigdata.AppConfigDataClient", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getLatestConfiguration","parameterTypes":["java.util.function.Consumer"] }, {"name":"getLatestConfiguration","parameterTypes":["software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationRequest"] }, {"name":"serviceClientConfiguration","parameterTypes":[] }, {"name":"startConfigurationSession","parameterTypes":["java.util.function.Consumer"] }, {"name":"startConfigurationSession","parameterTypes":["software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest"] }] +}, +{ + "name":"software.amazon.awssdk.utils.SdkAutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"close","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.BaseProvider", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"get","parameterTypes":["java.lang.String"] }, {"name":"get","parameterTypes":["java.lang.String","java.lang.Class"] }, {"name":"getMultiple","parameterTypes":["java.lang.String"] }, {"name":"now","parameterTypes":[] }, {"name":"resetToDefaults","parameterTypes":[] }, {"name":"withMaxAge","parameterTypes":["int","java.time.temporal.ChronoUnit"] }, {"name":"withTransformation","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.ParamProvider", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"software.amazon.lambda.powertools.parameters.appconfig.AppConfigParamAspectTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"parameterInjectedByProvider","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.appconfig.AppConfigParamAspectTest$MyInjectedClass", + "fields":[{"name":"myParameter"}] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.appconfig.AppConfigParametersAspect", + "fields":[{"name":"providerBuilder"}] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.appconfig.AppConfigProvider", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getMultipleValues","parameterTypes":["java.lang.String"] }, {"name":"getValue","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.appconfig.AppConfigProviderTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"getMultipleValuesThrowsException","parameterTypes":[] }, {"name":"getValueNoValueExists","parameterTypes":[] }, {"name":"getValueRetrievesValue","parameterTypes":[] }, {"name":"init","parameterTypes":[] }, {"name":"multipleKeysRetrievalWorks","parameterTypes":[] }, {"name":"testAppConfigProviderBuilderMissingApplication_throwsException","parameterTypes":[] }, {"name":"testAppConfigProviderBuilderMissingEnvironment_throwsException","parameterTypes":[] }, {"name":"testAppConfigProvider_withoutParameter_shouldHaveDefaultTransformationManager","parameterTypes":[] }] +}, +{ + "name":"sun.reflect.ReflectionFactory", + "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.security.SecureRandomParameters"] }] +}, +{ + "name":"sun.security.provider.SHA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.X509Factory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSAKeyFactory$Legacy", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.SSLContextImpl$TLSContext", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.x509.AuthorityInfoAccessExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.AuthorityKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.BasicConstraintsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CRLDistributionPointsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CertificatePoliciesExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.ExtendedKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.IssuerAlternativeNameExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.KeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.NetscapeCertTypeExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.PrivateKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.SubjectAlternativeNameExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.SubjectKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +} +] diff --git a/powertools-parameters/powertools-parameters-appconfig/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-appconfig/resource-config.json b/powertools-parameters/powertools-parameters-appconfig/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-appconfig/resource-config.json new file mode 100644 index 000000000..63bcd679e --- /dev/null +++ b/powertools-parameters/powertools-parameters-appconfig/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-appconfig/resource-config.json @@ -0,0 +1,59 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.net.spi.URLStreamHandlerProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.configuration.Configuration\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.presentation.Representation\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.engine.TestEngine\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.LauncherDiscoveryListener\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.LauncherSessionListener\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.PostDiscoveryFilter\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.TestExecutionListener\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\Qjunit-platform.properties\\E" + }, { + "pattern":"\\Qmockito-extensions/org.mockito.plugins.AnnotationEngine\\E" + }, { + "pattern":"\\Qmockito-extensions/org.mockito.plugins.DoNotMockEnforcer\\E" + }, { + "pattern":"\\Qmockito-extensions/org.mockito.plugins.InstantiatorProvider2\\E" + }, { + "pattern":"\\Qmockito-extensions/org.mockito.plugins.MemberAccessor\\E" + }, { + "pattern":"\\Qmockito-extensions/org.mockito.plugins.MockMaker\\E" + }, { + "pattern":"\\Qmockito-extensions/org.mockito.plugins.MockResolver\\E" + }, { + "pattern":"\\Qmockito-extensions/org.mockito.plugins.MockitoLogger\\E" + }, { + "pattern":"\\Qmockito-extensions/org.mockito.plugins.PluginSwitch\\E" + }, { + "pattern":"\\Qmockito-extensions/org.mockito.plugins.StackTraceCleanerProvider\\E" + }, { + "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" + }, { + "pattern":"\\Qsoftware/amazon/awssdk/global/handlers/execution.interceptors\\E" + }, { + "pattern":"\\Qsoftware/amazon/awssdk/services/appconfigdata/execution.interceptors\\E" + }, { + "pattern":"\\Qversion.properties\\E" + }, { + "pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt72b/nfc.nrm\\E" + }]}, + "bundles":[] +} diff --git a/powertools-parameters/powertools-parameters-appconfig/src/test/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigParamAspectTest.java b/powertools-parameters/powertools-parameters-appconfig/src/test/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigParamAspectTest.java new file mode 100644 index 000000000..f50e88ec5 --- /dev/null +++ b/powertools-parameters/powertools-parameters-appconfig/src/test/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigParamAspectTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.appconfig; + +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.BiFunction; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class AppConfigParamAspectTest { + + @Test + public void parameterInjectedByProvider() throws Exception { + // Setup our aspect to return a mocked AppConfigProvider + String environment = "myEnvironment"; + String appName = "myApp"; + String key = "myKey"; + String value = "myValue"; + AppConfigProvider provider = Mockito.mock(AppConfigProvider.class); + BiFunction providerBuilder = (String env, String app) -> { + if (env.equals(environment) && app.equals(appName)) { + return provider; + } + throw new RuntimeException("Whoops! Asked for an app/env that we weren't configured for"); + }; + writeStaticField(AppConfigParametersAspect.class, "providerBuilder", providerBuilder, true); + + // Setup our mocked AppConfigProvider to return a value for our test data + Mockito.when(provider.get(key)).thenReturn(value); + + // Create an instance of a class and let the AppConfigParametersAspect inject it + MyInjectedClass obj = new MyInjectedClass(); + assertThat(obj.myParameter).isEqualTo(value); + } + + class MyInjectedClass { + @AppConfigParam(application = "myApp", environment = "myEnvironment", key = "myKey") + public String myParameter; + } + +} diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java b/powertools-parameters/powertools-parameters-appconfig/src/test/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigProviderTest.java similarity index 94% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java rename to powertools-parameters/powertools-parameters-appconfig/src/test/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigProviderTest.java index f467dca72..039611875 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/AppConfigProviderTest.java +++ b/powertools-parameters/powertools-parameters-appconfig/src/test/java/software/amazon/lambda/powertools/parameters/appconfig/AppConfigProviderTest.java @@ -12,12 +12,14 @@ * */ -package software.amazon.lambda.powertools.parameters; +package software.amazon.lambda.powertools.parameters.appconfig; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatRuntimeException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.MockitoAnnotations.openMocks; +import static software.amazon.lambda.powertools.parameters.transform.Transformer.json; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -193,18 +195,6 @@ public void getMultipleValuesThrowsException() { .withMessage("Retrieving multiple parameter values is not supported with the AWS App Config Provider"); } - @Test - public void testAppConfigProviderBuilderMissingCacheManager_throwsException() { - - // Act & Assert - assertThatIllegalStateException().isThrownBy(() -> AppConfigProvider.builder() - .withEnvironment(environmentName) - .withApplication(applicationName) - .withClient(client) - .build()) - .withMessage("No CacheManager provided; please provide one"); - } - @Test public void testAppConfigProviderBuilderMissingEnvironment_throwsException() { @@ -228,4 +218,15 @@ public void testAppConfigProviderBuilderMissingApplication_throwsException() { .build()) .withMessage("No application provided; please provide one"); } + @Test + public void testAppConfigProvider_withoutParameter_shouldHaveDefaultTransformationManager() { + + // Act + AppConfigProvider appConfigProvider = AppConfigProvider.builder() + .withEnvironment("test") + .withApplication("app") + .build(); + // Assert + assertDoesNotThrow(()->appConfigProvider.withTransformation(json)); + } } diff --git a/powertools-parameters/powertools-parameters-dynamodb/pom.xml b/powertools-parameters/powertools-parameters-dynamodb/pom.xml new file mode 100644 index 000000000..1220b1eb5 --- /dev/null +++ b/powertools-parameters/powertools-parameters-dynamodb/pom.xml @@ -0,0 +1,190 @@ + + + 4.0.0 + + + software.amazon.lambda + powertools-parent + 2.0.0-RC1 + ../../pom.xml + + + powertools-parameters-dynamodb + Powertools for AWS Lambda (Java) library Parameters - DynamoDB + DynamoDB implementation for the Parameters module + + + + software.amazon.lambda + powertools-parameters + ${project.version} + + + + software.amazon.awssdk + dynamodb + + + software.amazon.awssdk + apache-client + + + software.amazon.awssdk + netty-nio-client + + + + + org.aspectj + aspectjrt + provided + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + + + org.apache.commons + commons-lang3 + test + + + org.assertj + assertj-core + test + + + org.aspectj + aspectjweaver + test + + + + + + generate-graalvm-files + + + org.mockito + mockito-subclass + 5.6.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-dynamodb,experimental-class-define-support + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + 5.6.0 + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.2 + true + + + test-native + + test + + test + + + + powertools-parameters-dynamodb + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + --native-image-info + -H:+UnlockExperimentalVMOptions + -H:Log=registerResource:5 + + + + + + + + + + + + maven-surefire-plugin + 3.1.2 + + + eu-central-1 + + + + + + diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/Param.java b/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbParam.java similarity index 52% rename from powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/Param.java rename to powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbParam.java index 7ffb0310c..946786cb4 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/Param.java +++ b/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbParam.java @@ -12,7 +12,7 @@ * */ -package software.amazon.lambda.powertools.parameters; +package software.amazon.lambda.powertools.parameters.dynamodb; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -21,25 +21,31 @@ import software.amazon.lambda.powertools.parameters.transform.Transformer; /** - * {@code Param} is used to signal that the annotated field should be - * populated with a value retrieved from a parameter store through a {@link ParamProvider}. + * Inject a parameter from the DynamoDB Parameter Store into a field. You can also use + * {@code DynamoDbProviderBuilder} to obtain DynamoDB values directly, rather than injecting them implicitly. * - *

By default {@code Param} use {@link SSMProvider} as parameter provider. This can be overridden specifying - * the annotation variable {@code Param(provider = )}.
- * The library provide a provider for AWS System Manager Parameters Store ({@link SSMProvider}) and a provider - * for AWS Secrets Manager ({@link SecretsProvider}). - * The user can implement a custom provider by extending the abstract class {@link BaseProvider}.

- * - *

If the parameter value requires transformation before being assigned to the annotated field - * users can specify a {@link Transformer} - *

+ * Usage: + *
+ * @DynamoDbParam(key = "my-param", table = "my-table")
+ * String myParameter;
+ * 
*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) -public @interface Param { +public @interface DynamoDbParam { + /** + * Mandatory. Partition key from the DynamoDB table + */ String key(); - Class provider() default SSMProvider.class; + /** + * Mandatory. Table name for the DynamoDB table + * @return + */ + String table(); + /** + * Optional Provide a Transformer to transform the returned parameter values. + */ Class transformer() default Transformer.class; } diff --git a/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbParamAspect.java b/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbParamAspect.java new file mode 100644 index 000000000..1aa022cbd --- /dev/null +++ b/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbParamAspect.java @@ -0,0 +1,51 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.dynamodb; + +import java.util.function.Function; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.FieldSignature; +import software.amazon.lambda.powertools.parameters.BaseParamAspect; +import software.amazon.lambda.powertools.parameters.BaseProvider; + +/** + * Provides the Amazon DynamoDB parameter aspect. This aspect is responsible for injecting + * parameters from DynamoDB into fields annotated with @DynamoDbParam. See the + * README and Powertools for Lambda (Java) documentation for information on using this feature. + */ +@Aspect +public class DynamoDbParamAspect extends BaseParamAspect { + + private static Function providerBuilder = + (String table) -> DynamoDbProvider.builder() + .withTable(table) + .build(); + + @Pointcut("get(* *) && @annotation(ddbConfigParam)") + public void getParam(DynamoDbParam ddbConfigParam) { + } + + @Around("getParam(ddbConfigParam)") + public Object injectParam(final ProceedingJoinPoint joinPoint, final DynamoDbParam ddbConfigParam) { + + BaseProvider provider = providerBuilder.apply(ddbConfigParam.table()); + return getAndTransform(ddbConfigParam.key(), ddbConfigParam.transformer(), provider, + (FieldSignature) joinPoint.getSignature()); + } + +} diff --git a/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbProvider.java b/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbProvider.java new file mode 100644 index 000000000..4a1476e38 --- /dev/null +++ b/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbProvider.java @@ -0,0 +1,121 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.dynamodb; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; +import software.amazon.lambda.powertools.parameters.BaseProvider; +import software.amazon.lambda.powertools.parameters.ParamProvider; +import software.amazon.lambda.powertools.parameters.cache.CacheManager; +import software.amazon.lambda.powertools.parameters.dynamodb.exception.DynamoDbProviderSchemaException; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; + +/** + * Implements a {@link ParamProvider} on top of Amazon DynamoDB. The schema of the table + * is described in the Powertools for AWS Lambda (Java) documentation. + * + * @see Parameters provider documentation + */ +public class DynamoDbProvider extends BaseProvider { + + private final DynamoDbClient client; + private final String tableName; + + DynamoDbProvider(CacheManager cacheManager, TransformationManager transformationManager, DynamoDbClient client, + String tableName) { + super(cacheManager, transformationManager); + this.client = client; + this.tableName = tableName; + } + + /** + * Create a builder that can be used to configure and create a {@link DynamoDbProvider}. + * + * @return a new instance of {@link DynamoDbProviderBuilder} + */ + public static DynamoDbProviderBuilder builder() { + return new DynamoDbProviderBuilder(); + } + + /** + * Return a single value from the DynamoDB parameter provider. + * + * @param key key of the parameter + * @return The value, if it exists, null if it doesn't. Throws if the row exists but doesn't match the schema. + */ + @Override + protected String getValue(String key) { + GetItemResponse resp = client.getItem(GetItemRequest.builder() + .tableName(tableName) + .key(Collections.singletonMap("id", AttributeValue.fromS(key))) + .attributesToGet("value") + .build()); + + // If we have an item at the key, we should be able to get a 'val' out of it. If not it's + // exceptional. + // If we don't have an item at the key, we should return null. + if (resp.hasItem() && !resp.item().values().isEmpty()) { + if (!resp.item().containsKey("value")) { + throw new DynamoDbProviderSchemaException("Missing 'value': " + resp.item()); + } + return resp.item().get("value").s(); + } + + return null; + } + + /** + * Returns multiple values from the DynamoDB parameter provider. + * + * @param path Parameter store path + * @return All values matching the given path, and an empty map if none do. Throws if any records exist that don't match the schema. + */ + @Override + protected Map getMultipleValues(String path) { + + QueryResponse resp = client.query(QueryRequest.builder() + .tableName(tableName) + .keyConditionExpression("id = :v_id") + .expressionAttributeValues(Collections.singletonMap(":v_id", AttributeValue.fromS(path))) + .build()); + + return resp + .items() + .stream() + .peek((i) -> + { + if (!i.containsKey("sk")) { + throw new DynamoDbProviderSchemaException("Missing 'sk': " + i); + } + if (!i.containsKey("value")) { + throw new DynamoDbProviderSchemaException("Missing 'value': " + i); + } + }) + .collect( + Collectors.toMap( + (i) -> i.get("sk").s(), + (i) -> i.get("value").s())); + + + } + +} diff --git a/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbProviderBuilder.java b/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbProviderBuilder.java new file mode 100644 index 000000000..b98ff285d --- /dev/null +++ b/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbProviderBuilder.java @@ -0,0 +1,116 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.dynamodb; + +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; +import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.lambda.powertools.common.internal.UserAgentConfigurator; +import software.amazon.lambda.powertools.parameters.BaseProvider; +import software.amazon.lambda.powertools.parameters.ParamProvider; +import software.amazon.lambda.powertools.parameters.cache.CacheManager; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; + +/** + * Implements a {@link ParamProvider} on top of the DynamoDB service. DynamoDB provides + */ +public class DynamoDbProviderBuilder { + private DynamoDbClient client; + private String table; + private CacheManager cacheManager; + private TransformationManager transformationManager; + + static DynamoDbClient createClient() { + return DynamoDbClient.builder() + .httpClientBuilder(UrlConnectionHttpClient.builder()) + .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) + .overrideConfiguration(ClientOverrideConfiguration.builder() + .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, + UserAgentConfigurator.getUserAgent(BaseProvider.PARAMETERS)).build()) + .build(); + } + + /** + * Create a {@link DynamoDbProvider} instance. + * + * @return a {@link DynamoDbProvider} + */ + public DynamoDbProvider build() { + if (cacheManager == null) { + cacheManager = new CacheManager(); + } + if (table == null) { + throw new IllegalStateException("No DynamoDB table name provided; please provide one"); + } + DynamoDbProvider provider; + if (client == null) { + client = createClient(); + } + if (transformationManager == null) { + transformationManager = new TransformationManager(); + } + provider = new DynamoDbProvider(cacheManager, transformationManager, client, table); + + return provider; + } + + /** + * Set custom {@link DynamoDbClient} to pass to the {@link DynamoDbClient}.
+ * Use it if you want to customize the region or any other part of the client. + * + * @param client Custom client + * @return the builder to chain calls (eg.
builder.withClient().build()
) + */ + public DynamoDbProviderBuilder withClient(DynamoDbClient client) { + this.client = client; + return this; + } + + /** + * Provide a CacheManager to the {@link DynamoDbProvider} + * + * @param cacheManager the manager that will handle the cache of parameters + * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) + */ + public DynamoDbProviderBuilder withCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; + return this; + } + + /** + * Mandatory. Provide a DynamoDB table to the {@link DynamoDbProvider} + * + * @param table the table that parameters will be retrieved from. + * @return the builder to chain calls (eg.
builder.withTable().build()
) + */ + public DynamoDbProviderBuilder withTable(String table) { + this.table = table; + return this; + } + + /** + * Provide a transformationManager to the {@link DynamoDbProvider} + * + * @param transformationManager the manager that will handle transformation of parameters + * @return the builder to chain calls (eg.
builder.withTransformationManager().build()
) + */ + public DynamoDbProviderBuilder withTransformationManager(TransformationManager transformationManager) { + this.transformationManager = transformationManager; + return this; + } +} diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/DynamoDbProviderSchemaException.java b/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/exception/DynamoDbProviderSchemaException.java similarity index 92% rename from powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/DynamoDbProviderSchemaException.java rename to powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/exception/DynamoDbProviderSchemaException.java index 77df6e3d3..4a22dbc99 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/DynamoDbProviderSchemaException.java +++ b/powertools-parameters/powertools-parameters-dynamodb/src/main/java/software/amazon/lambda/powertools/parameters/dynamodb/exception/DynamoDbProviderSchemaException.java @@ -12,7 +12,7 @@ * */ -package software.amazon.lambda.powertools.parameters.exception; +package software.amazon.lambda.powertools.parameters.dynamodb.exception; /** * Thrown when the DynamoDbProvider comes across parameter data that diff --git a/powertools-parameters/powertools-parameters-dynamodb/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-dynamodb/jni-config.json b/powertools-parameters/powertools-parameters-dynamodb/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-dynamodb/jni-config.json new file mode 100644 index 000000000..2689203aa --- /dev/null +++ b/powertools-parameters/powertools-parameters-dynamodb/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-dynamodb/jni-config.json @@ -0,0 +1,22 @@ +[ +{ + "name":"java.lang.Boolean", + "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.String", + "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"sun.instrument.InstrumentationImpl", + "methods":[{"name":"","parameterTypes":["long","boolean","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] +}, +{ + "name":"sun.management.VMManagementImpl", + "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] +} +] diff --git a/powertools-parameters/powertools-parameters-dynamodb/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-dynamodb/reflect-config.json b/powertools-parameters/powertools-parameters-dynamodb/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-dynamodb/reflect-config.json new file mode 100644 index 000000000..77e953aac --- /dev/null +++ b/powertools-parameters/powertools-parameters-dynamodb/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-dynamodb/reflect-config.json @@ -0,0 +1,326 @@ +[ +{ + "name":"com.sun.crypto.provider.AESCipher$General", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ARCFOURCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESedeCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.tools.attach.VirtualMachine" +}, +{ + "name":"java.io.Serializable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.AutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.Class", + "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] +}, +{ + "name":"java.lang.ClassLoader", + "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] +}, +{ + "name":"java.lang.Module", + "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] +}, +{ + "name":"java.lang.Object", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] +}, +{ + "name":"java.lang.ProcessHandle", + "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime", + "methods":[{"name":"version","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime$Version", + "methods":[{"name":"feature","parameterTypes":[] }] +}, +{ + "name":"java.lang.StackWalker" +}, +{ + "name":"java.lang.String" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.annotation.Retention", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.annotation.Target", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.invoke.MethodHandle", + "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles", + "methods":[{"name":"lookup","parameterTypes":[] }] +}, +{ + "name":"java.lang.invoke.MethodHandles$Lookup", + "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] +}, +{ + "name":"java.lang.invoke.MethodType", + "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] +}, +{ + "name":"java.lang.reflect.AccessibleObject", + "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] +}, +{ + "name":"java.lang.reflect.AnnotatedArrayType", + "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedParameterizedType", + "methods":[{"name":"getAnnotatedActualTypeArguments","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedType", + "methods":[{"name":"getType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedTypeVariable", + "methods":[{"name":"getAnnotatedBounds","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedWildcardType", + "methods":[{"name":"getAnnotatedUpperBounds","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Executable", + "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Parameter", + "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.TypeVariable", + "methods":[{"name":"getAnnotatedBounds","parameterTypes":[] }] +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.security.AlgorithmParametersSpi" +}, +{ + "name":"java.security.KeyStoreSpi" +}, +{ + "name":"java.security.SecureRandomParameters" +}, +{ + "name":"java.util.concurrent.ForkJoinTask", + "fields":[{"name":"aux"}, {"name":"status"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"javax.security.auth.x500.X500Principal", + "fields":[{"name":"thisX500Name"}], + "methods":[{"name":"","parameterTypes":["sun.security.x509.X500Name"] }] +}, +{ + "name":"jdk.internal.misc.Unsafe" +}, +{ + "name":"kotlin.Unit" +}, +{ + "name":"kotlin.jvm.JvmInline" +}, +{ + "name":"org.apiguardian.api.API", + "queryAllPublicMethods":true +}, +{ + "name":"scala.util.Properties" +}, +{ + "name":"software.amazon.awssdk.awscore.AwsClient", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"software.amazon.awssdk.core.SdkClient", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"serviceName","parameterTypes":[] }] +}, +{ + "name":"software.amazon.awssdk.services.dynamodb.DynamoDbClient", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"batchExecuteStatement","parameterTypes":["java.util.function.Consumer"] }, {"name":"batchExecuteStatement","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.BatchExecuteStatementRequest"] }, {"name":"batchGetItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"batchGetItem","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest"] }, {"name":"batchGetItemPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"batchGetItemPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest"] }, {"name":"batchWriteItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"batchWriteItem","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.BatchWriteItemRequest"] }, {"name":"createBackup","parameterTypes":["java.util.function.Consumer"] }, {"name":"createBackup","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.CreateBackupRequest"] }, {"name":"createGlobalTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"createGlobalTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.CreateGlobalTableRequest"] }, {"name":"createTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"createTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.CreateTableRequest"] }, {"name":"deleteBackup","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteBackup","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DeleteBackupRequest"] }, {"name":"deleteItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteItem","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest"] }, {"name":"deleteResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteResourcePolicy","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DeleteResourcePolicyRequest"] }, {"name":"deleteTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest"] }, {"name":"describeBackup","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeBackup","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeBackupRequest"] }, {"name":"describeContinuousBackups","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeContinuousBackups","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeContinuousBackupsRequest"] }, {"name":"describeContributorInsights","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeContributorInsights","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeContributorInsightsRequest"] }, {"name":"describeEndpoints","parameterTypes":[] }, {"name":"describeEndpoints","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeEndpoints","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeEndpointsRequest"] }, {"name":"describeExport","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeExport","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeExportRequest"] }, {"name":"describeGlobalTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeGlobalTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeGlobalTableRequest"] }, {"name":"describeGlobalTableSettings","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeGlobalTableSettings","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeGlobalTableSettingsRequest"] }, {"name":"describeImport","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeImport","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeImportRequest"] }, {"name":"describeKinesisStreamingDestination","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeKinesisStreamingDestination","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeKinesisStreamingDestinationRequest"] }, {"name":"describeLimits","parameterTypes":[] }, {"name":"describeLimits","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeLimits","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeLimitsRequest"] }, {"name":"describeTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest"] }, {"name":"describeTableReplicaAutoScaling","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeTableReplicaAutoScaling","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeTableReplicaAutoScalingRequest"] }, {"name":"describeTimeToLive","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeTimeToLive","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeTimeToLiveRequest"] }, {"name":"disableKinesisStreamingDestination","parameterTypes":["java.util.function.Consumer"] }, {"name":"disableKinesisStreamingDestination","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DisableKinesisStreamingDestinationRequest"] }, {"name":"enableKinesisStreamingDestination","parameterTypes":["java.util.function.Consumer"] }, {"name":"enableKinesisStreamingDestination","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.EnableKinesisStreamingDestinationRequest"] }, {"name":"executeStatement","parameterTypes":["java.util.function.Consumer"] }, {"name":"executeStatement","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ExecuteStatementRequest"] }, {"name":"executeTransaction","parameterTypes":["java.util.function.Consumer"] }, {"name":"executeTransaction","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ExecuteTransactionRequest"] }, {"name":"exportTableToPointInTime","parameterTypes":["java.util.function.Consumer"] }, {"name":"exportTableToPointInTime","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ExportTableToPointInTimeRequest"] }, {"name":"getItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"getItem","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.GetItemRequest"] }, {"name":"getResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"getResourcePolicy","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.GetResourcePolicyRequest"] }, {"name":"importTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"importTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ImportTableRequest"] }, {"name":"listBackups","parameterTypes":[] }, {"name":"listBackups","parameterTypes":["java.util.function.Consumer"] }, {"name":"listBackups","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListBackupsRequest"] }, {"name":"listContributorInsights","parameterTypes":["java.util.function.Consumer"] }, {"name":"listContributorInsights","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListContributorInsightsRequest"] }, {"name":"listContributorInsightsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listContributorInsightsPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListContributorInsightsRequest"] }, {"name":"listExports","parameterTypes":["java.util.function.Consumer"] }, {"name":"listExports","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListExportsRequest"] }, {"name":"listExportsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listExportsPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListExportsRequest"] }, {"name":"listGlobalTables","parameterTypes":[] }, {"name":"listGlobalTables","parameterTypes":["java.util.function.Consumer"] }, {"name":"listGlobalTables","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListGlobalTablesRequest"] }, {"name":"listImports","parameterTypes":["java.util.function.Consumer"] }, {"name":"listImports","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListImportsRequest"] }, {"name":"listImportsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listImportsPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListImportsRequest"] }, {"name":"listTables","parameterTypes":[] }, {"name":"listTables","parameterTypes":["java.util.function.Consumer"] }, {"name":"listTables","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListTablesRequest"] }, {"name":"listTablesPaginator","parameterTypes":[] }, {"name":"listTablesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listTablesPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListTablesRequest"] }, {"name":"listTagsOfResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"listTagsOfResource","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListTagsOfResourceRequest"] }, {"name":"putItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"putItem","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.PutItemRequest"] }, {"name":"putResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"putResourcePolicy","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.PutResourcePolicyRequest"] }, {"name":"query","parameterTypes":["java.util.function.Consumer"] }, {"name":"query","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.QueryRequest"] }, {"name":"queryPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"queryPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.QueryRequest"] }, {"name":"restoreTableFromBackup","parameterTypes":["java.util.function.Consumer"] }, {"name":"restoreTableFromBackup","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.RestoreTableFromBackupRequest"] }, {"name":"restoreTableToPointInTime","parameterTypes":["java.util.function.Consumer"] }, {"name":"restoreTableToPointInTime","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.RestoreTableToPointInTimeRequest"] }, {"name":"scan","parameterTypes":["java.util.function.Consumer"] }, {"name":"scan","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ScanRequest"] }, {"name":"scanPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"scanPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ScanRequest"] }, {"name":"serviceClientConfiguration","parameterTypes":[] }, {"name":"tagResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"tagResource","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.TagResourceRequest"] }, {"name":"transactGetItems","parameterTypes":["java.util.function.Consumer"] }, {"name":"transactGetItems","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.TransactGetItemsRequest"] }, {"name":"transactWriteItems","parameterTypes":["java.util.function.Consumer"] }, {"name":"transactWriteItems","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest"] }, {"name":"untagResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"untagResource","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UntagResourceRequest"] }, {"name":"updateContinuousBackups","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateContinuousBackups","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateContinuousBackupsRequest"] }, {"name":"updateContributorInsights","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateContributorInsights","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateContributorInsightsRequest"] }, {"name":"updateGlobalTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateGlobalTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateGlobalTableRequest"] }, {"name":"updateGlobalTableSettings","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateGlobalTableSettings","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateGlobalTableSettingsRequest"] }, {"name":"updateItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateItem","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest"] }, {"name":"updateKinesisStreamingDestination","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateKinesisStreamingDestination","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateKinesisStreamingDestinationRequest"] }, {"name":"updateTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateTableRequest"] }, {"name":"updateTableReplicaAutoScaling","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateTableReplicaAutoScaling","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateTableReplicaAutoScalingRequest"] }, {"name":"updateTimeToLive","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateTimeToLive","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateTimeToLiveRequest"] }, {"name":"waiter","parameterTypes":[] }] +}, +{ + "name":"software.amazon.awssdk.utils.SdkAutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"close","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.BaseProvider", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"get","parameterTypes":["java.lang.String"] }, {"name":"get","parameterTypes":["java.lang.String","java.lang.Class"] }, {"name":"getMultiple","parameterTypes":["java.lang.String"] }, {"name":"now","parameterTypes":[] }, {"name":"resetToDefaults","parameterTypes":[] }, {"name":"withMaxAge","parameterTypes":["int","java.time.temporal.ChronoUnit"] }, {"name":"withTransformation","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.ParamProvider", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"software.amazon.lambda.powertools.parameters.dynamodb.DynamoDbParamAspect", + "fields":[{"name":"providerBuilder"}] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.dynamodb.DynamoDbProvider", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getMultipleValues","parameterTypes":["java.lang.String"] }, {"name":"getValue","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.transform.TransformationManager", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"performBasicTransformation","parameterTypes":["java.lang.String"] }, {"name":"performComplexTransformation","parameterTypes":["java.lang.String","java.lang.Class"] }, {"name":"setTransformer","parameterTypes":["java.lang.Class"] }, {"name":"shouldTransform","parameterTypes":[] }] +}, +{ + "name":"sun.reflect.ReflectionFactory", + "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.security.SecureRandomParameters"] }] +}, +{ + "name":"sun.security.provider.SHA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.X509Factory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSAKeyFactory$Legacy", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.SSLContextImpl$TLSContext", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.x509.AuthorityInfoAccessExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.AuthorityKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.BasicConstraintsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CRLDistributionPointsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CertificatePoliciesExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.ExtendedKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.IssuerAlternativeNameExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.KeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.NetscapeCertTypeExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.PrivateKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.SubjectAlternativeNameExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.SubjectKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +} +] diff --git a/powertools-parameters/powertools-parameters-dynamodb/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-dynamodb/resource-config.json b/powertools-parameters/powertools-parameters-dynamodb/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-dynamodb/resource-config.json new file mode 100644 index 000000000..abbae66cf --- /dev/null +++ b/powertools-parameters/powertools-parameters-dynamodb/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-dynamodb/resource-config.json @@ -0,0 +1,23 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.net.spi.URLStreamHandlerProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" + }, { + "pattern":"\\Qsoftware/amazon/awssdk/global/handlers/execution.interceptors\\E" + }, { + "pattern":"\\Qsoftware/amazon/awssdk/services/dynamodb/execution.interceptors\\E" + }, { + "pattern":"\\Qversion.properties\\E" + }, { + "pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt72b/nfc.nrm\\E" + }]}, + "bundles":[] +} diff --git a/powertools-parameters/powertools-parameters-dynamodb/src/test/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbParamAspectTest.java b/powertools-parameters/powertools-parameters-dynamodb/src/test/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbParamAspectTest.java new file mode 100644 index 000000000..07e93a7c1 --- /dev/null +++ b/powertools-parameters/powertools-parameters-dynamodb/src/test/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbParamAspectTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.dynamodb; + +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Function; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class DynamoDbParamAspectTest { + + @Test + public void parameterInjectedByProvider() throws Exception { + // Setup our aspect to return a mocked DynamoDbProvider + String tableName = "my-test-tablename"; + String key = "myKey"; + String value = "myValue"; + DynamoDbProvider provider = Mockito.mock(DynamoDbProvider.class); + + Function providerBuilder = (String table) -> { + if (table.equals(tableName)) { + return provider; + } + throw new RuntimeException("Whoops! Asked for an app/env that we weren't configured for"); + }; + writeStaticField(DynamoDbParamAspect.class, "providerBuilder", providerBuilder, true); + + // Setup our mocked DynamoDbProvider to return a value for our test data + Mockito.when(provider.get(key)).thenReturn(value); + + // Create an instance of a class and let the AppConfigParametersAspect inject it + MyInjectedClass obj = new MyInjectedClass(); + assertThat(obj.myParameter).isEqualTo(value); + } + + class MyInjectedClass { + @DynamoDbParam(table = "my-test-tablename", key = "myKey") + public String myParameter; + } + +} diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderE2ETest.java b/powertools-parameters/powertools-parameters-dynamodb/src/test/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbProviderE2ETest.java similarity index 96% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderE2ETest.java rename to powertools-parameters/powertools-parameters-dynamodb/src/test/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbProviderE2ETest.java index 18212b45c..2695938d8 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderE2ETest.java +++ b/powertools-parameters/powertools-parameters-dynamodb/src/test/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbProviderE2ETest.java @@ -12,7 +12,7 @@ * */ -package software.amazon.lambda.powertools.parameters; +package software.amazon.lambda.powertools.parameters.dynamodb; import static org.assertj.core.api.Assertions.assertThat; @@ -104,7 +104,7 @@ public void TestGetValues() { } private DynamoDbProvider makeProvider(String tableName) { - return new DynamoDbProvider(new CacheManager(), DynamoDbClient.builder() + return new DynamoDbProvider(new CacheManager(), null, DynamoDbClient.builder() .httpClientBuilder(UrlConnectionHttpClient.builder()).build(), tableName); } diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderTest.java b/powertools-parameters/powertools-parameters-dynamodb/src/test/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbProviderTest.java similarity index 91% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderTest.java rename to powertools-parameters/powertools-parameters-dynamodb/src/test/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbProviderTest.java index 2cf84dad4..10d756c69 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/DynamoDbProviderTest.java +++ b/powertools-parameters/powertools-parameters-dynamodb/src/test/java/software/amazon/lambda/powertools/parameters/dynamodb/DynamoDbProviderTest.java @@ -12,11 +12,13 @@ * */ -package software.amazon.lambda.powertools.parameters; +package software.amazon.lambda.powertools.parameters.dynamodb; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.MockitoAnnotations.openMocks; +import static software.amazon.lambda.powertools.parameters.transform.Transformer.json; import java.util.HashMap; import java.util.Map; @@ -35,7 +37,7 @@ import software.amazon.awssdk.services.dynamodb.model.QueryRequest; import software.amazon.awssdk.services.dynamodb.model.QueryResponse; import software.amazon.lambda.powertools.parameters.cache.CacheManager; -import software.amazon.lambda.powertools.parameters.exception.DynamoDbProviderSchemaException; +import software.amazon.lambda.powertools.parameters.dynamodb.exception.DynamoDbProviderSchemaException; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; public class DynamoDbProviderTest { @@ -55,7 +57,7 @@ public class DynamoDbProviderTest { public void init() { openMocks(this); CacheManager cacheManager = new CacheManager(); - provider = new DynamoDbProvider(cacheManager, client, tableName); + provider = new DynamoDbProvider(cacheManager, transformationManager, client, tableName); } @@ -218,21 +220,21 @@ public void getValuesWithMalformedRowThrows() { } @Test - public void testDynamoDBBuilderMissingCacheManager_throwsException() { + public void testDynamoDBBuilderMissingTable_throwsException() { // Act & Assert assertThatIllegalStateException().isThrownBy(() -> DynamoDbProvider.builder() - .withTable("table") + .withCacheManager(new CacheManager()) .build()); } - @Test - public void testDynamoDBBuilderMissingTable_throwsException() { + public void testDynamoDBBuilder_withoutParameter_shouldHaveDefaultTransformationManager() { - // Act & Assert - assertThatIllegalStateException().isThrownBy(() -> DynamoDbProvider.builder() - .withCacheManager(new CacheManager()) - .build()); + // Act + DynamoDbProvider dynamoDbProvider = DynamoDbProvider.builder().withTable("test-table") + .build(); + // Assert + assertDoesNotThrow(()->dynamoDbProvider.withTransformation(json)); } } diff --git a/powertools-parameters/powertools-parameters-secrets/pom.xml b/powertools-parameters/powertools-parameters-secrets/pom.xml new file mode 100644 index 000000000..93bd9a26e --- /dev/null +++ b/powertools-parameters/powertools-parameters-secrets/pom.xml @@ -0,0 +1,190 @@ + + + 4.0.0 + + + software.amazon.lambda + powertools-parent + 2.0.0-RC1 + ../../pom.xml + + + powertools-parameters-secrets + Powertools for AWS Lambda (Java) library Parameters - Secrets Manager + Secrets Manager implementation for the Parameters module + + + + software.amazon.lambda + powertools-parameters + ${project.version} + + + + software.amazon.awssdk + secretsmanager + + + software.amazon.awssdk + apache-client + + + software.amazon.awssdk + netty-nio-client + + + + + org.aspectj + aspectjrt + provided + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + + + org.apache.commons + commons-lang3 + test + + + org.assertj + assertj-core + test + + + org.aspectj + aspectjweaver + test + + + + + + generate-graalvm-files + + + org.mockito + mockito-subclass + 5.6.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-secrets,experimental-class-define-support + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + 5.6.0 + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.2 + true + + + test-native + + test + + test + + + + powertools-parameters-secrets + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + --native-image-info + -H:+UnlockExperimentalVMOptions + -H:Log=registerResource:5 + + + + + + + + + + + + maven-surefire-plugin + 3.1.2 + + + eu-central-1 + + + + + + diff --git a/powertools-parameters/powertools-parameters-secrets/src/main/java/software/amazon/lambda/powertools/parameters/secrets/SecretsParam.java b/powertools-parameters/powertools-parameters-secrets/src/main/java/software/amazon/lambda/powertools/parameters/secrets/SecretsParam.java new file mode 100644 index 000000000..f9c110c49 --- /dev/null +++ b/powertools-parameters/powertools-parameters-secrets/src/main/java/software/amazon/lambda/powertools/parameters/secrets/SecretsParam.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.secrets; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import software.amazon.lambda.powertools.parameters.transform.Transformer; + +/** + * Inject a parameter from the Secrets Manager into a field. You can also use + * {@code SecretsProviderBuilder} to obtain Secrets Manager values directly, rather than + * injecting them implicitly. + * + *
+ * @SecretsParam(key = "my-secret")
+ * String mySecret;
+ * 
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SecretsParam { + /** + * Mandatory. key from the secrets manager store. + * @return + */ + String key(); + + /** + * Optional. a transfer to apply to the value + */ + Class transformer() default Transformer.class; +} diff --git a/powertools-parameters/powertools-parameters-secrets/src/main/java/software/amazon/lambda/powertools/parameters/secrets/SecretsParamAspect.java b/powertools-parameters/powertools-parameters-secrets/src/main/java/software/amazon/lambda/powertools/parameters/secrets/SecretsParamAspect.java new file mode 100644 index 000000000..748c88cc9 --- /dev/null +++ b/powertools-parameters/powertools-parameters-secrets/src/main/java/software/amazon/lambda/powertools/parameters/secrets/SecretsParamAspect.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.secrets; + +import java.util.function.Supplier; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.FieldSignature; +import software.amazon.lambda.powertools.parameters.BaseParamAspect; + +/** + * Provides the Secrets parameter aspect. This aspect is responsible for injecting + * parameters from AWS Secrets Manager into fields annotated with @SecretsParam. See the + * README and Powertools for Lambda (Java) documentation for information on using this feature. + */ +@Aspect +public class SecretsParamAspect extends BaseParamAspect { + + private static Supplier providerBuilder = () -> SecretsProvider.builder() + .build(); + + @Pointcut("get(* *) && @annotation(secretsParam)") + public void getParam(SecretsParam secretsParam) { + } + + @Around("getParam(secretsParam)") + public Object injectParam(final ProceedingJoinPoint joinPoint, final SecretsParam secretsParam) { + + SecretsProvider provider = providerBuilder.get(); + return getAndTransform(secretsParam.key(), secretsParam.transformer(), provider, + (FieldSignature) joinPoint.getSignature()); + } + +} diff --git a/powertools-parameters/powertools-parameters-secrets/src/main/java/software/amazon/lambda/powertools/parameters/secrets/SecretsProvider.java b/powertools-parameters/powertools-parameters-secrets/src/main/java/software/amazon/lambda/powertools/parameters/secrets/SecretsProvider.java new file mode 100644 index 000000000..9087c1ad6 --- /dev/null +++ b/powertools-parameters/powertools-parameters-secrets/src/main/java/software/amazon/lambda/powertools/parameters/secrets/SecretsProvider.java @@ -0,0 +1,115 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.secrets; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.util.Base64; +import java.util.Map; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; +import software.amazon.lambda.powertools.parameters.BaseProvider; +import software.amazon.lambda.powertools.parameters.cache.CacheManager; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; + +/** + * AWS Secrets Manager Parameter Provider

+ * + * Samples: + *
+ *     SecretsProvider provider = SecretsProvider.builder().build();
+ *
+ *     String value = provider.get("key");
+ *     System.out.println(value);
+ *     >>> "value"
+ *
+ *     // Get a value and cache it for 30 seconds (all others values will now be cached for 30 seconds)
+ *     String value = provider.defaultMaxAge(30, ChronoUnit.SECONDS).get("key");
+ *
+ *     // Get a value and cache it for 1 minute (all others values are cached for 5 seconds by default)
+ *     String value = provider.withMaxAge(1, ChronoUnit.MINUTES).get("key");
+ *
+ *     // Get a base64 encoded value, decoded into a String, and store it in the cache
+ *     String value = provider.withTransformation(Transformer.base64).get("key");
+ *
+ *     // Get a json value, transform it into an Object, and store it in the cache
+ *     TargetObject = provider.withTransformation(Transformer.json).get("key", TargetObject.class);
+ * 
+ */ +public class SecretsProvider extends BaseProvider { + + private final SecretsManagerClient client; + + /** + * Use the {@link SecretsProviderBuilder} to create an instance! + * + * @param client custom client you would like to use. + */ + SecretsProvider(CacheManager cacheManager, TransformationManager transformationManager, + SecretsManagerClient client) { + super(cacheManager, transformationManager); + this.client = client; + } + + /** + * Create a builder that can be used to configure and create a {@link SecretsProvider}. + * + * @return a new instance of {@link SecretsProviderBuilder} + */ + public static SecretsProviderBuilder builder() { + return new SecretsProviderBuilder(); + } + + /** + * Create a SecretsProvider with all default settings. + */ + public static SecretsProvider create() { + return new SecretsProviderBuilder().build(); + } + + /** + * Retrieve the parameter value from the AWS Secrets Manager. + * + * @param key key of the parameter + * @return the value of the parameter identified by the key + */ + @Override + protected String getValue(String key) { + GetSecretValueRequest request = GetSecretValueRequest.builder().secretId(key).build(); + + String secretValue = client.getSecretValue(request).secretString(); + if (secretValue == null) { + secretValue = + new String(Base64.getDecoder().decode(client.getSecretValue(request).secretBinary().asByteArray()), + UTF_8); + } + return secretValue; + } + + /** + * @throws UnsupportedOperationException as it is not possible to get multiple values simultaneously from Secrets Manager + */ + @Override + protected Map getMultipleValues(String path) { + throw new UnsupportedOperationException("Impossible to get multiple values from AWS Secrets Manager"); + } + + + // For test purpose only + SecretsManagerClient getClient() { + return client; + } + +} diff --git a/powertools-parameters/powertools-parameters-secrets/src/main/java/software/amazon/lambda/powertools/parameters/secrets/SecretsProviderBuilder.java b/powertools-parameters/powertools-parameters-secrets/src/main/java/software/amazon/lambda/powertools/parameters/secrets/SecretsProviderBuilder.java new file mode 100644 index 000000000..c5806689f --- /dev/null +++ b/powertools-parameters/powertools-parameters-secrets/src/main/java/software/amazon/lambda/powertools/parameters/secrets/SecretsProviderBuilder.java @@ -0,0 +1,103 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.secrets; + +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; +import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.lambda.powertools.common.internal.UserAgentConfigurator; +import software.amazon.lambda.powertools.parameters.BaseProvider; +import software.amazon.lambda.powertools.parameters.ParamProvider; +import software.amazon.lambda.powertools.parameters.cache.CacheManager; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; + +/** + * Implements a {@link ParamProvider} on top of the SecretsManager service. SecretsManager provides + */ +public class SecretsProviderBuilder { + + private SecretsManagerClient client; + private CacheManager cacheManager; + private TransformationManager transformationManager; + + private static SecretsManagerClient createClient() { + return SecretsManagerClient.builder() + .httpClientBuilder(UrlConnectionHttpClient.builder()) + .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) + .overrideConfiguration(ClientOverrideConfiguration.builder() + .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, + UserAgentConfigurator.getUserAgent(BaseProvider.PARAMETERS)).build()) + .build(); + } + + /** + * Create a {@link SecretsProvider} instance. + * + * @return a {@link SecretsProvider} + */ + public SecretsProvider build() { + if (cacheManager == null) { + // TODO - what should we do with this + cacheManager = new CacheManager(); + } + SecretsProvider provider; + if (client == null) { + client = createClient(); + } + if(transformationManager == null){ + transformationManager = new TransformationManager(); + } + provider = new SecretsProvider(cacheManager, transformationManager, client); + + return provider; + } + + /** + * Set custom {@link SecretsManagerClient} to pass to the {@link SecretsProvider}.
+ * Use it if you want to customize the region or any other part of the client. + * + * @param client Custom client + * @return the builder to chain calls (eg.
builder.withClient().build()
) + */ + public SecretsProviderBuilder withClient(SecretsManagerClient client) { + this.client = client; + return this; + } + + /** + * Provide a CacheManager to the {@link SecretsProvider} + * + * @param cacheManager the manager that will handle the cache of parameters + * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) + */ + public SecretsProviderBuilder withCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; + return this; + } + + /** + * Provide a transformationManager to the {@link SecretsProvider} + * + * @param transformationManager the manager that will handle transformation of parameters + * @return the builder to chain calls (eg.
builder.withTransformationManager().build()
) + */ + public SecretsProviderBuilder withTransformationManager(TransformationManager transformationManager) { + this.transformationManager = transformationManager; + return this; + } +} diff --git a/powertools-parameters/powertools-parameters-secrets/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-secrets/jni-config.json b/powertools-parameters/powertools-parameters-secrets/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-secrets/jni-config.json new file mode 100644 index 000000000..2689203aa --- /dev/null +++ b/powertools-parameters/powertools-parameters-secrets/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-secrets/jni-config.json @@ -0,0 +1,22 @@ +[ +{ + "name":"java.lang.Boolean", + "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.String", + "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"sun.instrument.InstrumentationImpl", + "methods":[{"name":"","parameterTypes":["long","boolean","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] +}, +{ + "name":"sun.management.VMManagementImpl", + "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] +} +] diff --git a/powertools-parameters/powertools-parameters-secrets/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-secrets/reflect-config.json b/powertools-parameters/powertools-parameters-secrets/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-secrets/reflect-config.json new file mode 100644 index 000000000..7dfaf1b03 --- /dev/null +++ b/powertools-parameters/powertools-parameters-secrets/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-secrets/reflect-config.json @@ -0,0 +1,323 @@ +[ +{ + "name":"com.sun.crypto.provider.AESCipher$General", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ARCFOURCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESedeCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.tools.attach.VirtualMachine" +}, +{ + "name":"java.io.Serializable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.AutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.Class", + "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] +}, +{ + "name":"java.lang.ClassLoader", + "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] +}, +{ + "name":"java.lang.Module", + "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] +}, +{ + "name":"java.lang.Object", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] +}, +{ + "name":"java.lang.ProcessHandle", + "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime", + "methods":[{"name":"version","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime$Version", + "methods":[{"name":"feature","parameterTypes":[] }] +}, +{ + "name":"java.lang.StackWalker" +}, +{ + "name":"java.lang.String" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.annotation.Retention", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.annotation.Target", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.invoke.MethodHandle", + "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles", + "methods":[{"name":"lookup","parameterTypes":[] }] +}, +{ + "name":"java.lang.invoke.MethodHandles$Lookup", + "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] +}, +{ + "name":"java.lang.invoke.MethodType", + "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] +}, +{ + "name":"java.lang.reflect.AccessibleObject", + "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] +}, +{ + "name":"java.lang.reflect.AnnotatedArrayType", + "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedParameterizedType", + "methods":[{"name":"getAnnotatedActualTypeArguments","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedType", + "methods":[{"name":"getType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedTypeVariable", + "methods":[{"name":"getAnnotatedBounds","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedWildcardType", + "methods":[{"name":"getAnnotatedUpperBounds","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Executable", + "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Parameter", + "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.TypeVariable", + "methods":[{"name":"getAnnotatedBounds","parameterTypes":[] }] +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.security.AlgorithmParametersSpi" +}, +{ + "name":"java.security.KeyStoreSpi" +}, +{ + "name":"java.security.SecureRandomParameters" +}, +{ + "name":"java.util.concurrent.ForkJoinTask", + "fields":[{"name":"aux"}, {"name":"status"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"javax.security.auth.x500.X500Principal", + "fields":[{"name":"thisX500Name"}], + "methods":[{"name":"","parameterTypes":["sun.security.x509.X500Name"] }] +}, +{ + "name":"jdk.internal.misc.Unsafe" +}, +{ + "name":"kotlin.jvm.JvmInline" +}, +{ + "name":"org.apiguardian.api.API", + "queryAllPublicMethods":true +}, +{ + "name":"scala.util.Properties" +}, +{ + "name":"software.amazon.awssdk.awscore.AwsClient", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"software.amazon.awssdk.core.SdkClient", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"serviceName","parameterTypes":[] }] +}, +{ + "name":"software.amazon.awssdk.services.secretsmanager.SecretsManagerClient", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"batchGetSecretValue","parameterTypes":["java.util.function.Consumer"] }, {"name":"batchGetSecretValue","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.BatchGetSecretValueRequest"] }, {"name":"batchGetSecretValuePaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"batchGetSecretValuePaginator","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.BatchGetSecretValueRequest"] }, {"name":"cancelRotateSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"cancelRotateSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.CancelRotateSecretRequest"] }, {"name":"createSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"createSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest"] }, {"name":"deleteResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteResourcePolicy","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.DeleteResourcePolicyRequest"] }, {"name":"deleteSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.DeleteSecretRequest"] }, {"name":"describeSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.DescribeSecretRequest"] }, {"name":"getRandomPassword","parameterTypes":[] }, {"name":"getRandomPassword","parameterTypes":["java.util.function.Consumer"] }, {"name":"getRandomPassword","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.GetRandomPasswordRequest"] }, {"name":"getResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"getResourcePolicy","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.GetResourcePolicyRequest"] }, {"name":"getSecretValue","parameterTypes":["java.util.function.Consumer"] }, {"name":"getSecretValue","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest"] }, {"name":"listSecretVersionIds","parameterTypes":["java.util.function.Consumer"] }, {"name":"listSecretVersionIds","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.ListSecretVersionIdsRequest"] }, {"name":"listSecretVersionIdsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listSecretVersionIdsPaginator","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.ListSecretVersionIdsRequest"] }, {"name":"listSecrets","parameterTypes":[] }, {"name":"listSecrets","parameterTypes":["java.util.function.Consumer"] }, {"name":"listSecrets","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.ListSecretsRequest"] }, {"name":"listSecretsPaginator","parameterTypes":[] }, {"name":"listSecretsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listSecretsPaginator","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.ListSecretsRequest"] }, {"name":"putResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"putResourcePolicy","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.PutResourcePolicyRequest"] }, {"name":"putSecretValue","parameterTypes":["java.util.function.Consumer"] }, {"name":"putSecretValue","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.PutSecretValueRequest"] }, {"name":"removeRegionsFromReplication","parameterTypes":["java.util.function.Consumer"] }, {"name":"removeRegionsFromReplication","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.RemoveRegionsFromReplicationRequest"] }, {"name":"replicateSecretToRegions","parameterTypes":["java.util.function.Consumer"] }, {"name":"replicateSecretToRegions","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.ReplicateSecretToRegionsRequest"] }, {"name":"restoreSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"restoreSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.RestoreSecretRequest"] }, {"name":"rotateSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"rotateSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.RotateSecretRequest"] }, {"name":"serviceClientConfiguration","parameterTypes":[] }, {"name":"stopReplicationToReplica","parameterTypes":["java.util.function.Consumer"] }, {"name":"stopReplicationToReplica","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.StopReplicationToReplicaRequest"] }, {"name":"tagResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"tagResource","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.TagResourceRequest"] }, {"name":"untagResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"untagResource","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.UntagResourceRequest"] }, {"name":"updateSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.UpdateSecretRequest"] }, {"name":"updateSecretVersionStage","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateSecretVersionStage","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.UpdateSecretVersionStageRequest"] }, {"name":"validateResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"validateResourcePolicy","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.ValidateResourcePolicyRequest"] }] +}, +{ + "name":"software.amazon.awssdk.utils.SdkAutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"close","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.BaseProvider", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"get","parameterTypes":["java.lang.String"] }, {"name":"get","parameterTypes":["java.lang.String","java.lang.Class"] }, {"name":"getMultiple","parameterTypes":["java.lang.String"] }, {"name":"now","parameterTypes":[] }, {"name":"resetToDefaults","parameterTypes":[] }, {"name":"withMaxAge","parameterTypes":["int","java.time.temporal.ChronoUnit"] }, {"name":"withTransformation","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.ParamProvider", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"software.amazon.lambda.powertools.parameters.secrets.SecretsParamAspect", + "fields":[{"name":"providerBuilder"}] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.secrets.SecretsProvider", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getMultipleValues","parameterTypes":["java.lang.String"] }, {"name":"getValue","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.transform.TransformationManager", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"performBasicTransformation","parameterTypes":["java.lang.String"] }, {"name":"performComplexTransformation","parameterTypes":["java.lang.String","java.lang.Class"] }, {"name":"setTransformer","parameterTypes":["java.lang.Class"] }, {"name":"shouldTransform","parameterTypes":[] }] +}, +{ + "name":"sun.reflect.ReflectionFactory", + "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.security.SecureRandomParameters"] }] +}, +{ + "name":"sun.security.provider.SHA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.X509Factory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSAKeyFactory$Legacy", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.SSLContextImpl$TLSContext", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.x509.AuthorityInfoAccessExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.AuthorityKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.BasicConstraintsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CRLDistributionPointsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CertificatePoliciesExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.ExtendedKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.IssuerAlternativeNameExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.KeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.NetscapeCertTypeExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.PrivateKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.SubjectAlternativeNameExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.SubjectKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +} +] diff --git a/powertools-parameters/powertools-parameters-secrets/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-secrets/resource-config.json b/powertools-parameters/powertools-parameters-secrets/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-secrets/resource-config.json new file mode 100644 index 000000000..f914dbf44 --- /dev/null +++ b/powertools-parameters/powertools-parameters-secrets/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-secrets/resource-config.json @@ -0,0 +1,23 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.net.spi.URLStreamHandlerProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" + }, { + "pattern":"\\Qsoftware/amazon/awssdk/global/handlers/execution.interceptors\\E" + }, { + "pattern":"\\Qsoftware/amazon/awssdk/services/secretsmanager/execution.interceptors\\E" + }, { + "pattern":"\\Qversion.properties\\E" + }, { + "pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt72b/nfc.nrm\\E" + }]}, + "bundles":[] +} diff --git a/powertools-parameters/powertools-parameters-secrets/src/test/java/software/amazon/lambda/powertools/parameters/secrets/SecretsParamAspectTest.java b/powertools-parameters/powertools-parameters-secrets/src/test/java/software/amazon/lambda/powertools/parameters/secrets/SecretsParamAspectTest.java new file mode 100644 index 000000000..7aa0f0872 --- /dev/null +++ b/powertools-parameters/powertools-parameters-secrets/src/test/java/software/amazon/lambda/powertools/parameters/secrets/SecretsParamAspectTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.secrets; + +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class SecretsParamAspectTest { + + @Test + public void parameterInjectedByProvider() throws Exception { + // Setup our aspect to return a mocked SecretsProvider + String key = "myKey"; + String value = "mySecretValue"; + SecretsProvider provider = Mockito.mock(SecretsProvider.class); + + Supplier providerBuilder = () -> provider; + writeStaticField(SecretsParamAspect.class, "providerBuilder", providerBuilder, true); + + // Setup our mocked SecretsProvider to return a value for our test data + Mockito.when(provider.get(key)).thenReturn(value); + + // Create an instance of a class and let the SecretsParamAspect inject it + MyInjectedClass obj = new MyInjectedClass(); + assertThat(obj.mySecret).isEqualTo(value); + } + + class MyInjectedClass { + @SecretsParam(key = "myKey") + public String mySecret; + } + +} diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SecretsProviderTest.java b/powertools-parameters/powertools-parameters-secrets/src/test/java/software/amazon/lambda/powertools/parameters/secrets/SecretsProviderTest.java similarity index 75% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SecretsProviderTest.java rename to powertools-parameters/powertools-parameters-secrets/src/test/java/software/amazon/lambda/powertools/parameters/secrets/SecretsProviderTest.java index f4f2d9bee..a2607dd2c 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SecretsProviderTest.java +++ b/powertools-parameters/powertools-parameters-secrets/src/test/java/software/amazon/lambda/powertools/parameters/secrets/SecretsProviderTest.java @@ -12,21 +12,24 @@ * */ -package software.amazon.lambda.powertools.parameters; +package software.amazon.lambda.powertools.parameters.secrets; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatRuntimeException; -import static org.mockito.MockitoAnnotations.openMocks; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static software.amazon.lambda.powertools.parameters.transform.Transformer.json; import java.time.temporal.ChronoUnit; import java.util.Base64; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; @@ -51,9 +54,9 @@ public class SecretsProviderTest { @BeforeEach public void init() { - openMocks(this); + MockitoAnnotations.openMocks(this); cacheManager = new CacheManager(); - provider = new SecretsProvider(cacheManager, client); + provider = new SecretsProvider(cacheManager, transformationManager, client); } @Test @@ -62,7 +65,6 @@ public void getValue() { String expectedValue = "Value1"; GetSecretValueResponse response = GetSecretValueResponse.builder().secretString(expectedValue).build(); Mockito.when(client.getSecretValue(paramCaptor.capture())).thenReturn(response); - provider.defaultMaxAge(1, ChronoUnit.DAYS); provider.withMaxAge(2, ChronoUnit.DAYS); String value = provider.getValue(key); @@ -96,13 +98,24 @@ public void getMultipleValuesThrowsException() { } @Test - public void testSecretsProviderBuilderMissingCacheManager_throwsException() { + public void testGetSecretsProvider_withoutParameter_shouldCreateDefaultClient() { - // Act & Assert - assertThatIllegalStateException().isThrownBy(() -> SecretsProvider.builder() - .withClient(client) - .withTransformationManager(transformationManager) - .build()) - .withMessage("No CacheManager provided, please provide one"); + // Act + SecretsProvider secretsProvider = SecretsProvider.builder() + .build(); + + // Assert + assertNotNull(secretsProvider); + assertNotNull(secretsProvider.getClient()); + } + + @Test + public void testGetSecretsProvider_withoutParameter_shouldHaveDefaultTransformationManager() { + + // Act + SecretsProvider secretsProvider = SecretsProvider.builder() + .build(); + // Assert + assertDoesNotThrow(()->secretsProvider.withTransformation(json)); } } diff --git a/powertools-parameters/powertools-parameters-ssm/pom.xml b/powertools-parameters/powertools-parameters-ssm/pom.xml new file mode 100644 index 000000000..1c91824e9 --- /dev/null +++ b/powertools-parameters/powertools-parameters-ssm/pom.xml @@ -0,0 +1,190 @@ + + + 4.0.0 + + + software.amazon.lambda + powertools-parent + 2.0.0-RC1 + ../../pom.xml + + + powertools-parameters-ssm + Powertools for AWS Lambda (Java) library Parameters - SSM + SSM Parameter Store implementation for the Parameters module + + + + software.amazon.lambda + powertools-parameters + ${project.version} + + + + software.amazon.awssdk + ssm + + + software.amazon.awssdk + apache-client + + + software.amazon.awssdk + netty-nio-client + + + + + org.aspectj + aspectjrt + provided + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + + + org.apache.commons + commons-lang3 + test + + + org.assertj + assertj-core + test + + + org.aspectj + aspectjweaver + test + + + + + + + maven-surefire-plugin + 3.1.2 + + + eu-central-1 + + + + + + + + + generate-graalvm-files + + + org.mockito + mockito-subclass + 5.6.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-ssm,experimental-class-define-support + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + 5.6.0 + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.2 + true + + + test-native + + test + + test + + + + powertools-parameters-ssm + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + --native-image-info + -H:+UnlockExperimentalVMOptions + -H:Log=registerResource:5 + + + + + + + + \ No newline at end of file diff --git a/powertools-parameters/powertools-parameters-ssm/src/main/java/software/amazon/lambda/powertools/parameters/ssm/SSMParam.java b/powertools-parameters/powertools-parameters-ssm/src/main/java/software/amazon/lambda/powertools/parameters/ssm/SSMParam.java new file mode 100644 index 000000000..9b1587bb4 --- /dev/null +++ b/powertools-parameters/powertools-parameters-ssm/src/main/java/software/amazon/lambda/powertools/parameters/ssm/SSMParam.java @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.ssm; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import software.amazon.lambda.powertools.parameters.transform.Transformer; + +/** + * Inject a parameter from the SSM Parameter Store into a field. You can also use + * {@code SSMProviderBuilder} to obtain SSM values directly, rather than injecting them implicitly. + * + * Usage: + *
+ * @SSMParam(key = "/my/parameter")
+ * String myParameter;
+ * 
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SSMParam { + /** + * Mandatory. Key from the SSM parameter store + * @return + */ + String key(); + + + /** + * Optional. a transfer to apply to the value + */ + Class transformer() default Transformer.class; +} diff --git a/powertools-parameters/powertools-parameters-ssm/src/main/java/software/amazon/lambda/powertools/parameters/ssm/SSMParamAspect.java b/powertools-parameters/powertools-parameters-ssm/src/main/java/software/amazon/lambda/powertools/parameters/ssm/SSMParamAspect.java new file mode 100644 index 000000000..b4d370506 --- /dev/null +++ b/powertools-parameters/powertools-parameters-ssm/src/main/java/software/amazon/lambda/powertools/parameters/ssm/SSMParamAspect.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.ssm; + +import java.util.function.Supplier; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.FieldSignature; +import software.amazon.lambda.powertools.parameters.BaseParamAspect; + +/** + * Provides the SSM parameter store parameter aspect. This aspect is responsible for injecting + * parameters from SSM Parameter Store into fields annotated with @SSMParam. See the + * README and Powertools for Lambda (Java) documentation for information on using this feature. + */ +@Aspect +public class SSMParamAspect extends BaseParamAspect { + + // This supplier produces a new SSMProvider each time it is called + private static Supplier providerBuilder = () -> SSMProvider.builder() + .build(); + + @Pointcut("get(* *) && @annotation(secretsParam)") + public void getParam(SSMParam secretsParam) { + } + + @Around("getParam(ssmPaam)") + public Object injectParam(final ProceedingJoinPoint joinPoint, final SSMParam ssmPaam) { + + SSMProvider provider = providerBuilder.get(); + return getAndTransform(ssmPaam.key(), ssmPaam.transformer(), provider, + (FieldSignature) joinPoint.getSignature()); + } + +} diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SSMProvider.java b/powertools-parameters/powertools-parameters-ssm/src/main/java/software/amazon/lambda/powertools/parameters/ssm/SSMProvider.java similarity index 60% rename from powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SSMProvider.java rename to powertools-parameters/powertools-parameters-ssm/src/main/java/software/amazon/lambda/powertools/parameters/ssm/SSMProvider.java index 549cdfbab..c24b08b84 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SSMProvider.java +++ b/powertools-parameters/powertools-parameters-ssm/src/main/java/software/amazon/lambda/powertools/parameters/ssm/SSMProvider.java @@ -12,32 +12,25 @@ * */ -package software.amazon.lambda.powertools.parameters; +package software.amazon.lambda.powertools.parameters.ssm; -import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; -import software.amazon.awssdk.core.SdkSystemSetting; -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; -import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; -import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; -import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.ssm.SsmClient; import software.amazon.awssdk.services.ssm.model.GetParameterRequest; import software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest; import software.amazon.awssdk.services.ssm.model.GetParametersByPathResponse; import software.amazon.awssdk.utils.StringUtils; -import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator; +import software.amazon.lambda.powertools.parameters.BaseProvider; import software.amazon.lambda.powertools.parameters.cache.CacheManager; import software.amazon.lambda.powertools.parameters.transform.TransformationManager; -import software.amazon.lambda.powertools.parameters.transform.Transformer; /** * AWS System Manager Parameter Store Provider

* * Samples: *
- *     SSMProvider provider = ParamManager.getSsmProvider();
+ *     SSMProvider provider = SSMProvider.builder().build();
  *
  *     String value = provider.get("key");
  *     System.out.println(value);
@@ -80,33 +73,30 @@ public class SSMProvider extends BaseProvider {
      * Constructor with custom {@link SsmClient}. 
* Use when you need to customize region or any other attribute of the client.

*

- * Use the {@link SSMProvider.Builder} to create an instance of it. + * Use the {@link SSMProviderBuilder} to create an instance of it. * - * @param client custom client you would like to use. + * @param client custom client you would like to use. + * @param transformationManager Null, or a transformation manager */ - SSMProvider(CacheManager cacheManager, SsmClient client) { - super(cacheManager); + SSMProvider(CacheManager cacheManager, TransformationManager transformationManager, SsmClient client) { + super(cacheManager, transformationManager); this.client = client; } /** - * Constructor with only a CacheManager
- *

- * Used in {@link ParamManager#createProvider(Class)} + * Create a builder that can be used to configure and create a {@link SSMProvider}. * - * @param cacheManager handles the parameter caching + * @return a new instance of {@link SSMProviderBuilder} */ - SSMProvider(CacheManager cacheManager) { - this(cacheManager, Builder.createClient()); + public static SSMProviderBuilder builder() { + return new SSMProviderBuilder(); } /** - * Create a builder that can be used to configure and create a {@link SSMProvider}. - * - * @return a new instance of {@link SSMProvider.Builder} + * Create a SSMProvider with all default settings. */ - public static SSMProvider.Builder builder() { - return new SSMProvider.Builder(); + public static SSMProvider create() { + return new SSMProviderBuilder().build(); } /** @@ -124,33 +114,6 @@ public String getValue(String key) { return client.getParameter(request).parameter().value(); } - /** - * {@inheritDoc} - */ - @Override - public SSMProvider defaultMaxAge(int maxAge, ChronoUnit unit) { - super.defaultMaxAge(maxAge, unit); - return this; - } - - /** - * {@inheritDoc} - */ - @Override - public SSMProvider withMaxAge(int maxAge, ChronoUnit unit) { - super.withMaxAge(maxAge, unit); - return this; - } - - /** - * {@inheritDoc} - */ - @Override - public SSMProvider withTransformation(Class transformerClass) { - super.withTransformation(transformerClass); - return this; - } - /** * Tells System Manager Parameter Store to decrypt the parameter value.
* By default, parameter values are not decrypted.
@@ -210,7 +173,7 @@ private Map getMultipleBis(String path, String nextToken) { res.parameters().forEach(parameter -> { /* Standardize the parameter name - The parameter name returned by SSM will contained the full path. + The parameter name returned by SSM will contain the full path. However, for readability, we should return only the part after the path. */ @@ -242,75 +205,4 @@ SsmClient getClient() { return client; } - static class Builder { - private SsmClient client; - private CacheManager cacheManager; - private TransformationManager transformationManager; - - private static SsmClient createClient() { - return SsmClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) - .overrideConfiguration(ClientOverrideConfiguration.builder() - .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, - UserAgentConfigurator.getUserAgent(PARAMETERS)).build()) - .build(); - } - - /** - * Create a {@link SSMProvider} instance. - * - * @return a {@link SSMProvider} - */ - public SSMProvider build() { - if (cacheManager == null) { - throw new IllegalStateException("No CacheManager provided, please provide one"); - } - SSMProvider provider; - if (client == null) { - client = createClient(); - } - - provider = new SSMProvider(cacheManager, client); - - if (transformationManager != null) { - provider.setTransformationManager(transformationManager); - } - return provider; - } - - /** - * Set custom {@link SsmClient} to pass to the {@link SSMProvider}.
- * Use it if you want to customize the region or any other part of the client. - * - * @param client Custom client - * @return the builder to chain calls (eg.

builder.withClient().build()
) - */ - public SSMProvider.Builder withClient(SsmClient client) { - this.client = client; - return this; - } - - /** - * Mandatory. Provide a CacheManager to the {@link SSMProvider} - * - * @param cacheManager the manager that will handle the cache of parameters - * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) - */ - public SSMProvider.Builder withCacheManager(CacheManager cacheManager) { - this.cacheManager = cacheManager; - return this; - } - - /** - * Provide a transformationManager to the {@link SSMProvider} - * - * @param transformationManager the manager that will handle transformation of parameters - * @return the builder to chain calls (eg.
builder.withTransformationManager().build()
) - */ - public SSMProvider.Builder withTransformationManager(TransformationManager transformationManager) { - this.transformationManager = transformationManager; - return this; - } - } } diff --git a/powertools-parameters/powertools-parameters-ssm/src/main/java/software/amazon/lambda/powertools/parameters/ssm/SSMProviderBuilder.java b/powertools-parameters/powertools-parameters-ssm/src/main/java/software/amazon/lambda/powertools/parameters/ssm/SSMProviderBuilder.java new file mode 100644 index 000000000..4c26463fb --- /dev/null +++ b/powertools-parameters/powertools-parameters-ssm/src/main/java/software/amazon/lambda/powertools/parameters/ssm/SSMProviderBuilder.java @@ -0,0 +1,101 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.ssm; + +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; +import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.ssm.SsmClient; +import software.amazon.lambda.powertools.common.internal.UserAgentConfigurator; +import software.amazon.lambda.powertools.parameters.BaseProvider; +import software.amazon.lambda.powertools.parameters.cache.CacheManager; +import software.amazon.lambda.powertools.parameters.transform.TransformationManager; + +/** + * Builder for the {@link SSMProvider} + */ +public class SSMProviderBuilder { + private SsmClient client; + private CacheManager cacheManager; + private TransformationManager transformationManager; + + private static SsmClient createClient() { + return SsmClient.builder() + .httpClientBuilder(UrlConnectionHttpClient.builder()) + .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) + .overrideConfiguration(ClientOverrideConfiguration.builder() + .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, + UserAgentConfigurator.getUserAgent(BaseProvider.PARAMETERS)).build()) + .build(); + } + + /** + * Create a {@link SSMProvider} instance. + * + * @return a {@link SSMProvider} + */ + public SSMProvider build() { + if (cacheManager == null) { + cacheManager = new CacheManager(); + } + SSMProvider provider; + if (client == null) { + client = createClient(); + } + + if(transformationManager == null){ + transformationManager = new TransformationManager(); + } + provider = new SSMProvider(cacheManager, transformationManager, client); + + return provider; + } + + /** + * Set custom {@link SsmClient} to pass to the {@link SSMProvider}.
+ * Use it if you want to customize the region or any other part of the client. + * + * @param client Custom client + * @return the builder to chain calls (eg.
builder.withClient().build()
) + */ + public SSMProviderBuilder withClient(SsmClient client) { + this.client = client; + return this; + } + + /** + * Provide a CacheManager to the {@link SSMProvider} + * + * @param cacheManager the manager that will handle the cache of parameters + * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) + */ + public SSMProviderBuilder withCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; + return this; + } + + /** + * Provide a transformationManager to the {@link SSMProvider} + * + * @param transformationManager the manager that will handle transformation of parameters + * @return the builder to chain calls (eg.
builder.withTransformationManager().build()
) + */ + public SSMProviderBuilder withTransformationManager(TransformationManager transformationManager) { + this.transformationManager = transformationManager; + return this; + } +} diff --git a/powertools-parameters/powertools-parameters-ssm/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-ssm/jni-config.json b/powertools-parameters/powertools-parameters-ssm/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-ssm/jni-config.json new file mode 100644 index 000000000..2c4de0562 --- /dev/null +++ b/powertools-parameters/powertools-parameters-ssm/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-ssm/jni-config.json @@ -0,0 +1,26 @@ +[ +{ + "name":"java.lang.Boolean", + "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.String", + "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"org.apache.maven.surefire.booter.ForkedBooter", + "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"sun.instrument.InstrumentationImpl", + "methods":[{"name":"","parameterTypes":["long","boolean","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] +}, +{ + "name":"sun.management.VMManagementImpl", + "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] +} +] diff --git a/powertools-parameters/powertools-parameters-ssm/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-ssm/reflect-config.json b/powertools-parameters/powertools-parameters-ssm/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-ssm/reflect-config.json new file mode 100644 index 000000000..5bc2deb4a --- /dev/null +++ b/powertools-parameters/powertools-parameters-ssm/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-ssm/reflect-config.json @@ -0,0 +1,341 @@ +[ +{ + "name":"com.sun.crypto.provider.AESCipher$General", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ARCFOURCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESedeCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.tools.attach.VirtualMachine" +}, +{ + "name":"java.io.Serializable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.AutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.Class", + "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] +}, +{ + "name":"java.lang.ClassLoader", + "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] +}, +{ + "name":"java.lang.Module", + "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] +}, +{ + "name":"java.lang.Object", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] +}, +{ + "name":"java.lang.ProcessHandle", + "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime", + "methods":[{"name":"version","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime$Version", + "methods":[{"name":"feature","parameterTypes":[] }] +}, +{ + "name":"java.lang.StackWalker" +}, +{ + "name":"java.lang.String" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.annotation.Retention", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.annotation.Target", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.invoke.MethodHandle", + "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles", + "methods":[{"name":"lookup","parameterTypes":[] }] +}, +{ + "name":"java.lang.invoke.MethodHandles$Lookup", + "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] +}, +{ + "name":"java.lang.invoke.MethodType", + "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] +}, +{ + "name":"java.lang.reflect.AccessibleObject", + "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] +}, +{ + "name":"java.lang.reflect.AnnotatedArrayType", + "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedParameterizedType", + "methods":[{"name":"getAnnotatedActualTypeArguments","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedType", + "methods":[{"name":"getType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedTypeVariable", + "methods":[{"name":"getAnnotatedBounds","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedWildcardType", + "methods":[{"name":"getAnnotatedUpperBounds","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Executable", + "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Parameter", + "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.TypeVariable", + "methods":[{"name":"getAnnotatedBounds","parameterTypes":[] }] +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.security.AlgorithmParametersSpi" +}, +{ + "name":"java.security.KeyStoreSpi" +}, +{ + "name":"java.security.SecureRandomParameters" +}, +{ + "name":"java.util.concurrent.ForkJoinTask", + "fields":[{"name":"aux"}, {"name":"status"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"javax.security.auth.x500.X500Principal", + "fields":[{"name":"thisX500Name"}], + "methods":[{"name":"","parameterTypes":["sun.security.x509.X500Name"] }] +}, +{ + "name":"jdk.internal.misc.Unsafe" +}, +{ + "name":"kotlin.jvm.JvmInline" +}, +{ + "name":"org.apiguardian.api.API", + "queryAllPublicMethods":true +}, +{ + "name":"scala.util.Properties" +}, +{ + "name":"software.amazon.awssdk.awscore.AwsClient", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"software.amazon.awssdk.core.SdkClient", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"serviceName","parameterTypes":[] }] +}, +{ + "name":"software.amazon.awssdk.services.ssm.SsmClient", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"addTagsToResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"addTagsToResource","parameterTypes":["software.amazon.awssdk.services.ssm.model.AddTagsToResourceRequest"] }, {"name":"associateOpsItemRelatedItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"associateOpsItemRelatedItem","parameterTypes":["software.amazon.awssdk.services.ssm.model.AssociateOpsItemRelatedItemRequest"] }, {"name":"cancelCommand","parameterTypes":["java.util.function.Consumer"] }, {"name":"cancelCommand","parameterTypes":["software.amazon.awssdk.services.ssm.model.CancelCommandRequest"] }, {"name":"cancelMaintenanceWindowExecution","parameterTypes":["java.util.function.Consumer"] }, {"name":"cancelMaintenanceWindowExecution","parameterTypes":["software.amazon.awssdk.services.ssm.model.CancelMaintenanceWindowExecutionRequest"] }, {"name":"createActivation","parameterTypes":["java.util.function.Consumer"] }, {"name":"createActivation","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateActivationRequest"] }, {"name":"createAssociation","parameterTypes":["java.util.function.Consumer"] }, {"name":"createAssociation","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateAssociationRequest"] }, {"name":"createAssociationBatch","parameterTypes":["java.util.function.Consumer"] }, {"name":"createAssociationBatch","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateAssociationBatchRequest"] }, {"name":"createDocument","parameterTypes":["java.util.function.Consumer"] }, {"name":"createDocument","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateDocumentRequest"] }, {"name":"createMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"createMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateMaintenanceWindowRequest"] }, {"name":"createOpsItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"createOpsItem","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateOpsItemRequest"] }, {"name":"createOpsMetadata","parameterTypes":["java.util.function.Consumer"] }, {"name":"createOpsMetadata","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateOpsMetadataRequest"] }, {"name":"createPatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"createPatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreatePatchBaselineRequest"] }, {"name":"createResourceDataSync","parameterTypes":["java.util.function.Consumer"] }, {"name":"createResourceDataSync","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateResourceDataSyncRequest"] }, {"name":"deleteActivation","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteActivation","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteActivationRequest"] }, {"name":"deleteAssociation","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteAssociation","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteAssociationRequest"] }, {"name":"deleteDocument","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteDocument","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteDocumentRequest"] }, {"name":"deleteInventory","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteInventory","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteInventoryRequest"] }, {"name":"deleteMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteMaintenanceWindowRequest"] }, {"name":"deleteOpsItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteOpsItem","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteOpsItemRequest"] }, {"name":"deleteOpsMetadata","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteOpsMetadata","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteOpsMetadataRequest"] }, {"name":"deleteParameter","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteParameter","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteParameterRequest"] }, {"name":"deleteParameters","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteParameters","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteParametersRequest"] }, {"name":"deletePatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"deletePatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeletePatchBaselineRequest"] }, {"name":"deleteResourceDataSync","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteResourceDataSync","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteResourceDataSyncRequest"] }, {"name":"deleteResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteResourcePolicy","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteResourcePolicyRequest"] }, {"name":"deregisterManagedInstance","parameterTypes":["java.util.function.Consumer"] }, {"name":"deregisterManagedInstance","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeregisterManagedInstanceRequest"] }, {"name":"deregisterPatchBaselineForPatchGroup","parameterTypes":["java.util.function.Consumer"] }, {"name":"deregisterPatchBaselineForPatchGroup","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeregisterPatchBaselineForPatchGroupRequest"] }, {"name":"deregisterTargetFromMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"deregisterTargetFromMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeregisterTargetFromMaintenanceWindowRequest"] }, {"name":"deregisterTaskFromMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"deregisterTaskFromMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeregisterTaskFromMaintenanceWindowRequest"] }, {"name":"describeActivations","parameterTypes":[] }, {"name":"describeActivations","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeActivations","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeActivationsRequest"] }, {"name":"describeActivationsPaginator","parameterTypes":[] }, {"name":"describeActivationsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeActivationsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeActivationsRequest"] }, {"name":"describeAssociation","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAssociation","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAssociationRequest"] }, {"name":"describeAssociationExecutionTargets","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAssociationExecutionTargets","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAssociationExecutionTargetsRequest"] }, {"name":"describeAssociationExecutionTargetsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAssociationExecutionTargetsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAssociationExecutionTargetsRequest"] }, {"name":"describeAssociationExecutions","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAssociationExecutions","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAssociationExecutionsRequest"] }, {"name":"describeAssociationExecutionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAssociationExecutionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAssociationExecutionsRequest"] }, {"name":"describeAutomationExecutions","parameterTypes":[] }, {"name":"describeAutomationExecutions","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAutomationExecutions","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAutomationExecutionsRequest"] }, {"name":"describeAutomationExecutionsPaginator","parameterTypes":[] }, {"name":"describeAutomationExecutionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAutomationExecutionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAutomationExecutionsRequest"] }, {"name":"describeAutomationStepExecutions","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAutomationStepExecutions","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAutomationStepExecutionsRequest"] }, {"name":"describeAutomationStepExecutionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAutomationStepExecutionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAutomationStepExecutionsRequest"] }, {"name":"describeAvailablePatches","parameterTypes":[] }, {"name":"describeAvailablePatches","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAvailablePatches","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAvailablePatchesRequest"] }, {"name":"describeAvailablePatchesPaginator","parameterTypes":[] }, {"name":"describeAvailablePatchesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAvailablePatchesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAvailablePatchesRequest"] }, {"name":"describeDocument","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeDocument","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeDocumentRequest"] }, {"name":"describeDocumentPermission","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeDocumentPermission","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeDocumentPermissionRequest"] }, {"name":"describeEffectiveInstanceAssociations","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeEffectiveInstanceAssociations","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeEffectiveInstanceAssociationsRequest"] }, {"name":"describeEffectiveInstanceAssociationsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeEffectiveInstanceAssociationsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeEffectiveInstanceAssociationsRequest"] }, {"name":"describeEffectivePatchesForPatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeEffectivePatchesForPatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeEffectivePatchesForPatchBaselineRequest"] }, {"name":"describeEffectivePatchesForPatchBaselinePaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeEffectivePatchesForPatchBaselinePaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeEffectivePatchesForPatchBaselineRequest"] }, {"name":"describeInstanceAssociationsStatus","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstanceAssociationsStatus","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstanceAssociationsStatusRequest"] }, {"name":"describeInstanceAssociationsStatusPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstanceAssociationsStatusPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstanceAssociationsStatusRequest"] }, {"name":"describeInstanceInformation","parameterTypes":[] }, {"name":"describeInstanceInformation","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstanceInformation","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstanceInformationRequest"] }, {"name":"describeInstanceInformationPaginator","parameterTypes":[] }, {"name":"describeInstanceInformationPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstanceInformationPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstanceInformationRequest"] }, {"name":"describeInstancePatchStates","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePatchStates","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePatchStatesRequest"] }, {"name":"describeInstancePatchStatesForPatchGroup","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePatchStatesForPatchGroup","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePatchStatesForPatchGroupRequest"] }, {"name":"describeInstancePatchStatesForPatchGroupPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePatchStatesForPatchGroupPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePatchStatesForPatchGroupRequest"] }, {"name":"describeInstancePatchStatesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePatchStatesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePatchStatesRequest"] }, {"name":"describeInstancePatches","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePatches","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePatchesRequest"] }, {"name":"describeInstancePatchesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePatchesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePatchesRequest"] }, {"name":"describeInstanceProperties","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstanceProperties","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePropertiesRequest"] }, {"name":"describeInstancePropertiesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePropertiesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePropertiesRequest"] }, {"name":"describeInventoryDeletions","parameterTypes":[] }, {"name":"describeInventoryDeletions","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInventoryDeletions","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInventoryDeletionsRequest"] }, {"name":"describeInventoryDeletionsPaginator","parameterTypes":[] }, {"name":"describeInventoryDeletionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInventoryDeletionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInventoryDeletionsRequest"] }, {"name":"describeMaintenanceWindowExecutionTaskInvocations","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowExecutionTaskInvocations","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowExecutionTaskInvocationsRequest"] }, {"name":"describeMaintenanceWindowExecutionTaskInvocationsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowExecutionTaskInvocationsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowExecutionTaskInvocationsRequest"] }, {"name":"describeMaintenanceWindowExecutionTasks","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowExecutionTasks","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowExecutionTasksRequest"] }, {"name":"describeMaintenanceWindowExecutionTasksPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowExecutionTasksPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowExecutionTasksRequest"] }, {"name":"describeMaintenanceWindowExecutions","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowExecutions","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowExecutionsRequest"] }, {"name":"describeMaintenanceWindowExecutionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowExecutionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowExecutionsRequest"] }, {"name":"describeMaintenanceWindowSchedule","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowSchedule","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowScheduleRequest"] }, {"name":"describeMaintenanceWindowSchedulePaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowSchedulePaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowScheduleRequest"] }, {"name":"describeMaintenanceWindowTargets","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowTargets","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowTargetsRequest"] }, {"name":"describeMaintenanceWindowTargetsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowTargetsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowTargetsRequest"] }, {"name":"describeMaintenanceWindowTasks","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowTasks","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowTasksRequest"] }, {"name":"describeMaintenanceWindowTasksPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowTasksPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowTasksRequest"] }, {"name":"describeMaintenanceWindows","parameterTypes":[] }, {"name":"describeMaintenanceWindows","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindows","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowsRequest"] }, {"name":"describeMaintenanceWindowsForTarget","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowsForTarget","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowsForTargetRequest"] }, {"name":"describeMaintenanceWindowsForTargetPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowsForTargetPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowsForTargetRequest"] }, {"name":"describeMaintenanceWindowsPaginator","parameterTypes":[] }, {"name":"describeMaintenanceWindowsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowsRequest"] }, {"name":"describeOpsItems","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeOpsItems","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeOpsItemsRequest"] }, {"name":"describeOpsItemsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeOpsItemsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeOpsItemsRequest"] }, {"name":"describeParameters","parameterTypes":[] }, {"name":"describeParameters","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeParameters","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeParametersRequest"] }, {"name":"describeParametersPaginator","parameterTypes":[] }, {"name":"describeParametersPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeParametersPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeParametersRequest"] }, {"name":"describePatchBaselines","parameterTypes":[] }, {"name":"describePatchBaselines","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchBaselines","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchBaselinesRequest"] }, {"name":"describePatchBaselinesPaginator","parameterTypes":[] }, {"name":"describePatchBaselinesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchBaselinesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchBaselinesRequest"] }, {"name":"describePatchGroupState","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchGroupState","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchGroupStateRequest"] }, {"name":"describePatchGroups","parameterTypes":[] }, {"name":"describePatchGroups","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchGroups","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchGroupsRequest"] }, {"name":"describePatchGroupsPaginator","parameterTypes":[] }, {"name":"describePatchGroupsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchGroupsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchGroupsRequest"] }, {"name":"describePatchProperties","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchProperties","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchPropertiesRequest"] }, {"name":"describePatchPropertiesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchPropertiesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchPropertiesRequest"] }, {"name":"describeSessions","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeSessions","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeSessionsRequest"] }, {"name":"describeSessionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeSessionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeSessionsRequest"] }, {"name":"disassociateOpsItemRelatedItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"disassociateOpsItemRelatedItem","parameterTypes":["software.amazon.awssdk.services.ssm.model.DisassociateOpsItemRelatedItemRequest"] }, {"name":"getAutomationExecution","parameterTypes":["java.util.function.Consumer"] }, {"name":"getAutomationExecution","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetAutomationExecutionRequest"] }, {"name":"getCalendarState","parameterTypes":["java.util.function.Consumer"] }, {"name":"getCalendarState","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetCalendarStateRequest"] }, {"name":"getCommandInvocation","parameterTypes":["java.util.function.Consumer"] }, {"name":"getCommandInvocation","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetCommandInvocationRequest"] }, {"name":"getConnectionStatus","parameterTypes":["java.util.function.Consumer"] }, {"name":"getConnectionStatus","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetConnectionStatusRequest"] }, {"name":"getDefaultPatchBaseline","parameterTypes":[] }, {"name":"getDefaultPatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"getDefaultPatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetDefaultPatchBaselineRequest"] }, {"name":"getDeployablePatchSnapshotForInstance","parameterTypes":["java.util.function.Consumer"] }, {"name":"getDeployablePatchSnapshotForInstance","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetDeployablePatchSnapshotForInstanceRequest"] }, {"name":"getDocument","parameterTypes":["java.util.function.Consumer"] }, {"name":"getDocument","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetDocumentRequest"] }, {"name":"getInventory","parameterTypes":[] }, {"name":"getInventory","parameterTypes":["java.util.function.Consumer"] }, {"name":"getInventory","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetInventoryRequest"] }, {"name":"getInventoryPaginator","parameterTypes":[] }, {"name":"getInventoryPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"getInventoryPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetInventoryRequest"] }, {"name":"getInventorySchema","parameterTypes":[] }, {"name":"getInventorySchema","parameterTypes":["java.util.function.Consumer"] }, {"name":"getInventorySchema","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetInventorySchemaRequest"] }, {"name":"getInventorySchemaPaginator","parameterTypes":[] }, {"name":"getInventorySchemaPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"getInventorySchemaPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetInventorySchemaRequest"] }, {"name":"getMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"getMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetMaintenanceWindowRequest"] }, {"name":"getMaintenanceWindowExecution","parameterTypes":["java.util.function.Consumer"] }, {"name":"getMaintenanceWindowExecution","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetMaintenanceWindowExecutionRequest"] }, {"name":"getMaintenanceWindowExecutionTask","parameterTypes":["java.util.function.Consumer"] }, {"name":"getMaintenanceWindowExecutionTask","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetMaintenanceWindowExecutionTaskRequest"] }, {"name":"getMaintenanceWindowExecutionTaskInvocation","parameterTypes":["java.util.function.Consumer"] }, {"name":"getMaintenanceWindowExecutionTaskInvocation","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetMaintenanceWindowExecutionTaskInvocationRequest"] }, {"name":"getMaintenanceWindowTask","parameterTypes":["java.util.function.Consumer"] }, {"name":"getMaintenanceWindowTask","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetMaintenanceWindowTaskRequest"] }, {"name":"getOpsItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"getOpsItem","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetOpsItemRequest"] }, {"name":"getOpsMetadata","parameterTypes":["java.util.function.Consumer"] }, {"name":"getOpsMetadata","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetOpsMetadataRequest"] }, {"name":"getOpsSummary","parameterTypes":["java.util.function.Consumer"] }, {"name":"getOpsSummary","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetOpsSummaryRequest"] }, {"name":"getOpsSummaryPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"getOpsSummaryPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetOpsSummaryRequest"] }, {"name":"getParameter","parameterTypes":["java.util.function.Consumer"] }, {"name":"getParameter","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetParameterRequest"] }, {"name":"getParameterHistory","parameterTypes":["java.util.function.Consumer"] }, {"name":"getParameterHistory","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetParameterHistoryRequest"] }, {"name":"getParameterHistoryPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"getParameterHistoryPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetParameterHistoryRequest"] }, {"name":"getParameters","parameterTypes":["java.util.function.Consumer"] }, {"name":"getParameters","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetParametersRequest"] }, {"name":"getParametersByPath","parameterTypes":["java.util.function.Consumer"] }, {"name":"getParametersByPath","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest"] }, {"name":"getParametersByPathPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"getParametersByPathPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest"] }, {"name":"getPatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"getPatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetPatchBaselineRequest"] }, {"name":"getPatchBaselineForPatchGroup","parameterTypes":["java.util.function.Consumer"] }, {"name":"getPatchBaselineForPatchGroup","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetPatchBaselineForPatchGroupRequest"] }, {"name":"getResourcePolicies","parameterTypes":["java.util.function.Consumer"] }, {"name":"getResourcePolicies","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetResourcePoliciesRequest"] }, {"name":"getResourcePoliciesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"getResourcePoliciesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetResourcePoliciesRequest"] }, {"name":"getServiceSetting","parameterTypes":["java.util.function.Consumer"] }, {"name":"getServiceSetting","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetServiceSettingRequest"] }, {"name":"labelParameterVersion","parameterTypes":["java.util.function.Consumer"] }, {"name":"labelParameterVersion","parameterTypes":["software.amazon.awssdk.services.ssm.model.LabelParameterVersionRequest"] }, {"name":"listAssociationVersions","parameterTypes":["java.util.function.Consumer"] }, {"name":"listAssociationVersions","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListAssociationVersionsRequest"] }, {"name":"listAssociationVersionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listAssociationVersionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListAssociationVersionsRequest"] }, {"name":"listAssociations","parameterTypes":[] }, {"name":"listAssociations","parameterTypes":["java.util.function.Consumer"] }, {"name":"listAssociations","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListAssociationsRequest"] }, {"name":"listAssociationsPaginator","parameterTypes":[] }, {"name":"listAssociationsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listAssociationsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListAssociationsRequest"] }, {"name":"listCommandInvocations","parameterTypes":[] }, {"name":"listCommandInvocations","parameterTypes":["java.util.function.Consumer"] }, {"name":"listCommandInvocations","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListCommandInvocationsRequest"] }, {"name":"listCommandInvocationsPaginator","parameterTypes":[] }, {"name":"listCommandInvocationsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listCommandInvocationsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListCommandInvocationsRequest"] }, {"name":"listCommands","parameterTypes":[] }, {"name":"listCommands","parameterTypes":["java.util.function.Consumer"] }, {"name":"listCommands","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListCommandsRequest"] }, {"name":"listCommandsPaginator","parameterTypes":[] }, {"name":"listCommandsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listCommandsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListCommandsRequest"] }, {"name":"listComplianceItems","parameterTypes":["java.util.function.Consumer"] }, {"name":"listComplianceItems","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListComplianceItemsRequest"] }, {"name":"listComplianceItemsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listComplianceItemsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListComplianceItemsRequest"] }, {"name":"listComplianceSummaries","parameterTypes":[] }, {"name":"listComplianceSummaries","parameterTypes":["java.util.function.Consumer"] }, {"name":"listComplianceSummaries","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListComplianceSummariesRequest"] }, {"name":"listComplianceSummariesPaginator","parameterTypes":[] }, {"name":"listComplianceSummariesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listComplianceSummariesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListComplianceSummariesRequest"] }, {"name":"listDocumentMetadataHistory","parameterTypes":["java.util.function.Consumer"] }, {"name":"listDocumentMetadataHistory","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListDocumentMetadataHistoryRequest"] }, {"name":"listDocumentVersions","parameterTypes":["java.util.function.Consumer"] }, {"name":"listDocumentVersions","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListDocumentVersionsRequest"] }, {"name":"listDocumentVersionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listDocumentVersionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListDocumentVersionsRequest"] }, {"name":"listDocuments","parameterTypes":[] }, {"name":"listDocuments","parameterTypes":["java.util.function.Consumer"] }, {"name":"listDocuments","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListDocumentsRequest"] }, {"name":"listDocumentsPaginator","parameterTypes":[] }, {"name":"listDocumentsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listDocumentsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListDocumentsRequest"] }, {"name":"listInventoryEntries","parameterTypes":["java.util.function.Consumer"] }, {"name":"listInventoryEntries","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListInventoryEntriesRequest"] }, {"name":"listOpsItemEvents","parameterTypes":["java.util.function.Consumer"] }, {"name":"listOpsItemEvents","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListOpsItemEventsRequest"] }, {"name":"listOpsItemEventsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listOpsItemEventsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListOpsItemEventsRequest"] }, {"name":"listOpsItemRelatedItems","parameterTypes":["java.util.function.Consumer"] }, {"name":"listOpsItemRelatedItems","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListOpsItemRelatedItemsRequest"] }, {"name":"listOpsItemRelatedItemsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listOpsItemRelatedItemsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListOpsItemRelatedItemsRequest"] }, {"name":"listOpsMetadata","parameterTypes":["java.util.function.Consumer"] }, {"name":"listOpsMetadata","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListOpsMetadataRequest"] }, {"name":"listOpsMetadataPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listOpsMetadataPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListOpsMetadataRequest"] }, {"name":"listResourceComplianceSummaries","parameterTypes":[] }, {"name":"listResourceComplianceSummaries","parameterTypes":["java.util.function.Consumer"] }, {"name":"listResourceComplianceSummaries","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListResourceComplianceSummariesRequest"] }, {"name":"listResourceComplianceSummariesPaginator","parameterTypes":[] }, {"name":"listResourceComplianceSummariesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listResourceComplianceSummariesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListResourceComplianceSummariesRequest"] }, {"name":"listResourceDataSync","parameterTypes":[] }, {"name":"listResourceDataSync","parameterTypes":["java.util.function.Consumer"] }, {"name":"listResourceDataSync","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListResourceDataSyncRequest"] }, {"name":"listResourceDataSyncPaginator","parameterTypes":[] }, {"name":"listResourceDataSyncPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listResourceDataSyncPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListResourceDataSyncRequest"] }, {"name":"listTagsForResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"listTagsForResource","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListTagsForResourceRequest"] }, {"name":"modifyDocumentPermission","parameterTypes":["java.util.function.Consumer"] }, {"name":"modifyDocumentPermission","parameterTypes":["software.amazon.awssdk.services.ssm.model.ModifyDocumentPermissionRequest"] }, {"name":"putComplianceItems","parameterTypes":["java.util.function.Consumer"] }, {"name":"putComplianceItems","parameterTypes":["software.amazon.awssdk.services.ssm.model.PutComplianceItemsRequest"] }, {"name":"putInventory","parameterTypes":["java.util.function.Consumer"] }, {"name":"putInventory","parameterTypes":["software.amazon.awssdk.services.ssm.model.PutInventoryRequest"] }, {"name":"putParameter","parameterTypes":["java.util.function.Consumer"] }, {"name":"putParameter","parameterTypes":["software.amazon.awssdk.services.ssm.model.PutParameterRequest"] }, {"name":"putResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"putResourcePolicy","parameterTypes":["software.amazon.awssdk.services.ssm.model.PutResourcePolicyRequest"] }, {"name":"registerDefaultPatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"registerDefaultPatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.RegisterDefaultPatchBaselineRequest"] }, {"name":"registerPatchBaselineForPatchGroup","parameterTypes":["java.util.function.Consumer"] }, {"name":"registerPatchBaselineForPatchGroup","parameterTypes":["software.amazon.awssdk.services.ssm.model.RegisterPatchBaselineForPatchGroupRequest"] }, {"name":"registerTargetWithMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"registerTargetWithMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.RegisterTargetWithMaintenanceWindowRequest"] }, {"name":"registerTaskWithMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"registerTaskWithMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.RegisterTaskWithMaintenanceWindowRequest"] }, {"name":"removeTagsFromResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"removeTagsFromResource","parameterTypes":["software.amazon.awssdk.services.ssm.model.RemoveTagsFromResourceRequest"] }, {"name":"resetServiceSetting","parameterTypes":["java.util.function.Consumer"] }, {"name":"resetServiceSetting","parameterTypes":["software.amazon.awssdk.services.ssm.model.ResetServiceSettingRequest"] }, {"name":"resumeSession","parameterTypes":["java.util.function.Consumer"] }, {"name":"resumeSession","parameterTypes":["software.amazon.awssdk.services.ssm.model.ResumeSessionRequest"] }, {"name":"sendAutomationSignal","parameterTypes":["java.util.function.Consumer"] }, {"name":"sendAutomationSignal","parameterTypes":["software.amazon.awssdk.services.ssm.model.SendAutomationSignalRequest"] }, {"name":"sendCommand","parameterTypes":["java.util.function.Consumer"] }, {"name":"sendCommand","parameterTypes":["software.amazon.awssdk.services.ssm.model.SendCommandRequest"] }, {"name":"serviceClientConfiguration","parameterTypes":[] }, {"name":"startAssociationsOnce","parameterTypes":["java.util.function.Consumer"] }, {"name":"startAssociationsOnce","parameterTypes":["software.amazon.awssdk.services.ssm.model.StartAssociationsOnceRequest"] }, {"name":"startAutomationExecution","parameterTypes":["java.util.function.Consumer"] }, {"name":"startAutomationExecution","parameterTypes":["software.amazon.awssdk.services.ssm.model.StartAutomationExecutionRequest"] }, {"name":"startChangeRequestExecution","parameterTypes":["java.util.function.Consumer"] }, {"name":"startChangeRequestExecution","parameterTypes":["software.amazon.awssdk.services.ssm.model.StartChangeRequestExecutionRequest"] }, {"name":"startSession","parameterTypes":["java.util.function.Consumer"] }, {"name":"startSession","parameterTypes":["software.amazon.awssdk.services.ssm.model.StartSessionRequest"] }, {"name":"stopAutomationExecution","parameterTypes":["java.util.function.Consumer"] }, {"name":"stopAutomationExecution","parameterTypes":["software.amazon.awssdk.services.ssm.model.StopAutomationExecutionRequest"] }, {"name":"terminateSession","parameterTypes":["java.util.function.Consumer"] }, {"name":"terminateSession","parameterTypes":["software.amazon.awssdk.services.ssm.model.TerminateSessionRequest"] }, {"name":"unlabelParameterVersion","parameterTypes":["java.util.function.Consumer"] }, {"name":"unlabelParameterVersion","parameterTypes":["software.amazon.awssdk.services.ssm.model.UnlabelParameterVersionRequest"] }, {"name":"updateAssociation","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateAssociation","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateAssociationRequest"] }, {"name":"updateAssociationStatus","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateAssociationStatus","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateAssociationStatusRequest"] }, {"name":"updateDocument","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateDocument","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateDocumentRequest"] }, {"name":"updateDocumentDefaultVersion","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateDocumentDefaultVersion","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateDocumentDefaultVersionRequest"] }, {"name":"updateDocumentMetadata","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateDocumentMetadata","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateDocumentMetadataRequest"] }, {"name":"updateMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateMaintenanceWindowRequest"] }, {"name":"updateMaintenanceWindowTarget","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateMaintenanceWindowTarget","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateMaintenanceWindowTargetRequest"] }, {"name":"updateMaintenanceWindowTask","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateMaintenanceWindowTask","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateMaintenanceWindowTaskRequest"] }, {"name":"updateManagedInstanceRole","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateManagedInstanceRole","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateManagedInstanceRoleRequest"] }, {"name":"updateOpsItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateOpsItem","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateOpsItemRequest"] }, {"name":"updateOpsMetadata","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateOpsMetadata","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateOpsMetadataRequest"] }, {"name":"updatePatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"updatePatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdatePatchBaselineRequest"] }, {"name":"updateResourceDataSync","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateResourceDataSync","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateResourceDataSyncRequest"] }, {"name":"updateServiceSetting","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateServiceSetting","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateServiceSettingRequest"] }, {"name":"waiter","parameterTypes":[] }] +}, +{ + "name":"software.amazon.awssdk.utils.SdkAutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"close","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.BaseProvider", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"get","parameterTypes":["java.lang.String"] }, {"name":"get","parameterTypes":["java.lang.String","java.lang.Class"] }, {"name":"getMultiple","parameterTypes":["java.lang.String"] }, {"name":"now","parameterTypes":[] }, {"name":"withMaxAge","parameterTypes":["int","java.time.temporal.ChronoUnit"] }, {"name":"withTransformation","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.ParamProvider", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"software.amazon.lambda.powertools.parameters.ssm.SSMParamAspect", + "fields":[{"name":"providerBuilder"}] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.ssm.SSMProvider", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getMultipleValues","parameterTypes":["java.lang.String"] }, {"name":"getValue","parameterTypes":["java.lang.String"] }, {"name":"recursive","parameterTypes":[] }, {"name":"resetToDefaults","parameterTypes":[] }, {"name":"withDecryption","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.transform.TransformationManager", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"performBasicTransformation","parameterTypes":["java.lang.String"] }, {"name":"performComplexTransformation","parameterTypes":["java.lang.String","java.lang.Class"] }, {"name":"setTransformer","parameterTypes":["java.lang.Class"] }, {"name":"shouldTransform","parameterTypes":[] }] +}, +{ + "name": "software.amazon.lambda.powertools.parameters.transform.JsonTransformer", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [{"name": "applyTransformation","parameterTypes":["java.lang.String","java.lang.Class"]}] +}, +{ + "name": "software.amazon.lambda.powertools.parameters.transform.Base64Transformer", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [{"name": "applyTransformation","parameterTypes":["java.lang.String"]}] +}, +{ + "name": "software.amazon.lambda.powertools.parameters.transform.BasicTransformer", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods": [{"name": "applyTransformation","parameterTypes":["java.lang.String","java.lang.Class"]}] +}, +{ + "name":"sun.reflect.ReflectionFactory", + "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.security.SecureRandomParameters"] }] +}, +{ + "name":"sun.security.provider.SHA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.X509Factory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSAKeyFactory$Legacy", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.SSLContextImpl$TLSContext", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.x509.AuthorityInfoAccessExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.AuthorityKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.BasicConstraintsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CRLDistributionPointsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CertificatePoliciesExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.ExtendedKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.IssuerAlternativeNameExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.KeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.NetscapeCertTypeExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.PrivateKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.SubjectAlternativeNameExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.SubjectKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +} +] diff --git a/powertools-parameters/powertools-parameters-ssm/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-ssm/resource-config.json b/powertools-parameters/powertools-parameters-ssm/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-ssm/resource-config.json new file mode 100644 index 000000000..6d8f3660f --- /dev/null +++ b/powertools-parameters/powertools-parameters-ssm/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters-ssm/resource-config.json @@ -0,0 +1,23 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.net.spi.URLStreamHandlerProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" + }, { + "pattern":"\\Qsoftware/amazon/awssdk/global/handlers/execution.interceptors\\E" + }, { + "pattern":"\\Qsoftware/amazon/awssdk/services/ssm/execution.interceptors\\E" + }, { + "pattern":"\\Qversion.properties\\E" + }, { + "pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt72b/nfc.nrm\\E" + }]}, + "bundles":[] +} diff --git a/powertools-parameters/powertools-parameters-ssm/src/test/java/software/amazon/lambda/powertools/parameters/ssm/SSMParamAspectTest.java b/powertools-parameters/powertools-parameters-ssm/src/test/java/software/amazon/lambda/powertools/parameters/ssm/SSMParamAspectTest.java new file mode 100644 index 000000000..e56d20ffa --- /dev/null +++ b/powertools-parameters/powertools-parameters-ssm/src/test/java/software/amazon/lambda/powertools/parameters/ssm/SSMParamAspectTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters.ssm; + +import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class SSMParamAspectTest { + + // This class tests the SSM Param aspect in the same fashion + // as the tests for the aspects for the other providers. + + @Test + public void parameterInjectedByProvider() throws Exception { + + String key = "myKey"; + String value = "mySecretValue"; + SSMProvider provider = Mockito.mock(SSMProvider.class); + + Supplier providerBuilder = () -> provider; + writeStaticField(SSMParamAspect.class, "providerBuilder", providerBuilder, true); + + // Setup our mocked SSMProvider to return a value for our test data + Mockito.when(provider.get(key)).thenReturn(value); + + // Create an instance of a class and let the SSMParamAspect inject it + MyInjectedClass obj = new MyInjectedClass(); + assertThat(obj.mySecret).isEqualTo(value); + } + + class MyInjectedClass { + @SSMParam(key = "myKey") + public String mySecret; + } + +} diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java b/powertools-parameters/powertools-parameters-ssm/src/test/java/software/amazon/lambda/powertools/parameters/ssm/SSMProviderTest.java similarity index 84% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java rename to powertools-parameters/powertools-parameters-ssm/src/test/java/software/amazon/lambda/powertools/parameters/ssm/SSMProviderTest.java index 2a4f8f927..db45dc21c 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java +++ b/powertools-parameters/powertools-parameters-ssm/src/test/java/software/amazon/lambda/powertools/parameters/ssm/SSMProviderTest.java @@ -12,16 +12,12 @@ * */ -package software.amazon.lambda.powertools.parameters; +package software.amazon.lambda.powertools.parameters.ssm; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static software.amazon.lambda.powertools.parameters.transform.Transformer.json; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -31,8 +27,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; import software.amazon.awssdk.services.ssm.SsmClient; import software.amazon.awssdk.services.ssm.model.GetParameterRequest; import software.amazon.awssdk.services.ssm.model.GetParameterResponse; @@ -62,9 +61,9 @@ public class SSMProviderTest { @BeforeEach public void init() { - openMocks(this); + MockitoAnnotations.openMocks(this); cacheManager = new CacheManager(); - provider = new SSMProvider(cacheManager, client); + provider = new SSMProvider(cacheManager, null, client); } @Test @@ -100,7 +99,7 @@ public void getMultiple() { parameters.add(Parameter.builder().name("/prod/app1/key2").value("foo2").build()); parameters.add(Parameter.builder().name("/prod/app1/key3").value("foo3").build()); GetParametersByPathResponse response = GetParametersByPathResponse.builder().parameters(parameters).build(); - when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response); + Mockito.when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response); Map params = provider.getMultiple("/prod/app1"); assertThat(params).contains( @@ -123,7 +122,7 @@ public void getMultipleWithTrailingSlash() { parameters.add(Parameter.builder().name("/prod/app1/key2").value("foo2").build()); parameters.add(Parameter.builder().name("/prod/app1/key3").value("foo3").build()); GetParametersByPathResponse response = GetParametersByPathResponse.builder().parameters(parameters).build(); - when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response); + Mockito.when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response); Map params = provider.getMultiple("/prod/app1/"); assertThat(params).contains( @@ -146,7 +145,7 @@ public void getMultiple_cached_shouldNotCallSSM() { parameters.add(Parameter.builder().name("/prod/app1/key2").value("foo2").build()); parameters.add(Parameter.builder().name("/prod/app1/key3").value("foo3").build()); GetParametersByPathResponse response = GetParametersByPathResponse.builder().parameters(parameters).build(); - when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response); + Mockito.when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response); provider.getMultiple("/prod/app1"); @@ -156,7 +155,8 @@ public void getMultiple_cached_shouldNotCallSSM() { provider.get("/prod/app1/key2"); provider.get("/prod/app1/key3"); - verify(client, times(1)).getParametersByPath(any(GetParametersByPathRequest.class)); + Mockito.verify(client, Mockito.times(1)) + .getParametersByPath(ArgumentMatchers.any(GetParametersByPathRequest.class)); } @@ -172,7 +172,7 @@ public void getMultipleWithNextToken() { parameters2.add(Parameter.builder().name("/prod/app1/key3").value("foo3").build()); GetParametersByPathResponse response2 = GetParametersByPathResponse.builder().parameters(parameters2).build(); - when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response1, response2); + Mockito.when(client.getParametersByPath(paramByPathCaptor.capture())).thenReturn(response1, response2); Map params = provider.getMultiple("/prod/app1"); @@ -197,21 +197,19 @@ public void getMultipleWithNextToken() { } @Test - public void testSecretsProviderBuilderMissingCacheManager_throwsException() { - - // Act & Assert - assertThatIllegalStateException().isThrownBy(() -> SSMProvider.builder() - .withClient(client) - .withTransformationManager(transformationManager) - .build()) - .withMessage("No CacheManager provided, please provide one"); + public void testSSMProvider_withoutParameter_shouldHaveDefaultTransformationManager() { + + // Act + SSMProvider ssmProvider = SSMProvider.builder() + .build(); + // Assert + assertDoesNotThrow(()->ssmProvider.withTransformation(json)); } private void initMock(String expectedValue) { Parameter parameter = Parameter.builder().value(expectedValue).build(); GetParameterResponse result = GetParameterResponse.builder().parameter(parameter).build(); - when(client.getParameter(paramCaptor.capture())).thenReturn(result); - provider.defaultMaxAge(1, ChronoUnit.DAYS); + Mockito.when(client.getParameter(paramCaptor.capture())).thenReturn(result); provider.withMaxAge(2, ChronoUnit.DAYS); provider.recursive(); } diff --git a/powertools-parameters/powertools-parameters-tests/pom.xml b/powertools-parameters/powertools-parameters-tests/pom.xml new file mode 100644 index 000000000..37e266bde --- /dev/null +++ b/powertools-parameters/powertools-parameters-tests/pom.xml @@ -0,0 +1,194 @@ + + + 4.0.0 + + software.amazon.lambda + powertools-parent + 2.0.0-RC1 + ../../pom.xml + + + Powertools for AWS Lambda (Java) library Parameters - Tests + powertools-parameters-tests + Powertools parameters tests that cut across all the parameters providers + + + + software.amazon.lambda + powertools-parameters-ssm + test + ${project.version} + + + software.amazon.lambda + powertools-parameters-secrets + test + ${project.version} + + + software.amazon.lambda + powertools-parameters-dynamodb + test + ${project.version} + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + + + org.apache.commons + commons-lang3 + test + + + org.assertj + assertj-core + test + + + org.aspectj + aspectjweaver + test + + + + + + default + + true + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.2 + + true + + + + + + + generate-graalvm-files + + + org.mockito + mockito-subclass + 5.6.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters,experimental-class-define-support + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + 5.6.0 + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.2 + true + + + test-native + + test + + test + + + + powertools-parameters + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + --native-image-info + -H:+UnlockExperimentalVMOptions + -H:Log=registerResource:5 + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.2 + + true + + + + + + + diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java similarity index 91% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java rename to powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java index edc671e2c..5fa740253 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java +++ b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java @@ -25,6 +25,7 @@ import static software.amazon.lambda.powertools.parameters.transform.Transformer.json; import java.time.Clock; +import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.HashMap; @@ -51,9 +52,8 @@ public void setup() { clock = Clock.systemDefaultZone(); cacheManager = new CacheManager(); - provider = new BasicProvider(cacheManager); transformationManager = new TransformationManager(); - provider.setTransformationManager(transformationManager); + provider = new BasicProvider(cacheManager, transformationManager); } @Test @@ -138,7 +138,8 @@ public void get_customTTL_expired_shouldGetValue() { @Test public void get_customDefaultTTL_cached_shouldGetFromCache() { - provider.defaultMaxAge(12, ChronoUnit.MINUTES).get("foobar"); + provider.cacheManager.setDefaultExpirationTime(Duration.of(12, MINUTES)); + provider.get("foobar"); getFromStore = false; provider.setClock(offset(clock, of(10, MINUTES))); @@ -149,7 +150,7 @@ public void get_customDefaultTTL_cached_shouldGetFromCache() { @Test public void get_customDefaultTTL_expired_shouldGetValue() { - provider.defaultMaxAge(2, ChronoUnit.MINUTES).get("barbaz"); + provider.cacheManager.setDefaultExpirationTime(Duration.of(2, MINUTES)); getFromStore = false; provider.setClock(offset(clock, of(3, MINUTES))); @@ -160,9 +161,7 @@ public void get_customDefaultTTL_expired_shouldGetValue() { @Test public void get_customDefaultTTLAndTTL_cached_shouldGetFromCache() { - provider.defaultMaxAge(12, ChronoUnit.MINUTES) - .withMaxAge(5, SECONDS) - .get("foobaz"); + provider.get("foobaz"); getFromStore = false; provider.setClock(offset(clock, of(4, SECONDS))); @@ -173,9 +172,10 @@ public void get_customDefaultTTLAndTTL_cached_shouldGetFromCache() { @Test public void get_customDefaultTTLAndTTL_expired_shouldGetValue() { - provider.defaultMaxAge(2, ChronoUnit.MINUTES) - .withMaxAge(5, SECONDS) - .get("bariton"); + + provider.cacheManager.setDefaultExpirationTime(Duration.ofMinutes(2)); + + provider.withMaxAge(5, SECONDS).get("bariton"); getFromStore = false; provider.setClock(offset(clock, of(6, SECONDS))); @@ -275,8 +275,9 @@ public void getObject_customTTL_expired_shouldGetValue() { public void getObject_customDefaultTTL_cached_shouldGetFromCache() { provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}"); - provider.defaultMaxAge(12, ChronoUnit.MINUTES) - .withTransformation(json) + provider.cacheManager.setDefaultExpirationTime(Duration.of(12, MINUTES)); + + provider.withTransformation(json) .get("foo", ObjectToDeserialize.class); getFromStore = false; @@ -290,8 +291,9 @@ public void getObject_customDefaultTTL_cached_shouldGetFromCache() { public void getObject_customDefaultTTL_expired_shouldGetValue() { provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}"); - provider.defaultMaxAge(2, ChronoUnit.MINUTES) - .withTransformation(json) + provider.cacheManager.setDefaultExpirationTime(Duration.of(2, MINUTES)); + + provider.withTransformation(json) .get("foo", ObjectToDeserialize.class); getFromStore = false; @@ -305,9 +307,10 @@ public void getObject_customDefaultTTL_expired_shouldGetValue() { public void getObject_customDefaultTTLAndTTL_cached_shouldGetFromCache() { provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}"); - provider.defaultMaxAge(12, ChronoUnit.MINUTES) + provider.cacheManager.setDefaultExpirationTime(Duration.ofSeconds(5)); + + provider.withTransformation(json) .withMaxAge(5, SECONDS) - .withTransformation(json) .get("foo", ObjectToDeserialize.class); getFromStore = false; @@ -321,8 +324,9 @@ public void getObject_customDefaultTTLAndTTL_cached_shouldGetFromCache() { public void getObject_customDefaultTTLAndTTL_expired_shouldGetValue() { provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}"); - provider.defaultMaxAge(2, ChronoUnit.MINUTES) - .withMaxAge(5, SECONDS) + provider.cacheManager.setDefaultExpirationTime(Duration.ofMinutes(2)); + + provider.withMaxAge(5, SECONDS) .withTransformation(json) .get("foo", ObjectToDeserialize.class); getFromStore = false; @@ -335,7 +339,7 @@ public void getObject_customDefaultTTLAndTTL_expired_shouldGetValue() { @Test public void get_noTransformationManager_shouldThrowException() { - provider.setTransformationManager(null); + provider = new BasicProvider(new CacheManager(), null); assertThatIllegalStateException() .isThrownBy(() -> provider.withTransformation(base64).get("foo")); @@ -343,7 +347,6 @@ public void get_noTransformationManager_shouldThrowException() { @Test public void getObject_noTransformationManager_shouldThrowException() { - provider.setTransformationManager(null); assertThatIllegalStateException() .isThrownBy(() -> provider.get("foo", ObjectToDeserialize.class)); @@ -379,8 +382,8 @@ class BasicProvider extends BaseProvider { private String value = "valueFromStore"; - public BasicProvider(CacheManager cacheManager) { - super(cacheManager); + public BasicProvider(CacheManager cacheManager, TransformationManager transformationManager) { + super(cacheManager, transformationManager); } public void setValue(String value) { diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/ParamProvidersIntegrationTest.java similarity index 81% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java rename to powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/ParamProvidersIntegrationTest.java index d6fbe66f0..7d790d140 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java +++ b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/ParamProvidersIntegrationTest.java @@ -14,7 +14,6 @@ package software.amazon.lambda.powertools.parameters; -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; @@ -25,14 +24,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import org.assertj.core.data.MapEntry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; @@ -43,8 +40,10 @@ import software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest; import software.amazon.awssdk.services.ssm.model.GetParametersByPathResponse; import software.amazon.awssdk.services.ssm.model.Parameter; +import software.amazon.lambda.powertools.parameters.secrets.SecretsProvider; +import software.amazon.lambda.powertools.parameters.ssm.SSMProvider; -public class ParamManagerIntegrationTest { +public class ParamProvidersIntegrationTest { @Mock SsmClient ssmClient; @@ -59,19 +58,17 @@ public class ParamManagerIntegrationTest { SecretsManagerClient secretsManagerClient; @Captor ArgumentCaptor secretsCaptor; - @Mock - private AppConfigDataClient appConfigDataClient; @BeforeEach public void setup() throws IllegalAccessException { openMocks(this); - - writeStaticField(ParamManager.class, "providers", new ConcurrentHashMap<>(), true); } @Test public void ssmProvider_get() { - SSMProvider ssmProvider = ParamManager.getSsmProvider(ssmClient); + SSMProvider ssmProvider = SSMProvider.builder() + .withClient(ssmClient) + .build(); String expectedValue = "value"; Parameter parameter = Parameter.builder().value(expectedValue).build(); @@ -87,7 +84,9 @@ public void ssmProvider_get() { @Test public void ssmProvider_getMultiple() { - SSMProvider ssmProvider = ParamManager.getSsmProvider(ssmClient); + SSMProvider ssmProvider = SSMProvider.builder() + .withClient(ssmClient) + .build(); List parameters = new ArrayList<>(); parameters.add(Parameter.builder().name("/prod/app1/key1").value("foo1").build()); @@ -112,7 +111,9 @@ public void ssmProvider_getMultiple() { @Test public void secretsProvider_get() { - SecretsProvider secretsProvider = ParamManager.getSecretsProvider(secretsManagerClient); + SecretsProvider secretsProvider = SecretsProvider.builder() + .withClient(secretsManagerClient) + .build(); String expectedValue = "Value1"; GetSecretValueResponse response = GetSecretValueResponse.builder().secretString(expectedValue).build(); @@ -125,24 +126,4 @@ public void secretsProvider_get() { verify(secretsManagerClient, times(1)).getSecretValue(any(GetSecretValueRequest.class)); } - @Test - public void getDynamoDbProvider() { - - // Act - DynamoDbProvider provider = ParamManager.getDynamoDbProvider(ddbClient, "test-table"); - - // Assert - assertThat(provider).isNotNull(); - } - - @Test - public void getAppConfigProvider() { - - // Act - AppConfigProvider provider = ParamManager.getAppConfigProvider(appConfigDataClient, "test-env", "test-app"); - - // Assert - assertThat(provider).isNotNull(); - - } } diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/CacheManagerTest.java b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/cache/CacheManagerTest.java similarity index 100% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/CacheManagerTest.java rename to powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/cache/CacheManagerTest.java diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/DataStoreTest.java b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/cache/DataStoreTest.java similarity index 100% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/DataStoreTest.java rename to powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/cache/DataStoreTest.java diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/AnotherObject.java b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/internal/AnotherObject.java similarity index 100% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/AnotherObject.java rename to powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/internal/AnotherObject.java diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/CustomProvider.java b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/internal/CustomProvider.java similarity index 97% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/CustomProvider.java rename to powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/internal/CustomProvider.java index 2c9db3712..a98d68c22 100644 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/CustomProvider.java +++ b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/internal/CustomProvider.java @@ -25,7 +25,7 @@ public class CustomProvider extends BaseProvider { private final Map values = new HashMap<>(); public CustomProvider(CacheManager cacheManager) { - super(cacheManager); + super(cacheManager, null); values.put("/simple", "value"); values.put("/base64", Base64.getEncoder().encodeToString("value".getBytes())); values.put("/json", "{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}"); diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/Base64TransformerTest.java b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/transform/Base64TransformerTest.java similarity index 100% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/Base64TransformerTest.java rename to powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/transform/Base64TransformerTest.java diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformerTest.java b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformerTest.java similarity index 100% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformerTest.java rename to powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformerTest.java diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/ObjectToDeserialize.java b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/transform/ObjectToDeserialize.java similarity index 100% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/ObjectToDeserialize.java rename to powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/transform/ObjectToDeserialize.java diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/TransformationManagerTest.java b/powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/transform/TransformationManagerTest.java similarity index 100% rename from powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/TransformationManagerTest.java rename to powertools-parameters/powertools-parameters-tests/src/test/java/software/amazon/lambda/powertools/parameters/transform/TransformationManagerTest.java diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseParamAspect.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseParamAspect.java new file mode 100644 index 000000000..f7ebdf97a --- /dev/null +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseParamAspect.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.parameters; + +import org.aspectj.lang.reflect.FieldSignature; +import software.amazon.lambda.powertools.parameters.transform.Transformer; + +/** + * Provides a common base for all parameter aspects. This lets us group functionality that + * we need to reimplement in each aspect. This class should be extended for each + * additional parameter aspect. + */ +public abstract class BaseParamAspect { + + /** + * Gets the parameter value from the provider and transforms it if necessary. This transformation + * is generic across all parameter providers. + * + * @param key The key of the parameter to get + * @param transformer The transformer to use to transform the parameter value + * @param provider A concrete instance of the parameter provider to retrieve the value from + * @param fieldSignature The signature of the field that the parameter is being injected into + * @return The value of the parameter, transformed if necessary + */ + protected Object getAndTransform(String key, Class transformer, BaseProvider provider, + FieldSignature fieldSignature) { + if (transformer.isInterface()) { + // No transformation + return provider.get(key); + } else { + Class fieldType = fieldSignature.getFieldType(); + if (String.class.isAssignableFrom(fieldType)) { + // Basic transformation + return provider + .withTransformation(transformer) + .get(key); + } else { + // Complex transformation + return provider + .withTransformation(transformer) + .get(key, fieldType); + } + } + } +} diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java index edb82f5ec..bedace28c 100644 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java +++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java @@ -34,11 +34,12 @@ public abstract class BaseProvider implements ParamProvider { public static final String PARAMETERS = "parameters"; protected final CacheManager cacheManager; - private TransformationManager transformationManager; + private final TransformationManager transformationManager; private Clock clock; - public BaseProvider(CacheManager cacheManager) { + public BaseProvider(CacheManager cacheManager, TransformationManager transformationManager) { this.cacheManager = cacheManager; + this.transformationManager = transformationManager; } /** @@ -59,25 +60,10 @@ public BaseProvider(CacheManager cacheManager) { */ protected abstract Map getMultipleValues(String path); - /** - * (Optional) Set the default max age for the cache of all parameters. Override the default 5 seconds.
- * If for some parameters, you need to set a different maxAge, use {@link #withMaxAge(int, ChronoUnit)}.
- * Use {@link #withMaxAge(int, ChronoUnit)} after {#defaultMaxAge(int, ChronoUnit)} in the chain. - * - * @param maxAge Maximum time to cache the parameter, before calling the underlying parameter store. - * @param unit Unit of time - * @return the provider itself in order to chain calls (eg.
provider.defaultMaxAge(10, SECONDS).get("key")
). - */ - protected BaseProvider defaultMaxAge(int maxAge, ChronoUnit unit) { - Duration duration = Duration.of(maxAge, unit); - cacheManager.setDefaultExpirationTime(duration); - return this; - } - /** * (Optional) Builder method to call before {@link #get(String)} or {@link #get(String, Class)} * to set cache max age for the parameter to get.

- * The max age is reset to default (either 5 or a custom value set with {@link #defaultMaxAge}) after each get, + * The max age is reset to default (either 5 or a custom value that may be set on the CacheManager) after each get, * so you need to use this method for each parameter to cache with non-default max age.

* * Not Thread Safe: calling this method simultaneously by several threads @@ -225,10 +211,6 @@ protected void resetToDefaults() { } } - protected void setTransformationManager(TransformationManager transformationManager) { - this.transformationManager = transformationManager; - } - /** * For test purpose * diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/DynamoDbProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/DynamoDbProvider.java deleted file mode 100644 index 3a8732e18..000000000 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/DynamoDbProvider.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.parameters; - -import java.util.Collections; -import java.util.Map; -import java.util.stream.Collectors; -import software.amazon.awssdk.core.SdkSystemSetting; -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; -import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; -import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; -import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; -import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; -import software.amazon.awssdk.services.dynamodb.model.QueryRequest; -import software.amazon.awssdk.services.dynamodb.model.QueryResponse; -import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator; -import software.amazon.lambda.powertools.parameters.cache.CacheManager; -import software.amazon.lambda.powertools.parameters.exception.DynamoDbProviderSchemaException; -import software.amazon.lambda.powertools.parameters.transform.TransformationManager; - -/** - * Implements a {@link ParamProvider} on top of DynamoDB. The schema of the table - * is described in the Powertools for AWS Lambda (Java) documentation. - * - * @see Parameters provider documentation - */ -public class DynamoDbProvider extends BaseProvider { - - private final DynamoDbClient client; - private final String tableName; - - DynamoDbProvider(CacheManager cacheManager, DynamoDbClient client, String tableName) { - super(cacheManager); - this.client = client; - this.tableName = tableName; - } - - DynamoDbProvider(CacheManager cacheManager, String tableName) { - this(cacheManager, Builder.createClient(), tableName); - } - - /** - * Create a builder that can be used to configure and create a {@link DynamoDbProvider}. - * - * @return a new instance of {@link DynamoDbProvider.Builder} - */ - public static DynamoDbProvider.Builder builder() { - return new DynamoDbProvider.Builder(); - } - - /** - * Return a single value from the DynamoDB parameter provider. - * - * @param key key of the parameter - * @return The value, if it exists, null if it doesn't. Throws if the row exists but doesn't match the schema. - */ - @Override - protected String getValue(String key) { - GetItemResponse resp = client.getItem(GetItemRequest.builder() - .tableName(tableName) - .key(Collections.singletonMap("id", AttributeValue.fromS(key))) - .attributesToGet("value") - .build()); - - // If we have an item at the key, we should be able to get a 'val' out of it. If not it's - // exceptional. - // If we don't have an item at the key, we should return null. - if (resp.hasItem() && !resp.item().values().isEmpty()) { - if (!resp.item().containsKey("value")) { - throw new DynamoDbProviderSchemaException("Missing 'value': " + resp.item().toString()); - } - return resp.item().get("value").s(); - } - - return null; - } - - /** - * Returns multiple values from the DynamoDB parameter provider. - * - * @param path Parameter store path - * @return All values matching the given path, and an empty map if none do. Throws if any records exist that don't match the schema. - */ - @Override - protected Map getMultipleValues(String path) { - - QueryResponse resp = client.query(QueryRequest.builder() - .tableName(tableName) - .keyConditionExpression("id = :v_id") - .expressionAttributeValues(Collections.singletonMap(":v_id", AttributeValue.fromS(path))) - .build()); - - return resp - .items() - .stream() - .peek((i) -> - { - if (!i.containsKey("sk")) { - throw new DynamoDbProviderSchemaException("Missing 'sk': " + i.toString()); - } - if (!i.containsKey("value")) { - throw new DynamoDbProviderSchemaException("Missing 'value': " + i.toString()); - } - }) - .collect( - Collectors.toMap( - (i) -> i.get("sk").s(), - (i) -> i.get("value").s())); - - - } - - static class Builder { - private DynamoDbClient client; - private String table; - private CacheManager cacheManager; - private TransformationManager transformationManager; - - private static DynamoDbClient createClient() { - return DynamoDbClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) - .overrideConfiguration(ClientOverrideConfiguration.builder() - .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, - UserAgentConfigurator.getUserAgent(PARAMETERS)).build()) - .build(); - } - - /** - * Create a {@link DynamoDbProvider} instance. - * - * @return a {@link DynamoDbProvider} - */ - public DynamoDbProvider build() { - if (cacheManager == null) { - throw new IllegalStateException("No CacheManager provided; please provide one"); - } - if (table == null) { - throw new IllegalStateException("No DynamoDB table name provided; please provide one"); - } - DynamoDbProvider provider; - if (client == null) { - client = createClient(); - } - provider = new DynamoDbProvider(cacheManager, client, table); - - if (transformationManager != null) { - provider.setTransformationManager(transformationManager); - } - return provider; - } - - /** - * Set custom {@link DynamoDbClient} to pass to the {@link DynamoDbClient}.
- * Use it if you want to customize the region or any other part of the client. - * - * @param client Custom client - * @return the builder to chain calls (eg.
builder.withClient().build()
) - */ - public DynamoDbProvider.Builder withClient(DynamoDbClient client) { - this.client = client; - return this; - } - - /** - * Mandatory. Provide a CacheManager to the {@link DynamoDbProvider} - * - * @param cacheManager the manager that will handle the cache of parameters - * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) - */ - public DynamoDbProvider.Builder withCacheManager(CacheManager cacheManager) { - this.cacheManager = cacheManager; - return this; - } - - /** - * Mandatory. Provide a DynamoDB table to the {@link DynamoDbProvider} - * - * @param table the table that parameters will be retrieved from. - * @return the builder to chain calls (eg.
builder.withTable().build()
) - */ - public DynamoDbProvider.Builder withTable(String table) { - this.table = table; - return this; - } - - /** - * Provide a transformationManager to the {@link DynamoDbProvider} - * - * @param transformationManager the manager that will handle transformation of parameters - * @return the builder to chain calls (eg.
builder.withTransformationManager().build()
) - */ - public DynamoDbProvider.Builder withTransformationManager(TransformationManager transformationManager) { - this.transformationManager = transformationManager; - return this; - } - } -} diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java deleted file mode 100644 index 6fee0f114..000000000 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.parameters; - -import java.lang.reflect.Constructor; -import java.util.concurrent.ConcurrentHashMap; -import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; -import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.lambda.powertools.parameters.cache.CacheManager; -import software.amazon.lambda.powertools.parameters.transform.TransformationManager; - -/** - * Utility class to retrieve instances of parameter providers. - * Each instance is unique (singleton). - */ -public final class ParamManager { - - private static final CacheManager cacheManager = new CacheManager(); - private static final TransformationManager transformationManager = new TransformationManager(); - - // NOTE: For testing purposes `providers` cannot be final - private static ConcurrentHashMap, BaseProvider> providers = new ConcurrentHashMap<>(); - - /** - * Get a concrete implementation of {@link BaseProvider}.
- * You can specify {@link SecretsProvider}, {@link SSMProvider} or create your - * custom provider by extending {@link BaseProvider} if you need to integrate with a different parameter store. - * - * @return a {@link SecretsProvider} - * @deprecated You should not use this method directly but a typed one (getSecretsProvider, getSsmProvider, getDynamoDbProvider, getAppConfigProvider), will be removed in v2 - */ - // TODO in v2: remove public access to this and review how we get providers (it was not designed for DDB and AppConfig in mind initially) - public static T getProvider(Class providerClass) { - if (providerClass == null) { - throw new IllegalStateException("providerClass cannot be null."); - } - if (providerClass == DynamoDbProvider.class || providerClass == AppConfigProvider.class) { - throw new IllegalArgumentException( - providerClass + " cannot be instantiated like this, additional parameters are required"); - } - return (T) providers.computeIfAbsent(providerClass, ParamManager::createProvider); - } - - /** - * Get a {@link SecretsProvider} with default {@link SecretsManagerClient}.
- * If you need to customize the region, or other part of the client, use {@link ParamManager#getSecretsProvider(SecretsManagerClient)} instead. - * - * @return a {@link SecretsProvider} - */ - public static SecretsProvider getSecretsProvider() { - return getProvider(SecretsProvider.class); - } - - /** - * Get a {@link SSMProvider} with default {@link SsmClient}.
- * If you need to customize the region, or other part of the client, use {@link ParamManager#getSsmProvider(SsmClient)} instead. - * - * @return a {@link SSMProvider} - */ - public static SSMProvider getSsmProvider() { - return getProvider(SSMProvider.class); - } - - /** - * Get a {@link DynamoDbProvider} with default {@link DynamoDbClient}
- * If you need to customize the region, or other part of the client, use {@link ParamManager#getDynamoDbProvider(DynamoDbClient, String)} - */ - public static DynamoDbProvider getDynamoDbProvider(String tableName) { - // Because we need a DDB table name to configure our client, we can't use - // ParamManager#getProvider. This means that we need to make sure we do the same stuff - - // set transformation manager and cache manager. - return DynamoDbProvider.builder() - .withCacheManager(cacheManager) - .withTable(tableName) - .withTransformationManager(transformationManager) - .build(); - } - - /** - * Get a {@link AppConfigProvider} with default {@link AppConfigDataClient}.
- * If you need to customize the region, or other part of the client, use {@link ParamManager#getAppConfigProvider(AppConfigDataClient, String, String)} instead. - * - * @return a {@link AppConfigProvider} - */ - public static AppConfigProvider getAppConfigProvider(String environment, String application) { - // Because we need a DDB table name to configure our client, we can't use - // ParamManager#getProvider. This means that we need to make sure we do the same stuff - - // set transformation manager and cache manager. - return AppConfigProvider.builder() - .withCacheManager(cacheManager) - .withTransformationManager(transformationManager) - .withEnvironment(environment) - .withApplication(application) - .build(); - } - - - /** - * Get a {@link SecretsProvider} with your custom {@link SecretsManagerClient}.
- * Use this to configure region or other part of the client. Use {@link ParamManager#getSsmProvider()} if you don't need this customization. - * - * @return a {@link SecretsProvider} - */ - public static SecretsProvider getSecretsProvider(SecretsManagerClient client) { - return (SecretsProvider) providers.computeIfAbsent(SecretsProvider.class, (k) -> SecretsProvider.builder() - .withClient(client) - .withCacheManager(cacheManager) - .withTransformationManager(transformationManager) - .build()); - } - - /** - * Get a {@link SSMProvider} with your custom {@link SsmClient}.
- * Use this to configure region or other part of the client. Use {@link ParamManager#getSsmProvider()} if you don't need this customization. - * - * @return a {@link SSMProvider} - */ - public static SSMProvider getSsmProvider(SsmClient client) { - return (SSMProvider) providers.computeIfAbsent(SSMProvider.class, (k) -> SSMProvider.builder() - .withClient(client) - .withCacheManager(cacheManager) - .withTransformationManager(transformationManager) - .build()); - } - - /** - * Get a {@link DynamoDbProvider} with your custom {@link DynamoDbClient}.
- * Use this to configure region or other part of the client. Use {@link ParamManager#getDynamoDbProvider(String)} )} if you don't need this customization. - * - * @return a {@link DynamoDbProvider} - */ - public static DynamoDbProvider getDynamoDbProvider(DynamoDbClient client, String table) { - return (DynamoDbProvider) providers.computeIfAbsent(DynamoDbProvider.class, (k) -> DynamoDbProvider.builder() - .withClient(client) - .withTable(table) - .withCacheManager(cacheManager) - .withTransformationManager(transformationManager) - .build()); - } - - /** - * Get a {@link AppConfigProvider} with your custom {@link AppConfigDataClient}.
- * Use this to configure region or other part of the client. Use {@link ParamManager#getAppConfigProvider(String, String)} if you don't need this customization. - * - * @return a {@link AppConfigProvider} - */ - public static AppConfigProvider getAppConfigProvider(AppConfigDataClient client, String environment, - String application) { - return (AppConfigProvider) providers.computeIfAbsent(AppConfigProvider.class, (k) -> AppConfigProvider.builder() - .withClient(client) - .withCacheManager(cacheManager) - .withTransformationManager(transformationManager) - .withEnvironment(environment) - .withApplication(application) - .build()); - } - - - public static CacheManager getCacheManager() { - return cacheManager; - } - - public static TransformationManager getTransformationManager() { - return transformationManager; - } - - static T createProvider(Class providerClass) { - try { - Constructor constructor = providerClass.getDeclaredConstructor(CacheManager.class); - T provider = - constructor.newInstance(cacheManager); // FIXME: avoid reflection here as we may have issues (#1280) - provider.setTransformationManager(transformationManager); - return provider; - } catch (ReflectiveOperationException e) { - throw new RuntimeException("Unexpected error occurred. Please raise issue at " + - "https://github.com/aws-powertools/powertools-lambda-java/issues", e); - } - } - -} diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SecretsProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SecretsProvider.java deleted file mode 100644 index 2612f6c7f..000000000 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SecretsProvider.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.parameters; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.time.temporal.ChronoUnit; -import java.util.Base64; -import java.util.Map; -import software.amazon.awssdk.core.SdkSystemSetting; -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; -import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; -import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; -import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; -import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator; -import software.amazon.lambda.powertools.parameters.cache.CacheManager; -import software.amazon.lambda.powertools.parameters.transform.TransformationManager; -import software.amazon.lambda.powertools.parameters.transform.Transformer; - -/** - * AWS Secrets Manager Parameter Provider

- * - * Samples: - *
- *     SecretsProvider provider = ParamManager.getSecretsProvider();
- *
- *     String value = provider.get("key");
- *     System.out.println(value);
- *     >>> "value"
- *
- *     // Get a value and cache it for 30 seconds (all others values will now be cached for 30 seconds)
- *     String value = provider.defaultMaxAge(30, ChronoUnit.SECONDS).get("key");
- *
- *     // Get a value and cache it for 1 minute (all others values are cached for 5 seconds by default)
- *     String value = provider.withMaxAge(1, ChronoUnit.MINUTES).get("key");
- *
- *     // Get a base64 encoded value, decoded into a String, and store it in the cache
- *     String value = provider.withTransformation(Transformer.base64).get("key");
- *
- *     // Get a json value, transform it into an Object, and store it in the cache
- *     TargetObject = provider.withTransformation(Transformer.json).get("key", TargetObject.class);
- * 
- */ -public class SecretsProvider extends BaseProvider { - - private final SecretsManagerClient client; - - /** - * Constructor with custom {@link SecretsManagerClient}.
- * Use when you need to customize region or any other attribute of the client.

- *

- * Use the {@link Builder} to create an instance of it. - * - * @param client custom client you would like to use. - */ - SecretsProvider(CacheManager cacheManager, SecretsManagerClient client) { - super(cacheManager); - this.client = client; - } - - /** - * Constructor with only a CacheManager
- *

- * Used in {@link ParamManager#createProvider(Class)} - * - * @param cacheManager handles the parameter caching - */ - SecretsProvider(CacheManager cacheManager) { - this(cacheManager, Builder.createClient()); - } - - /** - * Create a builder that can be used to configure and create a {@link SecretsProvider}. - * - * @return a new instance of {@link SecretsProvider.Builder} - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Retrieve the parameter value from the AWS Secrets Manager. - * - * @param key key of the parameter - * @return the value of the parameter identified by the key - */ - @Override - protected String getValue(String key) { - GetSecretValueRequest request = GetSecretValueRequest.builder().secretId(key).build(); - - String secretValue = client.getSecretValue(request).secretString(); - if (secretValue == null) { - secretValue = - new String(Base64.getDecoder().decode(client.getSecretValue(request).secretBinary().asByteArray()), - UTF_8); - } - return secretValue; - } - - /** - * @throws UnsupportedOperationException as it is not possible to get multiple values simultaneously from Secrets Manager - */ - @Override - protected Map getMultipleValues(String path) { - throw new UnsupportedOperationException("Impossible to get multiple values from AWS Secrets Manager"); - } - - /** - * {@inheritDoc} - */ - @Override - public SecretsProvider defaultMaxAge(int maxAge, ChronoUnit unit) { - super.defaultMaxAge(maxAge, unit); - return this; - } - - /** - * {@inheritDoc} - */ - @Override - public SecretsProvider withMaxAge(int maxAge, ChronoUnit unit) { - super.withMaxAge(maxAge, unit); - return this; - } - - /** - * {@inheritDoc} - */ - @Override - public SecretsProvider withTransformation(Class transformerClass) { - super.withTransformation(transformerClass); - return this; - } - - // For test purpose only - SecretsManagerClient getClient() { - return client; - } - - static class Builder { - - private SecretsManagerClient client; - private CacheManager cacheManager; - private TransformationManager transformationManager; - - private static SecretsManagerClient createClient() { - return SecretsManagerClient.builder() - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable()))) - .overrideConfiguration(ClientOverrideConfiguration.builder() - .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, - UserAgentConfigurator.getUserAgent(PARAMETERS)).build()) - .build(); - } - - /** - * Create a {@link SecretsProvider} instance. - * - * @return a {@link SecretsProvider} - */ - public SecretsProvider build() { - if (cacheManager == null) { - throw new IllegalStateException("No CacheManager provided, please provide one"); - } - SecretsProvider provider; - if (client == null) { - client = createClient(); - } - - provider = new SecretsProvider(cacheManager, client); - - if (transformationManager != null) { - provider.setTransformationManager(transformationManager); - } - return provider; - } - - /** - * Set custom {@link SecretsManagerClient} to pass to the {@link SecretsProvider}.
- * Use it if you want to customize the region or any other part of the client. - * - * @param client Custom client - * @return the builder to chain calls (eg.

builder.withClient().build()
) - */ - public Builder withClient(SecretsManagerClient client) { - this.client = client; - return this; - } - - /** - * Mandatory. Provide a CacheManager to the {@link SecretsProvider} - * - * @param cacheManager the manager that will handle the cache of parameters - * @return the builder to chain calls (eg.
builder.withCacheManager().build()
) - */ - public Builder withCacheManager(CacheManager cacheManager) { - this.cacheManager = cacheManager; - return this; - } - - /** - * Provide a transformationManager to the {@link SecretsProvider} - * - * @param transformationManager the manager that will handle transformation of parameters - * @return the builder to chain calls (eg.
builder.withTransformationManager().build()
) - */ - public Builder withTransformationManager(TransformationManager transformationManager) { - this.transformationManager = transformationManager; - return this; - } - } -} diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspect.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspect.java deleted file mode 100644 index 081af108d..000000000 --- a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspect.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.parameters.internal; - -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; -import org.aspectj.lang.reflect.FieldSignature; -import software.amazon.lambda.powertools.parameters.BaseProvider; -import software.amazon.lambda.powertools.parameters.Param; -import software.amazon.lambda.powertools.parameters.ParamManager; - -@Aspect -public class LambdaParametersAspect { - - @Pointcut("get(* *) && @annotation(paramAnnotation)") - public void getParam(Param paramAnnotation) { - } - - @Around("getParam(paramAnnotation)") - public Object injectParam(final ProceedingJoinPoint joinPoint, final Param paramAnnotation) { - BaseProvider provider = ParamManager.getProvider(paramAnnotation.provider()); - - if (paramAnnotation.transformer().isInterface()) { - // No transformation - return provider.get(paramAnnotation.key()); - } else { - FieldSignature s = (FieldSignature) joinPoint.getSignature(); - if (String.class.isAssignableFrom(s.getFieldType())) { - // Basic transformation - return provider - .withTransformation(paramAnnotation.transformer()) - .get(paramAnnotation.key()); - } else { - // Complex transformation - return provider - .withTransformation(paramAnnotation.transformer()) - .get(paramAnnotation.key(), s.getFieldType()); - } - } - } - -} diff --git a/powertools-parameters/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters/reflect-config.json b/powertools-parameters/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters/reflect-config.json new file mode 100644 index 000000000..380489707 --- /dev/null +++ b/powertools-parameters/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-parameters/reflect-config.json @@ -0,0 +1,197 @@ +[ +{ + "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.tools.attach.VirtualMachine" +}, +{ + "name":"java.io.Serializable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.AutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.Class", + "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] +}, +{ + "name":"java.lang.ClassLoader", + "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] +}, +{ + "name":"java.lang.Module", + "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] +}, +{ + "name":"java.lang.Object", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] +}, +{ + "name":"java.lang.ProcessHandle", + "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime", + "methods":[{"name":"version","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime$Version", + "methods":[{"name":"feature","parameterTypes":[] }] +}, +{ + "name":"java.lang.StackWalker" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.annotation.Retention", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.annotation.Target", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.invoke.MethodHandle", + "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles", + "methods":[{"name":"lookup","parameterTypes":[] }] +}, +{ + "name":"java.lang.invoke.MethodHandles$Lookup", + "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] +}, +{ + "name":"java.lang.invoke.MethodType", + "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] +}, +{ + "name":"java.lang.reflect.AccessibleObject", + "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] +}, +{ + "name":"java.lang.reflect.AnnotatedArrayType", + "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedParameterizedType", + "methods":[{"name":"getAnnotatedActualTypeArguments","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedType", + "methods":[{"name":"getType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Executable", + "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Parameter", + "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.util.concurrent.ForkJoinTask", + "fields":[{"name":"aux"}, {"name":"status"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"jdk.internal.misc.Unsafe" +}, +{ + "name":"kotlin.jvm.JvmInline" +}, +{ + "name":"org.apiguardian.api.API", + "queryAllPublicMethods":true +}, +{ + "name":"software.amazon.awssdk.awscore.AwsClient", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"software.amazon.awssdk.core.SdkClient", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"serviceName","parameterTypes":[] }] +}, +{ + "name":"software.amazon.awssdk.services.dynamodb.DynamoDbClient", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"batchExecuteStatement","parameterTypes":["java.util.function.Consumer"] }, {"name":"batchExecuteStatement","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.BatchExecuteStatementRequest"] }, {"name":"batchGetItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"batchGetItem","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest"] }, {"name":"batchGetItemPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"batchGetItemPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest"] }, {"name":"batchWriteItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"batchWriteItem","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.BatchWriteItemRequest"] }, {"name":"createBackup","parameterTypes":["java.util.function.Consumer"] }, {"name":"createBackup","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.CreateBackupRequest"] }, {"name":"createGlobalTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"createGlobalTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.CreateGlobalTableRequest"] }, {"name":"createTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"createTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.CreateTableRequest"] }, {"name":"deleteBackup","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteBackup","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DeleteBackupRequest"] }, {"name":"deleteItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteItem","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest"] }, {"name":"deleteResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteResourcePolicy","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DeleteResourcePolicyRequest"] }, {"name":"deleteTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest"] }, {"name":"describeBackup","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeBackup","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeBackupRequest"] }, {"name":"describeContinuousBackups","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeContinuousBackups","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeContinuousBackupsRequest"] }, {"name":"describeContributorInsights","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeContributorInsights","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeContributorInsightsRequest"] }, {"name":"describeEndpoints","parameterTypes":[] }, {"name":"describeEndpoints","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeEndpoints","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeEndpointsRequest"] }, {"name":"describeExport","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeExport","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeExportRequest"] }, {"name":"describeGlobalTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeGlobalTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeGlobalTableRequest"] }, {"name":"describeGlobalTableSettings","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeGlobalTableSettings","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeGlobalTableSettingsRequest"] }, {"name":"describeImport","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeImport","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeImportRequest"] }, {"name":"describeKinesisStreamingDestination","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeKinesisStreamingDestination","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeKinesisStreamingDestinationRequest"] }, {"name":"describeLimits","parameterTypes":[] }, {"name":"describeLimits","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeLimits","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeLimitsRequest"] }, {"name":"describeTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest"] }, {"name":"describeTableReplicaAutoScaling","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeTableReplicaAutoScaling","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeTableReplicaAutoScalingRequest"] }, {"name":"describeTimeToLive","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeTimeToLive","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DescribeTimeToLiveRequest"] }, {"name":"disableKinesisStreamingDestination","parameterTypes":["java.util.function.Consumer"] }, {"name":"disableKinesisStreamingDestination","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.DisableKinesisStreamingDestinationRequest"] }, {"name":"enableKinesisStreamingDestination","parameterTypes":["java.util.function.Consumer"] }, {"name":"enableKinesisStreamingDestination","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.EnableKinesisStreamingDestinationRequest"] }, {"name":"executeStatement","parameterTypes":["java.util.function.Consumer"] }, {"name":"executeStatement","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ExecuteStatementRequest"] }, {"name":"executeTransaction","parameterTypes":["java.util.function.Consumer"] }, {"name":"executeTransaction","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ExecuteTransactionRequest"] }, {"name":"exportTableToPointInTime","parameterTypes":["java.util.function.Consumer"] }, {"name":"exportTableToPointInTime","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ExportTableToPointInTimeRequest"] }, {"name":"getItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"getItem","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.GetItemRequest"] }, {"name":"getResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"getResourcePolicy","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.GetResourcePolicyRequest"] }, {"name":"importTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"importTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ImportTableRequest"] }, {"name":"listBackups","parameterTypes":[] }, {"name":"listBackups","parameterTypes":["java.util.function.Consumer"] }, {"name":"listBackups","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListBackupsRequest"] }, {"name":"listContributorInsights","parameterTypes":["java.util.function.Consumer"] }, {"name":"listContributorInsights","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListContributorInsightsRequest"] }, {"name":"listContributorInsightsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listContributorInsightsPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListContributorInsightsRequest"] }, {"name":"listExports","parameterTypes":["java.util.function.Consumer"] }, {"name":"listExports","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListExportsRequest"] }, {"name":"listExportsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listExportsPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListExportsRequest"] }, {"name":"listGlobalTables","parameterTypes":[] }, {"name":"listGlobalTables","parameterTypes":["java.util.function.Consumer"] }, {"name":"listGlobalTables","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListGlobalTablesRequest"] }, {"name":"listImports","parameterTypes":["java.util.function.Consumer"] }, {"name":"listImports","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListImportsRequest"] }, {"name":"listImportsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listImportsPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListImportsRequest"] }, {"name":"listTables","parameterTypes":[] }, {"name":"listTables","parameterTypes":["java.util.function.Consumer"] }, {"name":"listTables","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListTablesRequest"] }, {"name":"listTablesPaginator","parameterTypes":[] }, {"name":"listTablesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listTablesPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListTablesRequest"] }, {"name":"listTagsOfResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"listTagsOfResource","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ListTagsOfResourceRequest"] }, {"name":"putItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"putItem","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.PutItemRequest"] }, {"name":"putResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"putResourcePolicy","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.PutResourcePolicyRequest"] }, {"name":"query","parameterTypes":["java.util.function.Consumer"] }, {"name":"query","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.QueryRequest"] }, {"name":"queryPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"queryPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.QueryRequest"] }, {"name":"restoreTableFromBackup","parameterTypes":["java.util.function.Consumer"] }, {"name":"restoreTableFromBackup","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.RestoreTableFromBackupRequest"] }, {"name":"restoreTableToPointInTime","parameterTypes":["java.util.function.Consumer"] }, {"name":"restoreTableToPointInTime","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.RestoreTableToPointInTimeRequest"] }, {"name":"scan","parameterTypes":["java.util.function.Consumer"] }, {"name":"scan","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ScanRequest"] }, {"name":"scanPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"scanPaginator","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.ScanRequest"] }, {"name":"serviceClientConfiguration","parameterTypes":[] }, {"name":"tagResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"tagResource","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.TagResourceRequest"] }, {"name":"transactGetItems","parameterTypes":["java.util.function.Consumer"] }, {"name":"transactGetItems","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.TransactGetItemsRequest"] }, {"name":"transactWriteItems","parameterTypes":["java.util.function.Consumer"] }, {"name":"transactWriteItems","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest"] }, {"name":"untagResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"untagResource","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UntagResourceRequest"] }, {"name":"updateContinuousBackups","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateContinuousBackups","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateContinuousBackupsRequest"] }, {"name":"updateContributorInsights","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateContributorInsights","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateContributorInsightsRequest"] }, {"name":"updateGlobalTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateGlobalTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateGlobalTableRequest"] }, {"name":"updateGlobalTableSettings","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateGlobalTableSettings","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateGlobalTableSettingsRequest"] }, {"name":"updateItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateItem","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest"] }, {"name":"updateKinesisStreamingDestination","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateKinesisStreamingDestination","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateKinesisStreamingDestinationRequest"] }, {"name":"updateTable","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateTable","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateTableRequest"] }, {"name":"updateTableReplicaAutoScaling","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateTableReplicaAutoScaling","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateTableReplicaAutoScalingRequest"] }, {"name":"updateTimeToLive","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateTimeToLive","parameterTypes":["software.amazon.awssdk.services.dynamodb.model.UpdateTimeToLiveRequest"] }, {"name":"waiter","parameterTypes":[] }] +}, +{ + "name":"software.amazon.awssdk.services.secretsmanager.SecretsManagerClient", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"batchGetSecretValue","parameterTypes":["java.util.function.Consumer"] }, {"name":"batchGetSecretValue","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.BatchGetSecretValueRequest"] }, {"name":"batchGetSecretValuePaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"batchGetSecretValuePaginator","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.BatchGetSecretValueRequest"] }, {"name":"cancelRotateSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"cancelRotateSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.CancelRotateSecretRequest"] }, {"name":"createSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"createSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest"] }, {"name":"deleteResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteResourcePolicy","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.DeleteResourcePolicyRequest"] }, {"name":"deleteSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.DeleteSecretRequest"] }, {"name":"describeSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.DescribeSecretRequest"] }, {"name":"getRandomPassword","parameterTypes":[] }, {"name":"getRandomPassword","parameterTypes":["java.util.function.Consumer"] }, {"name":"getRandomPassword","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.GetRandomPasswordRequest"] }, {"name":"getResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"getResourcePolicy","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.GetResourcePolicyRequest"] }, {"name":"getSecretValue","parameterTypes":["java.util.function.Consumer"] }, {"name":"getSecretValue","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest"] }, {"name":"listSecretVersionIds","parameterTypes":["java.util.function.Consumer"] }, {"name":"listSecretVersionIds","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.ListSecretVersionIdsRequest"] }, {"name":"listSecretVersionIdsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listSecretVersionIdsPaginator","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.ListSecretVersionIdsRequest"] }, {"name":"listSecrets","parameterTypes":[] }, {"name":"listSecrets","parameterTypes":["java.util.function.Consumer"] }, {"name":"listSecrets","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.ListSecretsRequest"] }, {"name":"listSecretsPaginator","parameterTypes":[] }, {"name":"listSecretsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listSecretsPaginator","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.ListSecretsRequest"] }, {"name":"putResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"putResourcePolicy","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.PutResourcePolicyRequest"] }, {"name":"putSecretValue","parameterTypes":["java.util.function.Consumer"] }, {"name":"putSecretValue","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.PutSecretValueRequest"] }, {"name":"removeRegionsFromReplication","parameterTypes":["java.util.function.Consumer"] }, {"name":"removeRegionsFromReplication","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.RemoveRegionsFromReplicationRequest"] }, {"name":"replicateSecretToRegions","parameterTypes":["java.util.function.Consumer"] }, {"name":"replicateSecretToRegions","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.ReplicateSecretToRegionsRequest"] }, {"name":"restoreSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"restoreSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.RestoreSecretRequest"] }, {"name":"rotateSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"rotateSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.RotateSecretRequest"] }, {"name":"serviceClientConfiguration","parameterTypes":[] }, {"name":"stopReplicationToReplica","parameterTypes":["java.util.function.Consumer"] }, {"name":"stopReplicationToReplica","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.StopReplicationToReplicaRequest"] }, {"name":"tagResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"tagResource","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.TagResourceRequest"] }, {"name":"untagResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"untagResource","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.UntagResourceRequest"] }, {"name":"updateSecret","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateSecret","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.UpdateSecretRequest"] }, {"name":"updateSecretVersionStage","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateSecretVersionStage","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.UpdateSecretVersionStageRequest"] }, {"name":"validateResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"validateResourcePolicy","parameterTypes":["software.amazon.awssdk.services.secretsmanager.model.ValidateResourcePolicyRequest"] }] +}, +{ + "name":"software.amazon.awssdk.services.ssm.SsmClient", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"addTagsToResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"addTagsToResource","parameterTypes":["software.amazon.awssdk.services.ssm.model.AddTagsToResourceRequest"] }, {"name":"associateOpsItemRelatedItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"associateOpsItemRelatedItem","parameterTypes":["software.amazon.awssdk.services.ssm.model.AssociateOpsItemRelatedItemRequest"] }, {"name":"cancelCommand","parameterTypes":["java.util.function.Consumer"] }, {"name":"cancelCommand","parameterTypes":["software.amazon.awssdk.services.ssm.model.CancelCommandRequest"] }, {"name":"cancelMaintenanceWindowExecution","parameterTypes":["java.util.function.Consumer"] }, {"name":"cancelMaintenanceWindowExecution","parameterTypes":["software.amazon.awssdk.services.ssm.model.CancelMaintenanceWindowExecutionRequest"] }, {"name":"createActivation","parameterTypes":["java.util.function.Consumer"] }, {"name":"createActivation","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateActivationRequest"] }, {"name":"createAssociation","parameterTypes":["java.util.function.Consumer"] }, {"name":"createAssociation","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateAssociationRequest"] }, {"name":"createAssociationBatch","parameterTypes":["java.util.function.Consumer"] }, {"name":"createAssociationBatch","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateAssociationBatchRequest"] }, {"name":"createDocument","parameterTypes":["java.util.function.Consumer"] }, {"name":"createDocument","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateDocumentRequest"] }, {"name":"createMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"createMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateMaintenanceWindowRequest"] }, {"name":"createOpsItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"createOpsItem","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateOpsItemRequest"] }, {"name":"createOpsMetadata","parameterTypes":["java.util.function.Consumer"] }, {"name":"createOpsMetadata","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateOpsMetadataRequest"] }, {"name":"createPatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"createPatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreatePatchBaselineRequest"] }, {"name":"createResourceDataSync","parameterTypes":["java.util.function.Consumer"] }, {"name":"createResourceDataSync","parameterTypes":["software.amazon.awssdk.services.ssm.model.CreateResourceDataSyncRequest"] }, {"name":"deleteActivation","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteActivation","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteActivationRequest"] }, {"name":"deleteAssociation","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteAssociation","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteAssociationRequest"] }, {"name":"deleteDocument","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteDocument","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteDocumentRequest"] }, {"name":"deleteInventory","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteInventory","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteInventoryRequest"] }, {"name":"deleteMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteMaintenanceWindowRequest"] }, {"name":"deleteOpsItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteOpsItem","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteOpsItemRequest"] }, {"name":"deleteOpsMetadata","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteOpsMetadata","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteOpsMetadataRequest"] }, {"name":"deleteParameter","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteParameter","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteParameterRequest"] }, {"name":"deleteParameters","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteParameters","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteParametersRequest"] }, {"name":"deletePatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"deletePatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeletePatchBaselineRequest"] }, {"name":"deleteResourceDataSync","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteResourceDataSync","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteResourceDataSyncRequest"] }, {"name":"deleteResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"deleteResourcePolicy","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeleteResourcePolicyRequest"] }, {"name":"deregisterManagedInstance","parameterTypes":["java.util.function.Consumer"] }, {"name":"deregisterManagedInstance","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeregisterManagedInstanceRequest"] }, {"name":"deregisterPatchBaselineForPatchGroup","parameterTypes":["java.util.function.Consumer"] }, {"name":"deregisterPatchBaselineForPatchGroup","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeregisterPatchBaselineForPatchGroupRequest"] }, {"name":"deregisterTargetFromMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"deregisterTargetFromMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeregisterTargetFromMaintenanceWindowRequest"] }, {"name":"deregisterTaskFromMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"deregisterTaskFromMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.DeregisterTaskFromMaintenanceWindowRequest"] }, {"name":"describeActivations","parameterTypes":[] }, {"name":"describeActivations","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeActivations","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeActivationsRequest"] }, {"name":"describeActivationsPaginator","parameterTypes":[] }, {"name":"describeActivationsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeActivationsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeActivationsRequest"] }, {"name":"describeAssociation","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAssociation","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAssociationRequest"] }, {"name":"describeAssociationExecutionTargets","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAssociationExecutionTargets","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAssociationExecutionTargetsRequest"] }, {"name":"describeAssociationExecutionTargetsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAssociationExecutionTargetsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAssociationExecutionTargetsRequest"] }, {"name":"describeAssociationExecutions","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAssociationExecutions","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAssociationExecutionsRequest"] }, {"name":"describeAssociationExecutionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAssociationExecutionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAssociationExecutionsRequest"] }, {"name":"describeAutomationExecutions","parameterTypes":[] }, {"name":"describeAutomationExecutions","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAutomationExecutions","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAutomationExecutionsRequest"] }, {"name":"describeAutomationExecutionsPaginator","parameterTypes":[] }, {"name":"describeAutomationExecutionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAutomationExecutionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAutomationExecutionsRequest"] }, {"name":"describeAutomationStepExecutions","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAutomationStepExecutions","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAutomationStepExecutionsRequest"] }, {"name":"describeAutomationStepExecutionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAutomationStepExecutionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAutomationStepExecutionsRequest"] }, {"name":"describeAvailablePatches","parameterTypes":[] }, {"name":"describeAvailablePatches","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAvailablePatches","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAvailablePatchesRequest"] }, {"name":"describeAvailablePatchesPaginator","parameterTypes":[] }, {"name":"describeAvailablePatchesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeAvailablePatchesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeAvailablePatchesRequest"] }, {"name":"describeDocument","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeDocument","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeDocumentRequest"] }, {"name":"describeDocumentPermission","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeDocumentPermission","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeDocumentPermissionRequest"] }, {"name":"describeEffectiveInstanceAssociations","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeEffectiveInstanceAssociations","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeEffectiveInstanceAssociationsRequest"] }, {"name":"describeEffectiveInstanceAssociationsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeEffectiveInstanceAssociationsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeEffectiveInstanceAssociationsRequest"] }, {"name":"describeEffectivePatchesForPatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeEffectivePatchesForPatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeEffectivePatchesForPatchBaselineRequest"] }, {"name":"describeEffectivePatchesForPatchBaselinePaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeEffectivePatchesForPatchBaselinePaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeEffectivePatchesForPatchBaselineRequest"] }, {"name":"describeInstanceAssociationsStatus","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstanceAssociationsStatus","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstanceAssociationsStatusRequest"] }, {"name":"describeInstanceAssociationsStatusPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstanceAssociationsStatusPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstanceAssociationsStatusRequest"] }, {"name":"describeInstanceInformation","parameterTypes":[] }, {"name":"describeInstanceInformation","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstanceInformation","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstanceInformationRequest"] }, {"name":"describeInstanceInformationPaginator","parameterTypes":[] }, {"name":"describeInstanceInformationPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstanceInformationPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstanceInformationRequest"] }, {"name":"describeInstancePatchStates","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePatchStates","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePatchStatesRequest"] }, {"name":"describeInstancePatchStatesForPatchGroup","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePatchStatesForPatchGroup","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePatchStatesForPatchGroupRequest"] }, {"name":"describeInstancePatchStatesForPatchGroupPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePatchStatesForPatchGroupPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePatchStatesForPatchGroupRequest"] }, {"name":"describeInstancePatchStatesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePatchStatesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePatchStatesRequest"] }, {"name":"describeInstancePatches","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePatches","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePatchesRequest"] }, {"name":"describeInstancePatchesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePatchesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePatchesRequest"] }, {"name":"describeInstanceProperties","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstanceProperties","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePropertiesRequest"] }, {"name":"describeInstancePropertiesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInstancePropertiesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInstancePropertiesRequest"] }, {"name":"describeInventoryDeletions","parameterTypes":[] }, {"name":"describeInventoryDeletions","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInventoryDeletions","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInventoryDeletionsRequest"] }, {"name":"describeInventoryDeletionsPaginator","parameterTypes":[] }, {"name":"describeInventoryDeletionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeInventoryDeletionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeInventoryDeletionsRequest"] }, {"name":"describeMaintenanceWindowExecutionTaskInvocations","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowExecutionTaskInvocations","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowExecutionTaskInvocationsRequest"] }, {"name":"describeMaintenanceWindowExecutionTaskInvocationsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowExecutionTaskInvocationsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowExecutionTaskInvocationsRequest"] }, {"name":"describeMaintenanceWindowExecutionTasks","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowExecutionTasks","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowExecutionTasksRequest"] }, {"name":"describeMaintenanceWindowExecutionTasksPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowExecutionTasksPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowExecutionTasksRequest"] }, {"name":"describeMaintenanceWindowExecutions","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowExecutions","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowExecutionsRequest"] }, {"name":"describeMaintenanceWindowExecutionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowExecutionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowExecutionsRequest"] }, {"name":"describeMaintenanceWindowSchedule","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowSchedule","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowScheduleRequest"] }, {"name":"describeMaintenanceWindowSchedulePaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowSchedulePaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowScheduleRequest"] }, {"name":"describeMaintenanceWindowTargets","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowTargets","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowTargetsRequest"] }, {"name":"describeMaintenanceWindowTargetsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowTargetsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowTargetsRequest"] }, {"name":"describeMaintenanceWindowTasks","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowTasks","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowTasksRequest"] }, {"name":"describeMaintenanceWindowTasksPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowTasksPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowTasksRequest"] }, {"name":"describeMaintenanceWindows","parameterTypes":[] }, {"name":"describeMaintenanceWindows","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindows","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowsRequest"] }, {"name":"describeMaintenanceWindowsForTarget","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowsForTarget","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowsForTargetRequest"] }, {"name":"describeMaintenanceWindowsForTargetPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowsForTargetPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowsForTargetRequest"] }, {"name":"describeMaintenanceWindowsPaginator","parameterTypes":[] }, {"name":"describeMaintenanceWindowsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeMaintenanceWindowsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeMaintenanceWindowsRequest"] }, {"name":"describeOpsItems","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeOpsItems","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeOpsItemsRequest"] }, {"name":"describeOpsItemsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeOpsItemsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeOpsItemsRequest"] }, {"name":"describeParameters","parameterTypes":[] }, {"name":"describeParameters","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeParameters","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeParametersRequest"] }, {"name":"describeParametersPaginator","parameterTypes":[] }, {"name":"describeParametersPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeParametersPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeParametersRequest"] }, {"name":"describePatchBaselines","parameterTypes":[] }, {"name":"describePatchBaselines","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchBaselines","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchBaselinesRequest"] }, {"name":"describePatchBaselinesPaginator","parameterTypes":[] }, {"name":"describePatchBaselinesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchBaselinesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchBaselinesRequest"] }, {"name":"describePatchGroupState","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchGroupState","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchGroupStateRequest"] }, {"name":"describePatchGroups","parameterTypes":[] }, {"name":"describePatchGroups","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchGroups","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchGroupsRequest"] }, {"name":"describePatchGroupsPaginator","parameterTypes":[] }, {"name":"describePatchGroupsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchGroupsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchGroupsRequest"] }, {"name":"describePatchProperties","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchProperties","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchPropertiesRequest"] }, {"name":"describePatchPropertiesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describePatchPropertiesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribePatchPropertiesRequest"] }, {"name":"describeSessions","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeSessions","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeSessionsRequest"] }, {"name":"describeSessionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"describeSessionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.DescribeSessionsRequest"] }, {"name":"disassociateOpsItemRelatedItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"disassociateOpsItemRelatedItem","parameterTypes":["software.amazon.awssdk.services.ssm.model.DisassociateOpsItemRelatedItemRequest"] }, {"name":"getAutomationExecution","parameterTypes":["java.util.function.Consumer"] }, {"name":"getAutomationExecution","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetAutomationExecutionRequest"] }, {"name":"getCalendarState","parameterTypes":["java.util.function.Consumer"] }, {"name":"getCalendarState","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetCalendarStateRequest"] }, {"name":"getCommandInvocation","parameterTypes":["java.util.function.Consumer"] }, {"name":"getCommandInvocation","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetCommandInvocationRequest"] }, {"name":"getConnectionStatus","parameterTypes":["java.util.function.Consumer"] }, {"name":"getConnectionStatus","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetConnectionStatusRequest"] }, {"name":"getDefaultPatchBaseline","parameterTypes":[] }, {"name":"getDefaultPatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"getDefaultPatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetDefaultPatchBaselineRequest"] }, {"name":"getDeployablePatchSnapshotForInstance","parameterTypes":["java.util.function.Consumer"] }, {"name":"getDeployablePatchSnapshotForInstance","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetDeployablePatchSnapshotForInstanceRequest"] }, {"name":"getDocument","parameterTypes":["java.util.function.Consumer"] }, {"name":"getDocument","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetDocumentRequest"] }, {"name":"getInventory","parameterTypes":[] }, {"name":"getInventory","parameterTypes":["java.util.function.Consumer"] }, {"name":"getInventory","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetInventoryRequest"] }, {"name":"getInventoryPaginator","parameterTypes":[] }, {"name":"getInventoryPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"getInventoryPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetInventoryRequest"] }, {"name":"getInventorySchema","parameterTypes":[] }, {"name":"getInventorySchema","parameterTypes":["java.util.function.Consumer"] }, {"name":"getInventorySchema","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetInventorySchemaRequest"] }, {"name":"getInventorySchemaPaginator","parameterTypes":[] }, {"name":"getInventorySchemaPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"getInventorySchemaPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetInventorySchemaRequest"] }, {"name":"getMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"getMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetMaintenanceWindowRequest"] }, {"name":"getMaintenanceWindowExecution","parameterTypes":["java.util.function.Consumer"] }, {"name":"getMaintenanceWindowExecution","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetMaintenanceWindowExecutionRequest"] }, {"name":"getMaintenanceWindowExecutionTask","parameterTypes":["java.util.function.Consumer"] }, {"name":"getMaintenanceWindowExecutionTask","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetMaintenanceWindowExecutionTaskRequest"] }, {"name":"getMaintenanceWindowExecutionTaskInvocation","parameterTypes":["java.util.function.Consumer"] }, {"name":"getMaintenanceWindowExecutionTaskInvocation","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetMaintenanceWindowExecutionTaskInvocationRequest"] }, {"name":"getMaintenanceWindowTask","parameterTypes":["java.util.function.Consumer"] }, {"name":"getMaintenanceWindowTask","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetMaintenanceWindowTaskRequest"] }, {"name":"getOpsItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"getOpsItem","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetOpsItemRequest"] }, {"name":"getOpsMetadata","parameterTypes":["java.util.function.Consumer"] }, {"name":"getOpsMetadata","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetOpsMetadataRequest"] }, {"name":"getOpsSummary","parameterTypes":["java.util.function.Consumer"] }, {"name":"getOpsSummary","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetOpsSummaryRequest"] }, {"name":"getOpsSummaryPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"getOpsSummaryPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetOpsSummaryRequest"] }, {"name":"getParameter","parameterTypes":["java.util.function.Consumer"] }, {"name":"getParameter","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetParameterRequest"] }, {"name":"getParameterHistory","parameterTypes":["java.util.function.Consumer"] }, {"name":"getParameterHistory","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetParameterHistoryRequest"] }, {"name":"getParameterHistoryPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"getParameterHistoryPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetParameterHistoryRequest"] }, {"name":"getParameters","parameterTypes":["java.util.function.Consumer"] }, {"name":"getParameters","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetParametersRequest"] }, {"name":"getParametersByPath","parameterTypes":["java.util.function.Consumer"] }, {"name":"getParametersByPath","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest"] }, {"name":"getParametersByPathPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"getParametersByPathPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest"] }, {"name":"getPatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"getPatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetPatchBaselineRequest"] }, {"name":"getPatchBaselineForPatchGroup","parameterTypes":["java.util.function.Consumer"] }, {"name":"getPatchBaselineForPatchGroup","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetPatchBaselineForPatchGroupRequest"] }, {"name":"getResourcePolicies","parameterTypes":["java.util.function.Consumer"] }, {"name":"getResourcePolicies","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetResourcePoliciesRequest"] }, {"name":"getResourcePoliciesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"getResourcePoliciesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetResourcePoliciesRequest"] }, {"name":"getServiceSetting","parameterTypes":["java.util.function.Consumer"] }, {"name":"getServiceSetting","parameterTypes":["software.amazon.awssdk.services.ssm.model.GetServiceSettingRequest"] }, {"name":"labelParameterVersion","parameterTypes":["java.util.function.Consumer"] }, {"name":"labelParameterVersion","parameterTypes":["software.amazon.awssdk.services.ssm.model.LabelParameterVersionRequest"] }, {"name":"listAssociationVersions","parameterTypes":["java.util.function.Consumer"] }, {"name":"listAssociationVersions","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListAssociationVersionsRequest"] }, {"name":"listAssociationVersionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listAssociationVersionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListAssociationVersionsRequest"] }, {"name":"listAssociations","parameterTypes":[] }, {"name":"listAssociations","parameterTypes":["java.util.function.Consumer"] }, {"name":"listAssociations","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListAssociationsRequest"] }, {"name":"listAssociationsPaginator","parameterTypes":[] }, {"name":"listAssociationsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listAssociationsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListAssociationsRequest"] }, {"name":"listCommandInvocations","parameterTypes":[] }, {"name":"listCommandInvocations","parameterTypes":["java.util.function.Consumer"] }, {"name":"listCommandInvocations","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListCommandInvocationsRequest"] }, {"name":"listCommandInvocationsPaginator","parameterTypes":[] }, {"name":"listCommandInvocationsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listCommandInvocationsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListCommandInvocationsRequest"] }, {"name":"listCommands","parameterTypes":[] }, {"name":"listCommands","parameterTypes":["java.util.function.Consumer"] }, {"name":"listCommands","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListCommandsRequest"] }, {"name":"listCommandsPaginator","parameterTypes":[] }, {"name":"listCommandsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listCommandsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListCommandsRequest"] }, {"name":"listComplianceItems","parameterTypes":["java.util.function.Consumer"] }, {"name":"listComplianceItems","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListComplianceItemsRequest"] }, {"name":"listComplianceItemsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listComplianceItemsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListComplianceItemsRequest"] }, {"name":"listComplianceSummaries","parameterTypes":[] }, {"name":"listComplianceSummaries","parameterTypes":["java.util.function.Consumer"] }, {"name":"listComplianceSummaries","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListComplianceSummariesRequest"] }, {"name":"listComplianceSummariesPaginator","parameterTypes":[] }, {"name":"listComplianceSummariesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listComplianceSummariesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListComplianceSummariesRequest"] }, {"name":"listDocumentMetadataHistory","parameterTypes":["java.util.function.Consumer"] }, {"name":"listDocumentMetadataHistory","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListDocumentMetadataHistoryRequest"] }, {"name":"listDocumentVersions","parameterTypes":["java.util.function.Consumer"] }, {"name":"listDocumentVersions","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListDocumentVersionsRequest"] }, {"name":"listDocumentVersionsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listDocumentVersionsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListDocumentVersionsRequest"] }, {"name":"listDocuments","parameterTypes":[] }, {"name":"listDocuments","parameterTypes":["java.util.function.Consumer"] }, {"name":"listDocuments","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListDocumentsRequest"] }, {"name":"listDocumentsPaginator","parameterTypes":[] }, {"name":"listDocumentsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listDocumentsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListDocumentsRequest"] }, {"name":"listInventoryEntries","parameterTypes":["java.util.function.Consumer"] }, {"name":"listInventoryEntries","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListInventoryEntriesRequest"] }, {"name":"listOpsItemEvents","parameterTypes":["java.util.function.Consumer"] }, {"name":"listOpsItemEvents","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListOpsItemEventsRequest"] }, {"name":"listOpsItemEventsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listOpsItemEventsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListOpsItemEventsRequest"] }, {"name":"listOpsItemRelatedItems","parameterTypes":["java.util.function.Consumer"] }, {"name":"listOpsItemRelatedItems","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListOpsItemRelatedItemsRequest"] }, {"name":"listOpsItemRelatedItemsPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listOpsItemRelatedItemsPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListOpsItemRelatedItemsRequest"] }, {"name":"listOpsMetadata","parameterTypes":["java.util.function.Consumer"] }, {"name":"listOpsMetadata","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListOpsMetadataRequest"] }, {"name":"listOpsMetadataPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listOpsMetadataPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListOpsMetadataRequest"] }, {"name":"listResourceComplianceSummaries","parameterTypes":[] }, {"name":"listResourceComplianceSummaries","parameterTypes":["java.util.function.Consumer"] }, {"name":"listResourceComplianceSummaries","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListResourceComplianceSummariesRequest"] }, {"name":"listResourceComplianceSummariesPaginator","parameterTypes":[] }, {"name":"listResourceComplianceSummariesPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listResourceComplianceSummariesPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListResourceComplianceSummariesRequest"] }, {"name":"listResourceDataSync","parameterTypes":[] }, {"name":"listResourceDataSync","parameterTypes":["java.util.function.Consumer"] }, {"name":"listResourceDataSync","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListResourceDataSyncRequest"] }, {"name":"listResourceDataSyncPaginator","parameterTypes":[] }, {"name":"listResourceDataSyncPaginator","parameterTypes":["java.util.function.Consumer"] }, {"name":"listResourceDataSyncPaginator","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListResourceDataSyncRequest"] }, {"name":"listTagsForResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"listTagsForResource","parameterTypes":["software.amazon.awssdk.services.ssm.model.ListTagsForResourceRequest"] }, {"name":"modifyDocumentPermission","parameterTypes":["java.util.function.Consumer"] }, {"name":"modifyDocumentPermission","parameterTypes":["software.amazon.awssdk.services.ssm.model.ModifyDocumentPermissionRequest"] }, {"name":"putComplianceItems","parameterTypes":["java.util.function.Consumer"] }, {"name":"putComplianceItems","parameterTypes":["software.amazon.awssdk.services.ssm.model.PutComplianceItemsRequest"] }, {"name":"putInventory","parameterTypes":["java.util.function.Consumer"] }, {"name":"putInventory","parameterTypes":["software.amazon.awssdk.services.ssm.model.PutInventoryRequest"] }, {"name":"putParameter","parameterTypes":["java.util.function.Consumer"] }, {"name":"putParameter","parameterTypes":["software.amazon.awssdk.services.ssm.model.PutParameterRequest"] }, {"name":"putResourcePolicy","parameterTypes":["java.util.function.Consumer"] }, {"name":"putResourcePolicy","parameterTypes":["software.amazon.awssdk.services.ssm.model.PutResourcePolicyRequest"] }, {"name":"registerDefaultPatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"registerDefaultPatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.RegisterDefaultPatchBaselineRequest"] }, {"name":"registerPatchBaselineForPatchGroup","parameterTypes":["java.util.function.Consumer"] }, {"name":"registerPatchBaselineForPatchGroup","parameterTypes":["software.amazon.awssdk.services.ssm.model.RegisterPatchBaselineForPatchGroupRequest"] }, {"name":"registerTargetWithMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"registerTargetWithMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.RegisterTargetWithMaintenanceWindowRequest"] }, {"name":"registerTaskWithMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"registerTaskWithMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.RegisterTaskWithMaintenanceWindowRequest"] }, {"name":"removeTagsFromResource","parameterTypes":["java.util.function.Consumer"] }, {"name":"removeTagsFromResource","parameterTypes":["software.amazon.awssdk.services.ssm.model.RemoveTagsFromResourceRequest"] }, {"name":"resetServiceSetting","parameterTypes":["java.util.function.Consumer"] }, {"name":"resetServiceSetting","parameterTypes":["software.amazon.awssdk.services.ssm.model.ResetServiceSettingRequest"] }, {"name":"resumeSession","parameterTypes":["java.util.function.Consumer"] }, {"name":"resumeSession","parameterTypes":["software.amazon.awssdk.services.ssm.model.ResumeSessionRequest"] }, {"name":"sendAutomationSignal","parameterTypes":["java.util.function.Consumer"] }, {"name":"sendAutomationSignal","parameterTypes":["software.amazon.awssdk.services.ssm.model.SendAutomationSignalRequest"] }, {"name":"sendCommand","parameterTypes":["java.util.function.Consumer"] }, {"name":"sendCommand","parameterTypes":["software.amazon.awssdk.services.ssm.model.SendCommandRequest"] }, {"name":"serviceClientConfiguration","parameterTypes":[] }, {"name":"startAssociationsOnce","parameterTypes":["java.util.function.Consumer"] }, {"name":"startAssociationsOnce","parameterTypes":["software.amazon.awssdk.services.ssm.model.StartAssociationsOnceRequest"] }, {"name":"startAutomationExecution","parameterTypes":["java.util.function.Consumer"] }, {"name":"startAutomationExecution","parameterTypes":["software.amazon.awssdk.services.ssm.model.StartAutomationExecutionRequest"] }, {"name":"startChangeRequestExecution","parameterTypes":["java.util.function.Consumer"] }, {"name":"startChangeRequestExecution","parameterTypes":["software.amazon.awssdk.services.ssm.model.StartChangeRequestExecutionRequest"] }, {"name":"startSession","parameterTypes":["java.util.function.Consumer"] }, {"name":"startSession","parameterTypes":["software.amazon.awssdk.services.ssm.model.StartSessionRequest"] }, {"name":"stopAutomationExecution","parameterTypes":["java.util.function.Consumer"] }, {"name":"stopAutomationExecution","parameterTypes":["software.amazon.awssdk.services.ssm.model.StopAutomationExecutionRequest"] }, {"name":"terminateSession","parameterTypes":["java.util.function.Consumer"] }, {"name":"terminateSession","parameterTypes":["software.amazon.awssdk.services.ssm.model.TerminateSessionRequest"] }, {"name":"unlabelParameterVersion","parameterTypes":["java.util.function.Consumer"] }, {"name":"unlabelParameterVersion","parameterTypes":["software.amazon.awssdk.services.ssm.model.UnlabelParameterVersionRequest"] }, {"name":"updateAssociation","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateAssociation","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateAssociationRequest"] }, {"name":"updateAssociationStatus","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateAssociationStatus","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateAssociationStatusRequest"] }, {"name":"updateDocument","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateDocument","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateDocumentRequest"] }, {"name":"updateDocumentDefaultVersion","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateDocumentDefaultVersion","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateDocumentDefaultVersionRequest"] }, {"name":"updateDocumentMetadata","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateDocumentMetadata","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateDocumentMetadataRequest"] }, {"name":"updateMaintenanceWindow","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateMaintenanceWindow","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateMaintenanceWindowRequest"] }, {"name":"updateMaintenanceWindowTarget","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateMaintenanceWindowTarget","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateMaintenanceWindowTargetRequest"] }, {"name":"updateMaintenanceWindowTask","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateMaintenanceWindowTask","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateMaintenanceWindowTaskRequest"] }, {"name":"updateManagedInstanceRole","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateManagedInstanceRole","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateManagedInstanceRoleRequest"] }, {"name":"updateOpsItem","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateOpsItem","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateOpsItemRequest"] }, {"name":"updateOpsMetadata","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateOpsMetadata","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateOpsMetadataRequest"] }, {"name":"updatePatchBaseline","parameterTypes":["java.util.function.Consumer"] }, {"name":"updatePatchBaseline","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdatePatchBaselineRequest"] }, {"name":"updateResourceDataSync","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateResourceDataSync","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateResourceDataSyncRequest"] }, {"name":"updateServiceSetting","parameterTypes":["java.util.function.Consumer"] }, {"name":"updateServiceSetting","parameterTypes":["software.amazon.awssdk.services.ssm.model.UpdateServiceSettingRequest"] }, {"name":"waiter","parameterTypes":[] }] +}, +{ + "name":"software.amazon.awssdk.utils.SdkAutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"close","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.transform.Base64Transformer", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.transform.BasicTransformer", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.transform.JsonTransformer", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.parameters.transform.ObjectToDeserialize", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setBar","parameterTypes":["int"] }, {"name":"setBaz","parameterTypes":["long"] }, {"name":"setFoo","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"sun.reflect.ReflectionFactory", + "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] +} +] diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerTest.java deleted file mode 100644 index b84fcf743..000000000 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerTest.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.parameters; - -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatRuntimeException; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import org.junit.jupiter.api.Test; -import software.amazon.lambda.powertools.parameters.cache.CacheManager; -import software.amazon.lambda.powertools.parameters.internal.CustomProvider; -import software.amazon.lambda.powertools.parameters.transform.TransformationManager; - -public class ParamManagerTest { - - @Test - public void testGetCacheManager() { - - // Act - CacheManager cacheManager = ParamManager.getCacheManager(); - - // Assert - assertNotNull(cacheManager); - } - - @Test - public void testGetTransformationManager() { - - // Act - TransformationManager transformationManager = ParamManager.getTransformationManager(); - - // Assert - assertNotNull(transformationManager); - } - - @Test - public void testCreateProvider() { - - // Act - CustomProvider customProvider = ParamManager.createProvider(CustomProvider.class); - - // Assert - assertNotNull(customProvider); - } - - @Test - public void testCreateUninstanciableProvider_throwsException() { - - // Act & Assert - assertThatRuntimeException().isThrownBy(() -> ParamManager.createProvider(BaseProvider.class)); - } - - @Test - public void testGetProviderWithProviderClass() { - - // Act - SecretsProvider secretsProvider = ParamManager.getProvider(SecretsProvider.class); - - // Assert - assertNotNull(secretsProvider); - } - - @Test - public void testGetProviderWithProviderClass_throwsException() { - - // Act & Assert - assertThatIllegalStateException().isThrownBy(() -> ParamManager.getProvider(null)); - } - - @Test - public void testGetSecretsProvider_withoutParameter_shouldCreateDefaultClient() { - - // Act - SecretsProvider secretsProvider = ParamManager.getSecretsProvider(); - - // Assert - assertNotNull(secretsProvider); - assertNotNull(secretsProvider.getClient()); - } - - @Test - public void testGetSSMProvider_withoutParameter_shouldCreateDefaultClient() { - - // Act - SSMProvider ssmProvider = ParamManager.getSsmProvider(); - - // Assert - assertNotNull(ssmProvider); - assertNotNull(ssmProvider.getClient()); - } - - @Test - public void testGetDynamoDBProvider_requireOtherParameters_throwException() { - - // Act & Assert - assertThatIllegalArgumentException().isThrownBy(() -> ParamManager.getProvider(DynamoDbProvider.class)); - } - - @Test - public void testGetAppConfigProvider_requireOtherParameters_throwException() { - - // Act & Assert - assertThatIllegalArgumentException().isThrownBy(() -> ParamManager.getProvider(AppConfigProvider.class)); - } -} diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspectTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspectTest.java deleted file mode 100644 index 2c246336b..000000000 --- a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/internal/LambdaParametersAspectTest.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.parameters.internal; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import software.amazon.lambda.powertools.parameters.Param; -import software.amazon.lambda.powertools.parameters.ParamManager; -import software.amazon.lambda.powertools.parameters.SSMProvider; -import software.amazon.lambda.powertools.parameters.exception.TransformationException; -import software.amazon.lambda.powertools.parameters.transform.Base64Transformer; -import software.amazon.lambda.powertools.parameters.transform.JsonTransformer; -import software.amazon.lambda.powertools.parameters.transform.ObjectToDeserialize; - -public class LambdaParametersAspectTest { - - @Mock - private SSMProvider defaultProvider; - - @Param(key = "/default") - private String defaultValue; - - @Param(key = "/simple", provider = CustomProvider.class) - private String param; - - @Param(key = "/base64", provider = CustomProvider.class, transformer = Base64Transformer.class) - private String basicTransform; - - @Param(key = "/json", provider = CustomProvider.class, transformer = JsonTransformer.class) - private ObjectToDeserialize complexTransform; - - @Param(key = "/json", provider = CustomProvider.class, transformer = JsonTransformer.class) - private AnotherObject wrongTransform; - - @BeforeEach - public void init() { - openMocks(this); - } - - @Test - public void testDefault_ShouldUseSSMProvider() { - try (MockedStatic mocked = mockStatic(ParamManager.class)) { - mocked.when(() -> ParamManager.getProvider(SSMProvider.class)).thenReturn(defaultProvider); - when(defaultProvider.get("/default")).thenReturn("value"); - - assertThat(defaultValue).isEqualTo("value"); - mocked.verify(() -> ParamManager.getProvider(SSMProvider.class), times(1)); - verify(defaultProvider, times(1)).get("/default"); - - mocked.reset(); - } - } - - @Test - public void testSimple() { - assertThat(param).isEqualTo("value"); - } - - @Test - public void testWithBasicTransform() { - assertThat(basicTransform).isEqualTo("value"); - } - - @Test - public void testWithComplexTransform() { - assertThat(complexTransform) - .isInstanceOf(ObjectToDeserialize.class) - .matches( - o -> o.getFoo().equals("Foo") && - o.getBar() == 42 && - o.getBaz() == 123456789); - } - - @Test - public void testWithComplexTransformWrongTargetClass_ShouldThrowException() { - assertThatExceptionOfType(TransformationException.class) - .isThrownBy(() -> - { - AnotherObject obj = wrongTransform; - }); - } - -} diff --git a/powertools-serialization/pom.xml b/powertools-serialization/pom.xml index 70280a0b8..ec8acab9e 100644 --- a/powertools-serialization/pom.xml +++ b/powertools-serialization/pom.xml @@ -21,38 +21,14 @@ powertools-parent software.amazon.lambda - 1.19.0-SNAPSHOT + 2.0.0-RC1 powertools-serialization jar - Powertools for AWS Lambda (Java) library Serialization Utilities - - - - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - + Powertools for AWS Lambda (Java) - Serialization Utilities + Utilities for JSON serialization used across the project. @@ -64,8 +40,8 @@ aws-lambda-java-events - org.apache.logging.log4j - log4j-slf4j2-impl + org.slf4j + slf4j-api com.fasterxml.jackson.core @@ -78,6 +54,11 @@ junit-jupiter-api test + + org.slf4j + slf4j-simple + test + org.assertj assertj-core @@ -89,8 +70,102 @@ test - + + + generate-graalvm-files + + + org.mockito + mockito-subclass + 5.17.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-serialization,experimental-class-define-support + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + 5.17.0 + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.2 + true + + + test-native + + test + + test + + + + + true + Standard + + powertools-serialization + + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + + + + + + + + + + + src/main/resources + + dev.aspectj @@ -100,11 +175,7 @@ true - - org.apache.maven.plugins - maven-checkstyle-plugin - - \ No newline at end of file + diff --git a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java index baa6a0367..fc0f083e5 100644 --- a/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java +++ b/powertools-serialization/src/main/java/software/amazon/lambda/powertools/utilities/JsonConfig.java @@ -14,29 +14,51 @@ package software.amazon.lambda.powertools.utilities; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; import io.burt.jmespath.JmesPath; import io.burt.jmespath.RuntimeConfiguration; import io.burt.jmespath.function.BaseFunction; import io.burt.jmespath.function.FunctionRegistry; import io.burt.jmespath.jackson.JacksonRuntime; +import java.util.function.Supplier; import software.amazon.lambda.powertools.utilities.jmespath.Base64Function; import software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction; import software.amazon.lambda.powertools.utilities.jmespath.JsonFunction; -public class JsonConfig { - private static final ThreadLocal om = ThreadLocal.withInitial(ObjectMapper::new); +public final class JsonConfig { + + private static final Supplier objectMapperSupplier = () -> JsonMapper.builder() + // Don't throw an exception when json has extra fields you are not serializing on. + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + // Ignore null values when writing json. + .serializationInclusion(JsonInclude.Include.NON_NULL) + // Write times as a String instead of a Long so its human-readable. + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + // Sort fields in alphabetical order + .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true) + .build(); + + private static final ThreadLocal om = ThreadLocal.withInitial(objectMapperSupplier); + private final FunctionRegistry defaultFunctions = FunctionRegistry.defaultRegistry(); + private final FunctionRegistry customFunctions = defaultFunctions.extend( new Base64Function(), new Base64GZipFunction(), new JsonFunction() ); + private final RuntimeConfiguration configuration = new RuntimeConfiguration.Builder() .withSilentTypeErrors(true) .withFunctionRegistry(customFunctions) .build(); + private JmesPath jmesPath = new JacksonRuntime(configuration, getObjectMapper()); private JsonConfig() { @@ -82,6 +104,6 @@ public void addFunction(T function) { } private static class ConfigHolder { - private final static JsonConfig instance = new JsonConfig(); + private static final JsonConfig instance = new JsonConfig(); } } diff --git a/powertools-serialization/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-serialization/jni-config.json b/powertools-serialization/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-serialization/jni-config.json new file mode 100644 index 000000000..079c02a4d --- /dev/null +++ b/powertools-serialization/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-serialization/jni-config.json @@ -0,0 +1,18 @@ +[ +{ + "name":"java.lang.String", + "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"org.apache.maven.surefire.booter.ForkedBooter", + "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"sun.management.VMManagementImpl", + "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] +} +] diff --git a/powertools-serialization/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-serialization/reflect-config.json b/powertools-serialization/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-serialization/reflect-config.json new file mode 100644 index 000000000..1a4f89735 --- /dev/null +++ b/powertools-serialization/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-serialization/reflect-config.json @@ -0,0 +1,470 @@ +[ +{ + "name":"[B" +}, +{ + "name":"[Lcom.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers;" +}, +{ + "name":"[Lcom.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.KeyDeserializers;" +}, +{ + "name":"[Lcom.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.ValueInstantiators;" +}, +{ + "name":"[Lcom.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ser.Serializers;" +}, +{ + "name":"com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7HandlersImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setBody","parameterTypes":["java.lang.String"] }, {"name":"setHeaders","parameterTypes":["java.util.Map"] }, {"name":"setHttpMethod","parameterTypes":["java.lang.String"] }, {"name":"setIsBase64Encoded","parameterTypes":["java.lang.Boolean"] }, {"name":"setPath","parameterTypes":["java.lang.String"] }, {"name":"setPathParameters","parameterTypes":["java.util.Map"] }, {"name":"setQueryStringParameters","parameterTypes":["java.util.Map"] }, {"name":"setRequestContext","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$ProxyRequestContext"] }, {"name":"setResource","parameterTypes":["java.lang.String"] }, {"name":"setStageVariables","parameterTypes":["java.util.Map"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$ProxyRequestContext", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setAccountId","parameterTypes":["java.lang.String"] }, {"name":"setApiId","parameterTypes":["java.lang.String"] }, {"name":"setHttpMethod","parameterTypes":["java.lang.String"] }, {"name":"setIdentity","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$RequestIdentity"] }, {"name":"setPath","parameterTypes":["java.lang.String"] }, {"name":"setProtocol","parameterTypes":["java.lang.String"] }, {"name":"setRequestId","parameterTypes":["java.lang.String"] }, {"name":"setRequestTime","parameterTypes":["java.lang.String"] }, {"name":"setRequestTimeEpoch","parameterTypes":["java.lang.Long"] }, {"name":"setResourceId","parameterTypes":["java.lang.String"] }, {"name":"setResourcePath","parameterTypes":["java.lang.String"] }, {"name":"setStage","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$RequestIdentity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setAccessKey","parameterTypes":["java.lang.String"] }, {"name":"setAccountId","parameterTypes":["java.lang.String"] }, {"name":"setCaller","parameterTypes":["java.lang.String"] }, {"name":"setCognitoAuthenticationProvider","parameterTypes":["java.lang.String"] }, {"name":"setCognitoAuthenticationType","parameterTypes":["java.lang.String"] }, {"name":"setCognitoIdentityId","parameterTypes":["java.lang.String"] }, {"name":"setCognitoIdentityPoolId","parameterTypes":["java.lang.String"] }, {"name":"setSourceIp","parameterTypes":["java.lang.String"] }, {"name":"setUser","parameterTypes":["java.lang.String"] }, {"name":"setUserAgent","parameterTypes":["java.lang.String"] }, {"name":"setUserArn","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setBody","parameterTypes":["java.lang.String"] }, {"name":"setCookies","parameterTypes":["java.util.List"] }, {"name":"setHeaders","parameterTypes":["java.util.Map"] }, {"name":"setIsBase64Encoded","parameterTypes":["boolean"] }, {"name":"setPathParameters","parameterTypes":["java.util.Map"] }, {"name":"setQueryStringParameters","parameterTypes":["java.util.Map"] }, {"name":"setRawPath","parameterTypes":["java.lang.String"] }, {"name":"setRawQueryString","parameterTypes":["java.lang.String"] }, {"name":"setRequestContext","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext"] }, {"name":"setRouteKey","parameterTypes":["java.lang.String"] }, {"name":"setStageVariables","parameterTypes":["java.util.Map"] }, {"name":"setVersion","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setAccountId","parameterTypes":["java.lang.String"] }, {"name":"setApiId","parameterTypes":["java.lang.String"] }, {"name":"setDomainName","parameterTypes":["java.lang.String"] }, {"name":"setDomainPrefix","parameterTypes":["java.lang.String"] }, {"name":"setHttp","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext$Http"] }, {"name":"setRouteKey","parameterTypes":["java.lang.String"] }, {"name":"setStage","parameterTypes":["java.lang.String"] }, {"name":"setTime","parameterTypes":["java.lang.String"] }, {"name":"setTimeEpoch","parameterTypes":["long"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext$Authorizer", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext$Authorizer$JWT", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext$CognitoIdentity", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext$Http", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setMethod","parameterTypes":["java.lang.String"] }, {"name":"setPath","parameterTypes":["java.lang.String"] }, {"name":"setProtocol","parameterTypes":["java.lang.String"] }, {"name":"setSourceIp","parameterTypes":["java.lang.String"] }, {"name":"setUserAgent","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent$RequestContext$IAM", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.ActiveMQEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setEventSource","parameterTypes":["java.lang.String"] }, {"name":"setEventSourceArn","parameterTypes":["java.lang.String"] }, {"name":"setMessages","parameterTypes":["java.util.List"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.ActiveMQEvent$ActiveMQMessage", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setBrokerInTime","parameterTypes":["long"] }, {"name":"setBrokerOutTime","parameterTypes":["long"] }, {"name":"setData","parameterTypes":["java.lang.String"] }, {"name":"setDeliveryMode","parameterTypes":["int"] }, {"name":"setDestination","parameterTypes":["com.amazonaws.services.lambda.runtime.events.ActiveMQEvent$Destination"] }, {"name":"setExpiration","parameterTypes":["long"] }, {"name":"setMessageID","parameterTypes":["java.lang.String"] }, {"name":"setMessageType","parameterTypes":["java.lang.String"] }, {"name":"setPriority","parameterTypes":["int"] }, {"name":"setProperties","parameterTypes":["java.util.Map"] }, {"name":"setRedelivered","parameterTypes":["boolean"] }, {"name":"setReplyTo","parameterTypes":["java.lang.String"] }, {"name":"setTimestamp","parameterTypes":["long"] }, {"name":"setType","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.ActiveMQEvent$Destination", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setPhysicalName","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setBody","parameterTypes":["java.lang.String"] }, {"name":"setHeaders","parameterTypes":["java.util.Map"] }, {"name":"setHttpMethod","parameterTypes":["java.lang.String"] }, {"name":"setIsBase64Encoded","parameterTypes":["boolean"] }, {"name":"setPath","parameterTypes":["java.lang.String"] }, {"name":"setQueryStringParameters","parameterTypes":["java.util.Map"] }, {"name":"setRequestContext","parameterTypes":["com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent$RequestContext"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent$Elb", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setTargetGroupArn","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent$RequestContext", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setElb","parameterTypes":["com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent$Elb"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setLogicalResourceId","parameterTypes":["java.lang.String"] }, {"name":"setOldResourceProperties","parameterTypes":["java.util.Map"] }, {"name":"setRequestId","parameterTypes":["java.lang.String"] }, {"name":"setRequestType","parameterTypes":["java.lang.String"] }, {"name":"setResourceProperties","parameterTypes":["java.util.Map"] }, {"name":"setResourceType","parameterTypes":["java.lang.String"] }, {"name":"setServiceToken","parameterTypes":["java.lang.String"] }, {"name":"setStackId","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setAwsLogs","parameterTypes":["com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent$AWSLogs"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent$AWSLogs", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setData","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KafkaEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setBootstrapServers","parameterTypes":["java.lang.String"] }, {"name":"setEventSource","parameterTypes":["java.lang.String"] }, {"name":"setEventSourceArn","parameterTypes":["java.lang.String"] }, {"name":"setRecords","parameterTypes":["java.util.Map"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KafkaEvent$KafkaEventRecord", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setOffset","parameterTypes":["long"] }, {"name":"setPartition","parameterTypes":["int"] }, {"name":"setTimestamp","parameterTypes":["long"] }, {"name":"setTimestampType","parameterTypes":["java.lang.String"] }, {"name":"setTopic","parameterTypes":["java.lang.String"] }, {"name":"setValue","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setApplicationArn","parameterTypes":["java.lang.String"] }, {"name":"setInvocationId","parameterTypes":["java.lang.String"] }, {"name":"setRecords","parameterTypes":["java.util.List"] }, {"name":"setStreamArn","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent$Record", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setData","parameterTypes":["java.nio.ByteBuffer"] }, {"name":"setKinesisFirehoseRecordMetadata","parameterTypes":["com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent$Record$KinesisFirehoseRecordMetadata"] }, {"name":"setRecordId","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent$Record$KinesisFirehoseRecordMetadata", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setApproximateArrivalTimestamp","parameterTypes":["java.lang.Long"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setApplicationArn","parameterTypes":["java.lang.String"] }, {"name":"setInvocationId","parameterTypes":["java.lang.String"] }, {"name":"setRecords","parameterTypes":["java.util.List"] }, {"name":"setStreamArn","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent$Record", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setData","parameterTypes":["java.nio.ByteBuffer"] }, {"name":"setKinesisStreamRecordMetadata","parameterTypes":["com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent$Record$KinesisStreamRecordMetadata"] }, {"name":"setRecordId","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent$Record$KinesisStreamRecordMetadata", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setApproximateArrivalTimestamp","parameterTypes":["java.lang.Long"] }, {"name":"setPartitionKey","parameterTypes":["java.lang.String"] }, {"name":"setSequenceNumber","parameterTypes":["java.lang.String"] }, {"name":"setShardId","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KinesisEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setRecords","parameterTypes":["java.util.List"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KinesisEvent$KinesisEventRecord", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setAwsRegion","parameterTypes":["java.lang.String"] }, {"name":"setEventID","parameterTypes":["java.lang.String"] }, {"name":"setEventName","parameterTypes":["java.lang.String"] }, {"name":"setEventSource","parameterTypes":["java.lang.String"] }, {"name":"setEventSourceARN","parameterTypes":["java.lang.String"] }, {"name":"setEventVersion","parameterTypes":["java.lang.String"] }, {"name":"setInvokeIdentityArn","parameterTypes":["java.lang.String"] }, {"name":"setKinesis","parameterTypes":["com.amazonaws.services.lambda.runtime.events.KinesisEvent$Record"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KinesisEvent$Record", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setKinesisSchemaVersion","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setDeliveryStreamArn","parameterTypes":["java.lang.String"] }, {"name":"setInvocationId","parameterTypes":["java.lang.String"] }, {"name":"setRecords","parameterTypes":["java.util.List"] }, {"name":"setRegion","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent$Record", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setApproximateArrivalTimestamp","parameterTypes":["java.lang.Long"] }, {"name":"setData","parameterTypes":["java.nio.ByteBuffer"] }, {"name":"setRecordId","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.RabbitMQEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setEventSource","parameterTypes":["java.lang.String"] }, {"name":"setEventSourceArn","parameterTypes":["java.lang.String"] }, {"name":"setRmqMessagesByQueue","parameterTypes":["java.util.Map"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.RabbitMQEvent$BasicProperties", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setAppId","parameterTypes":["java.lang.String"] }, {"name":"setBodySize","parameterTypes":["int"] }, {"name":"setClusterId","parameterTypes":["java.lang.String"] }, {"name":"setContentEncoding","parameterTypes":["java.lang.String"] }, {"name":"setContentType","parameterTypes":["java.lang.String"] }, {"name":"setCorrelationId","parameterTypes":["java.lang.String"] }, {"name":"setDeliveryMode","parameterTypes":["int"] }, {"name":"setExpiration","parameterTypes":["int"] }, {"name":"setHeaders","parameterTypes":["java.util.Map"] }, {"name":"setMessageId","parameterTypes":["java.lang.String"] }, {"name":"setPriority","parameterTypes":["int"] }, {"name":"setReplyTo","parameterTypes":["java.lang.String"] }, {"name":"setTimestamp","parameterTypes":["java.lang.String"] }, {"name":"setType","parameterTypes":["java.lang.String"] }, {"name":"setUserId","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.RabbitMQEvent$RabbitMessage", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setBasicProperties","parameterTypes":["com.amazonaws.services.lambda.runtime.events.RabbitMQEvent$BasicProperties"] }, {"name":"setData","parameterTypes":["java.lang.String"] }, {"name":"setRedelivered","parameterTypes":["boolean"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.SNSEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setRecords","parameterTypes":["java.util.List"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.SNSEvent$MessageAttribute", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setType","parameterTypes":["java.lang.String"] }, {"name":"setValue","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.SNSEvent$SNS", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setMessage","parameterTypes":["java.lang.String"] }, {"name":"setMessageAttributes","parameterTypes":["java.util.Map"] }, {"name":"setMessageId","parameterTypes":["java.lang.String"] }, {"name":"setSignature","parameterTypes":["java.lang.String"] }, {"name":"setSignatureVersion","parameterTypes":["java.lang.String"] }, {"name":"setSigningCertUrl","parameterTypes":["java.lang.String"] }, {"name":"setSubject","parameterTypes":["java.lang.String"] }, {"name":"setTimestamp","parameterTypes":["org.joda.time.DateTime"] }, {"name":"setTopicArn","parameterTypes":["java.lang.String"] }, {"name":"setType","parameterTypes":["java.lang.String"] }, {"name":"setUnsubscribeUrl","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.SNSEvent$SNSRecord", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setEventSource","parameterTypes":["java.lang.String"] }, {"name":"setEventSubscriptionArn","parameterTypes":["java.lang.String"] }, {"name":"setEventVersion","parameterTypes":["java.lang.String"] }, {"name":"setSns","parameterTypes":["com.amazonaws.services.lambda.runtime.events.SNSEvent$SNS"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.SQSEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setRecords","parameterTypes":["java.util.List"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.SQSEvent$MessageAttribute", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.SQSEvent$SQSMessage", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setAttributes","parameterTypes":["java.util.Map"] }, {"name":"setAwsRegion","parameterTypes":["java.lang.String"] }, {"name":"setBody","parameterTypes":["java.lang.String"] }, {"name":"setEventSource","parameterTypes":["java.lang.String"] }, {"name":"setEventSourceArn","parameterTypes":["java.lang.String"] }, {"name":"setMd5OfBody","parameterTypes":["java.lang.String"] }, {"name":"setMessageAttributes","parameterTypes":["java.util.Map"] }, {"name":"setMessageId","parameterTypes":["java.lang.String"] }, {"name":"setReceiptHandle","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.ScheduledEvent", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setAccount","parameterTypes":["java.lang.String"] }, {"name":"setDetail","parameterTypes":["java.util.Map"] }, {"name":"setDetailType","parameterTypes":["java.lang.String"] }, {"name":"setId","parameterTypes":["java.lang.String"] }, {"name":"setRegion","parameterTypes":["java.lang.String"] }, {"name":"setResources","parameterTypes":["java.util.List"] }, {"name":"setSource","parameterTypes":["java.lang.String"] }, {"name":"setTime","parameterTypes":["org.joda.time.DateTime"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.models.kinesis.Record", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"setApproximateArrivalTimestamp","parameterTypes":["java.util.Date"] }, {"name":"setData","parameterTypes":["java.nio.ByteBuffer"] }, {"name":"setEncryptionType","parameterTypes":["java.lang.String"] }, {"name":"setPartitionKey","parameterTypes":["java.lang.String"] }, {"name":"setSequenceNumber","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.serialization.events.mixins.CloudFormationCustomResourceEventMixin", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.serialization.events.mixins.CloudWatchLogsEventMixin", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.serialization.events.mixins.KinesisEventMixin", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.serialization.events.mixins.KinesisEventMixin$RecordMixin", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.serialization.events.mixins.SNSEventMixin", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.serialization.events.mixins.SNSEventMixin$SNSRecordMixin", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.serialization.events.mixins.SQSEventMixin", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.serialization.events.mixins.SQSEventMixin$SQSMessageMixin", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.serialization.events.mixins.ScheduledEventMixin", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.tests.EventArgumentsProvider", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.services.lambda.runtime.tests.annotations.Event", + "queryAllPublicMethods":true +}, +{ + "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"java.io.Serializable", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.Cloneable", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.Object", + "allDeclaredFields":true +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.function.Consumer", + "queryAllPublicMethods":true +}, +{ + "name":"org.apiguardian.api.API", + "queryAllPublicMethods":true +}, +{ + "name":"org.joda.time.DateTime", + "methods":[{"name":"parse","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"software.amazon.lambda.powertools.utilities.EventDeserializerTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"testDeserializeALBEventMessageAsObjectShouldReturnObject","parameterTypes":["com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent"] }, {"name":"testDeserializeAMQEventMessageAsListShouldReturnList","parameterTypes":["com.amazonaws.services.lambda.runtime.events.ActiveMQEvent"] }, {"name":"testDeserializeAPIGWEventBodyAsObject_shouldReturnObject","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent"] }, {"name":"testDeserializeAPIGatewayEventAsList_shouldThrowException","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent"] }, {"name":"testDeserializeAPIGatewayMapEventAsList_shouldThrowException","parameterTypes":["java.util.Map"] }, {"name":"testDeserializeAPIGatewayNoBodyAsList_shouldThrowException","parameterTypes":[] }, {"name":"testDeserializeAPIGatewayNoBody_shouldThrowException","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent"] }, {"name":"testDeserializeApiGWV2EventMessageAsObjectShouldReturnObject","parameterTypes":["com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent"] }, {"name":"testDeserializeCWLEventMessageAsObjectShouldReturnObject","parameterTypes":["com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent"] }, {"name":"testDeserializeCfcrEventMessageAsObjectShouldReturnObject","parameterTypes":["com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent"] }, {"name":"testDeserializeEmptyEventAsList_shouldThrowException","parameterTypes":[] }, {"name":"testDeserializeKFEventMessageAsListShouldReturnList","parameterTypes":["com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent"] }, {"name":"testDeserializeKafipEventMessageAsListShouldReturnList","parameterTypes":["com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsFirehoseInputPreprocessingEvent"] }, {"name":"testDeserializeKafkaEventMessageAsList_shouldReturnList","parameterTypes":["com.amazonaws.services.lambda.runtime.events.KafkaEvent"] }, {"name":"testDeserializeKasipEventMessageAsListShouldReturnList","parameterTypes":["com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsStreamsInputPreprocessingEvent"] }, {"name":"testDeserializeKinesisEventMessageAsList_shouldReturnList","parameterTypes":["com.amazonaws.services.lambda.runtime.events.KinesisEvent"] }, {"name":"testDeserializeMapAsObject_shouldReturnObject","parameterTypes":[] }, {"name":"testDeserializeProductAsProduct_shouldReturnProduct","parameterTypes":[] }, {"name":"testDeserializeRabbitMQEventMessageAsListShouldReturnList","parameterTypes":["com.amazonaws.services.lambda.runtime.events.RabbitMQEvent"] }, {"name":"testDeserializeSNSEventMessageAsObject_shouldReturnObject","parameterTypes":["com.amazonaws.services.lambda.runtime.events.SNSEvent"] }, {"name":"testDeserializeSQSEventMessageAsList_shouldReturnList","parameterTypes":["com.amazonaws.services.lambda.runtime.events.SQSEvent"] }, {"name":"testDeserializeSQSEventMessageAsObject_shouldThrowException","parameterTypes":["com.amazonaws.services.lambda.runtime.events.SQSEvent"] }, {"name":"testDeserializeSQSEventNoBody_shouldThrowException","parameterTypes":["com.amazonaws.services.lambda.runtime.events.SQSEvent"] }, {"name":"testDeserializeScheduledEventMessageAsObject_shouldReturnObject","parameterTypes":["com.amazonaws.services.lambda.runtime.events.ScheduledEvent"] }, {"name":"testDeserializeStringArrayAsList_shouldReturnList","parameterTypes":[] }, {"name":"testDeserializeStringAsList_shouldThrowException","parameterTypes":[] }, {"name":"testDeserializeStringAsObject_shouldReturnObject","parameterTypes":[] }, {"name":"testDeserializeStringAsString_shouldReturnString","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.utilities.jmespath.Base64FunctionTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"testPowertoolsBase64","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunctionTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"testBase64GzipDecompressNull","parameterTypes":[] }, {"name":"testConstructor","parameterTypes":[] }, {"name":"testPowertoolsGzip","parameterTypes":[] }, {"name":"testPowertoolsGzipEmptyJsonAttribute","parameterTypes":[] }, {"name":"testPowertoolsGzipNotCompressedJsonAttribute","parameterTypes":[] }, {"name":"testPowertoolsGzipWrongArgumentType","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.utilities.jmespath.JsonFunctionTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"testJsonFunction","parameterTypes":[] }, {"name":"testJsonFunctionChild","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.utilities.model.Product", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setId","parameterTypes":["long"] }, {"name":"setName","parameterTypes":["java.lang.String"] }, {"name":"setPrice","parameterTypes":["double"] }] +} +] diff --git a/powertools-serialization/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-serialization/resource-config.json b/powertools-serialization/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-serialization/resource-config.json new file mode 100644 index 000000000..d5bd7e14c --- /dev/null +++ b/powertools-serialization/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-serialization/resource-config.json @@ -0,0 +1,85 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.configuration.Configuration\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.presentation.Representation\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.engine.TestEngine\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.LauncherDiscoveryListener\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.LauncherSessionListener\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.PostDiscoveryFilter\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.TestExecutionListener\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\Qalb_event.json\\E" + }, { + "pattern":"\\Qamq_event.json\\E" + }, { + "pattern":"\\Qapigw_event.json\\E" + }, { + "pattern":"\\Qapigw_event_no_body.json\\E" + }, { + "pattern":"\\Qapigwv2_event.json\\E" + }, { + "pattern":"\\Qcfcr_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/lambda/thirdparty/org/joda/time/tz/data/Europe/Berlin\\E" + }, { + "pattern":"\\Qcom/amazonaws/lambda/thirdparty/org/joda/time/tz/data/ZoneInfoMap\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/alb_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/amq_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/apigw_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/apigw_event_no_body.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/apigwv2_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/cfcr_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/custom_event_map.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/cwl_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/kafip_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/kafka_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/kasip_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/kf_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/kinesis_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/rabbitmq_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/scheduled_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/sns_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/sqs_event.json\\E" + }, { + "pattern":"\\Qcom/amazonaws/services/lambda/runtime/serialization/factories/sqs_event_no_body.json\\E" + }, { + "pattern":"\\Qorg/joda/time/tz/data/Europe/Berlin\\E" + }, { + "pattern":"\\Qorg/joda/time/tz/data/ZoneInfoMap\\E" + }, { + "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" + }]}, + "bundles":[] +} diff --git a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java index fcfdb47e3..2914bd286 100644 --- a/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java +++ b/powertools-serialization/src/test/java/software/amazon/lambda/powertools/utilities/EventDeserializerTest.java @@ -39,7 +39,6 @@ import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import software.amazon.lambda.powertools.utilities.model.Basket; import software.amazon.lambda.powertools.utilities.model.Order; import software.amazon.lambda.powertools.utilities.model.Product; @@ -93,14 +92,6 @@ public void testDeserializeAPIGWEventBodyAsObject_shouldReturnObject(APIGatewayP assertProduct(product); } - @ParameterizedTest - @Event(value = "apigw_event.json", type = APIGatewayProxyRequestEvent.class) - public void testDeserializeAPIGWEventBodyAsWrongObjectType_shouldThrowException(APIGatewayProxyRequestEvent event) { - assertThatThrownBy(() -> extractDataFrom(event).as(Basket.class)) - .isInstanceOf(EventDeserializationException.class) - .hasMessage("Cannot load the event as Basket"); - } - @ParameterizedTest @Event(value = "sns_event.json", type = SNSEvent.class) public void testDeserializeSNSEventMessageAsObject_shouldReturnObject(SNSEvent event) { @@ -164,14 +155,6 @@ public void testDeserializeEmptyEventAsList_shouldThrowException() { .hasMessage("Event content is null: the event may be malformed (missing fields)"); } - @ParameterizedTest - @Event(value = "sqs_event.json", type = SQSEvent.class) - public void testDeserializeSQSEventBodyAsWrongObjectType_shouldThrowException(SQSEvent event) { - assertThatThrownBy(() -> extractDataFrom(event).asListOf(Basket.class)) - .isInstanceOf(EventDeserializationException.class) - .hasMessage("Cannot load the event as a list of Basket"); - } - @ParameterizedTest @Event(value = "apigw_event_no_body.json", type = APIGatewayProxyRequestEvent.class) public void testDeserializeAPIGatewayNoBody_shouldThrowException(APIGatewayProxyRequestEvent event) { diff --git a/powertools-sqs/pom.xml b/powertools-sqs/pom.xml deleted file mode 100644 index d65458d5e..000000000 --- a/powertools-sqs/pom.xml +++ /dev/null @@ -1,139 +0,0 @@ - - - - - 4.0.0 - - powertools-sqs - jar - - - powertools-parent - software.amazon.lambda - 1.19.0-SNAPSHOT - - - Powertools for AWS Lambda (Java) library SQS - - Deprecated: Batch processing is now handled in powertools-batch and large messages in powertools-large-messages modules. - A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. - - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - - - - - software.amazon.lambda - powertools-core - - - com.amazonaws - aws-lambda-java-tests - - - com.amazonaws - aws-lambda-java-core - - - software.amazon.payloadoffloading - payloadoffloading-common - - - com.amazonaws - aws-lambda-java-events - - - software.amazon.awssdk - sqs - - - software.amazon.awssdk - s3 - - - com.fasterxml.jackson.core - jackson-databind - - - - org.aspectj - aspectjrt - - - - - org.junit.jupiter - junit-jupiter-api - test - - - org.junit.jupiter - junit-jupiter-engine - test - - - org.junit.jupiter - junit-jupiter-params - test - - - org.apache.commons - commons-lang3 - test - - - org.aspectj - aspectjweaver - test - - - org.assertj - assertj-core - test - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - \ No newline at end of file diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SQSBatchProcessingException.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SQSBatchProcessingException.java deleted file mode 100644 index 7adc2afe5..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SQSBatchProcessingException.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static java.util.Collections.unmodifiableList; -import static java.util.stream.Collectors.joining; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import java.util.ArrayList; -import java.util.List; - -/** - *

- * When one or more {@link SQSMessage} fails and if any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} - * during processing of a messages, this exception is with all the details of successful and failed messages. - *

- * - *

- * This exception can be thrown form: - *

    - *
  • {@link SqsBatch}
  • - *
  • {@link SqsUtils#batchProcessor(SQSEvent, Class)}
  • - *
  • {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)}
  • - *
  • {@link SqsUtils#batchProcessor(SQSEvent, SqsMessageHandler)}
  • - *
  • {@link SqsUtils#batchProcessor(SQSEvent, boolean, SqsMessageHandler)}
  • - *
- *

- */ -public class SQSBatchProcessingException extends RuntimeException { - - private final List exceptions; - private final List failures; - private final List returnValues; - - public SQSBatchProcessingException(final List exceptions, - final List failures, - final List successReturns) { - super(exceptions.stream() - .map(Throwable::toString) - .collect(joining("\n"))); - - this.exceptions = new ArrayList<>(exceptions); - this.failures = new ArrayList<>(failures); - this.returnValues = new ArrayList<>(successReturns); - } - - /** - * Details for exceptions that occurred while processing messages in {@link SqsMessageHandler#process(SQSMessage)} - * - * @return List of exceptions that occurred while processing messages - */ - public List getExceptions() { - return unmodifiableList(exceptions); - } - - /** - * List of returns from {@link SqsMessageHandler#process(SQSMessage)} that were successfully processed. - * - * @return List of returns from successfully processed messages - */ - public List successMessageReturnValues() { - return unmodifiableList(returnValues); - } - - /** - * Details of {@link SQSMessage} that failed in {@link SqsMessageHandler#process(SQSMessage)} - * - * @return List of failed messages - */ - public List getFailures() { - return unmodifiableList(failures); - } - - @Override - public void printStackTrace() { - for (Exception exception : exceptions) { - exception.printStackTrace(); - } - } -} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsBatch.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsBatch.java deleted file mode 100644 index 4378fa707..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsBatch.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @deprecated - * @see software.amazon.lambda.powertools.batch in powertools-batch module. - * Will be removed in V2. - * - * {@link SqsBatch} is used to process batch messages in {@link SQSEvent} - * - *

- * When using the annotation, implementation of {@link SqsMessageHandler} is required. Annotation will take care of - * calling {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} in the received {@link SQSEvent} - *

- * - *

- * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a messages, Utility - * will take care of deleting all the successful messages from SQS. When one or more single message fails processing due - * to exception thrown from {@link SqsMessageHandler#process(SQSMessage)}, Lambda execution will fail - * with {@link SQSBatchProcessingException}. - *

- * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

- * - *

- * If you want to suppress the exception even if any message in batch fails, set - * {@link SqsBatch#suppressException()} to true. By default its value is false - *

- * - *

- * If you want certain exceptions to be treated as permanent failures, i.e. exceptions where the result of retrying will - * always be a failure and want these can be immediately moved to the dead letter queue associated to the source SQS queue, - *

- * you can use {@link SqsBatch#nonRetryableExceptions()} to configure such exceptions. - * Make sure function execution role has sqs:GetQueueAttributes permission on source SQS queue and sqs:SendMessage, - * sqs:SendMessageBatch permission for configured DLQ. - *

- * If you want such messages to be deleted instead, set {@link SqsBatch#deleteNonRetryableMessageFromQueue()} to true. - * By default its value is false. - *

- * If there is no DLQ configured on source SQS queue and {@link SqsBatch#nonRetryableExceptions()} attribute is set, if - * nonRetryableExceptions occurs from {@link SqsMessageHandler}, such exceptions will still be treated as temporary - * exceptions and the message will be moved back to source SQS queue for reprocessing. The same behaviour will occur if - * for some reason the utility is unable to move the message to the DLQ. An example of this could be because the function - * is missing the correct permissions. - *

- * - *
- * public class SqsMessageHandler implements RequestHandler {
- *
- *    {@literal @}Override
- *    {@literal @}{@link SqsBatch (SqsMessageHandler)}
- *     public String handleRequest(SQSEvent sqsEvent, Context context) {
- *
- *         return "ok";
- *     }
- *
- *     public class DummySqsMessageHandler implements SqsMessageHandler{
- *     @Override
- *     public Object process(SQSEvent.SQSMessage message) {
- *         throw new UnsupportedOperationException();
- *     }
- * }
- *
- *     ...
- * 
- *
- * @see Amazon SQS dead-letter queues
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.METHOD)
-@Deprecated
-public @interface SqsBatch {
-
-    Class> value();
-
-    boolean suppressException() default false;
-
-    Class[] nonRetryableExceptions() default {};
-
-    boolean deleteNonRetryableMessageFromQueue() default false;
-}
diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsLargeMessage.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsLargeMessage.java
deleted file mode 100644
index a3a92cea1..000000000
--- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsLargeMessage.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright 2023 Amazon.com, Inc. or its affiliates.
- * 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
- *     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.
- *
- */
-
-package software.amazon.lambda.powertools.sqs;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * @deprecated See software.amazon.lambda.powertools.largemessages.LargeMessage in powertools-large-messages module.
- * Will be removed in version 2.
- *
- * 

{@code SqsLargeMessage} is used to signal that the annotated method - * should be extended to handle large SQS messages which have been offloaded - * to S3

- * - *

{@code SqsLargeMessage} automatically retrieves and deletes messages - * which have been offloaded to S3 using the {@code amazon-sqs-java-extended-client-lib} - * client library.

- * - *

This version of the {@code SqsLargeMessage} is compatible with version - * 1.1.0+ of {@code amazon-sqs-java-extended-client-lib}.

- * - *
- * <dependency>
- *   <groupId>com.amazonaws</groupId>
- *   <artifactId>amazon-sqs-java-extended-client-lib</artifactId>
- *   <version>1.1.0</version>
- * </dependency>
- * 
- * - *

{@code SqsLargeMessage} should be used with the handleRequest method of a class - * which implements {@code com.amazonaws.services.lambda.runtime.RequestHandler} with - * {@code com.amazonaws.services.lambda.runtime.events.SQSEvent} as the first parameter.

- * - *
- * public class SqsMessageHandler implements RequestHandler {
- *
- *    {@literal @}Override
- *    {@literal @}SqsLargeMessage
- *     public String handleRequest(SQSEvent sqsEvent, Context context) {
- *
- *         // process messages
- *
- *         return "ok";
- *     }
- *
- *     ...
- * 
- * - *

Using the default S3 Client {@code AmazonS3 amazonS3 = AmazonS3ClientBuilder.defaultClient();} - * each record received in the SQSEvent {@code SqsLargeMessage} will checked - * to see if it's body contains a payload which has been offloaded to S3. If it - * does then {@code getObject(bucket, key)} will be called and the payload - * retrieved.

- * - *

Note: Retreiving payloads from S3 will increase the duration of the - * Lambda function.

- * - *

If the request handler method returns then each payload will be deleted - * from S3 using {@code deleteObject(bucket, key)}

- * - *

To disable deletion of payloads setting the following annotation parameter - * {@code @SqsLargeMessage(deletePayloads=false)}

- */ -@Deprecated -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface SqsLargeMessage { - - boolean deletePayloads() default true; -} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsMessageHandler.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsMessageHandler.java deleted file mode 100644 index 0c8f03ee9..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsMessageHandler.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; - -/** - *

- * This interface should be implemented for processing {@link SQSMessage} inside {@link SQSEvent} received by lambda - * function. - *

- * - *

- * It is required by utilities: - *

    - *
  • {@link SqsBatch}
  • - *
  • {@link SqsUtils#batchProcessor(SQSEvent, Class)}
  • - *
  • {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)}
  • - *
  • {@link SqsUtils#batchProcessor(SQSEvent, SqsMessageHandler)}
  • - *
  • {@link SqsUtils#batchProcessor(SQSEvent, boolean, SqsMessageHandler)}
  • - *
- *

- * - * @param Return value type from {@link SqsMessageHandler#process(SQSMessage)} - */ -@FunctionalInterface -public interface SqsMessageHandler { - - R process(SQSMessage message); -} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsUtils.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsUtils.java deleted file mode 100644 index d89642780..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsUtils.java +++ /dev/null @@ -1,653 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect.processMessages; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.lang.reflect.Constructor; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; -import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.sqs.SqsClient; -import software.amazon.lambda.powertools.core.internal.UserAgentConfigurator; -import software.amazon.lambda.powertools.sqs.exception.SkippedMessageDueToFailedBatchException; -import software.amazon.lambda.powertools.sqs.internal.BatchContext; -import software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect; -import software.amazon.payloadoffloading.PayloadS3Pointer; - -/** - * A class of helper functions to add additional functionality to {@link SQSEvent} processing. - * - * @deprecated Batch processing is now handled in powertools-batch and large messages in powertools-large-messages. - * This class will no longer be available in version 2. - */ -@Deprecated -public final class SqsUtils { - - public static final String SQS = "sqs"; - private static final Logger LOG = LoggerFactory.getLogger(SqsUtils.class); - private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final String MESSAGE_GROUP_ID = "MessageGroupId"; - private static SqsClient client; - private static S3Client s3Client; - - private SqsUtils() { - } - - /** - * This is a utility method when you want to avoid using {@code SqsLargeMessage} annotation. - * Gives you access to enriched messages from S3 in the SQS event produced via extended client lib. - * If all the large S3 payload are successfully retrieved, it will delete them from S3 post success. - * - * @param sqsEvent Event received from SQS Extended client library - * @param messageFunction Function to execute you business logic which provides access to enriched messages from S3 when needed. - * @return Return value from the function. - */ - public static R enrichedMessageFromS3(final SQSEvent sqsEvent, - final Function, R> messageFunction) { - return enrichedMessageFromS3(sqsEvent, true, messageFunction); - } - - /** - * This is a utility method when you want to avoid using {@code SqsLargeMessage} annotation. - * Gives you access to enriched messages from S3 in the SQS event produced via extended client lib. - * if all the large S3 payload are successfully retrieved, Control if it will delete payload from S3 post success. - * - * @param sqsEvent Event received from SQS Extended client library - * @param messageFunction Function to execute you business logic which provides access to enriched messages from S3 when needed. - * @return Return value from the function. - */ - public static R enrichedMessageFromS3(final SQSEvent sqsEvent, - final boolean deleteS3Payload, - final Function, R> messageFunction) { - - List sqsMessages = sqsEvent.getRecords().stream() - .map(SqsUtils::clonedMessage) - .collect(Collectors.toList()); - - List s3Pointers = processMessages(sqsMessages); - - R returnValue = messageFunction.apply(sqsMessages); - - if (deleteS3Payload) { - s3Pointers.forEach(SqsLargeMessageAspect::deleteMessage); - } - - return returnValue; - } - - /** - * Provides ability to set default {@link SqsClient} to be used by utility. - * If no default configuration is provided, client is instantiated via {@link SqsClient#create()} - * - * @param client {@link SqsClient} to be used by utility - */ - public static void overrideSqsClient(SqsClient client) { - SqsUtils.client = client; - } - - /** - * By default, the S3Client is instantiated via {@link S3Client#create()}. - * This method provides the ability to override the S3Client with your own custom version. - * - * @param s3Client {@link S3Client} to be used by utility - */ - public static void overrideS3Client(S3Client s3Client) { - SqsUtils.s3Client = s3Client; - } - - /** - * - * @deprecated - * @see software.amazon.lambda.powertools.batch in powertools-batch module. - * Will be removed in V2. - * - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

- * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

- * - *

- * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a message, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - *

- * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

- * - *

- * If you dont want the utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress - * it, Refer {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)} - *

- * - * @param event {@link SQSEvent} received by lambda function. - * @param handler Class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing. - */ - @Deprecated - public static List batchProcessor(final SQSEvent event, - final Class> handler) { - return batchProcessor(event, false, handler); - } - - /** - * @deprecated - * @see software.amazon.lambda.powertools.batch in powertools-batch module. - * Will be removed in V2. - * - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

- * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

- *

- * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a message, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - * - *

- * - *

- * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

- * - *

- * If you dont want the utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress - * it, Refer {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)} - *

- * - *

- * If you want certain exceptions to be treated as permanent failures, i.e. exceptions where the result of retrying will - * always be a failure and want these can be immediately moved to the dead letter queue associated to the source SQS queue, - * you can use nonRetryableExceptions parameter to configure such exceptions. - *

- * Make sure function execution role has sqs:GetQueueAttributes permission on source SQS queue and sqs:SendMessage, - * sqs:SendMessageBatch permission for configured DLQ. - *

- * If there is no DLQ configured on source SQS queue and {@link SqsBatch#nonRetryableExceptions()} attribute is set, if - * nonRetryableExceptions occurs from {@link SqsMessageHandler}, such exceptions will still be treated as temporary - * exceptions and the message will be moved back to source SQS queue for reprocessing. The same behaviour will occur if - * for some reason the utility is unable to move the message to the DLQ. An example of this could be because the function - * is missing the correct permissions. - *

- * - * @param event {@link SQSEvent} received by lambda function. - * @param handler Class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @param nonRetryableExceptions exception classes that are to be treated as permanent exceptions and to be moved - * to DLQ. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing. - * @see Amazon SQS dead-letter queues - */ - @SafeVarargs - @Deprecated - public static List batchProcessor(final SQSEvent event, - final Class> handler, - final Class... nonRetryableExceptions) { - return batchProcessor(event, false, handler, nonRetryableExceptions); - } - - /** - * @deprecated - * @see software.amazon.lambda.powertools.batch in powertools-batch module. - * Will be removed in V2. - * - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

- * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

- *

- * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a message, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - *

- * Exception can also be suppressed if desired. - *

- * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

- * - * @param event {@link SQSEvent} received by lambda function. - * @param suppressException if this is set to true, No {@link SQSBatchProcessingException} is thrown even on failed - * messages. - * @param handler Class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing and no suppression enabled. - */ - @Deprecated - public static List batchProcessor(final SQSEvent event, - final boolean suppressException, - final Class> handler) { - - SqsMessageHandler handlerInstance = instantiatedHandler(handler); - return batchProcessor(event, suppressException, handlerInstance); - } - - /** - * @deprecated - * @see software.amazon.lambda.powertools.batch in powertools-batch module. - * Will be removed in V2. - * - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

- * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

- *

- * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a message, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - * - *

- * - *

- * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

- * - *

- * If you dont want the utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress - * it, Refer {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)} - *

- * - *

- * If you want certain exceptions to be treated as permanent failures, i.e. exceptions where the result of retrying will - * always be a failure and want these can be immediately moved to the dead letter queue associated to the source SQS queue, - * you can use nonRetryableExceptions parameter to configure such exceptions. - *

- * Make sure function execution role has sqs:GetQueueAttributes permission on source SQS queue and sqs:SendMessage, - * sqs:SendMessageBatch permission for configured DLQ. - *

- * If there is no DLQ configured on source SQS queue and {@link SqsBatch#nonRetryableExceptions()} attribute is set, if - * nonRetryableExceptions occurs from {@link SqsMessageHandler}, such exceptions will still be treated as temporary - * exceptions and the message will be moved back to source SQS queue for reprocessing. The same behaviour will occur if - * for some reason the utility is unable to move the message to the DLQ. An example of this could be because the function - * is missing the correct permissions. - *

- * - * @param event {@link SQSEvent} received by lambda function. - * @param suppressException if this is set to true, No {@link SQSBatchProcessingException} is thrown even on failed - * messages. - * @param handler Class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @param nonRetryableExceptions exception classes that are to be treated as permanent exceptions and to be moved - * to DLQ. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing. - * @see Amazon SQS dead-letter queues - */ - @SafeVarargs - @Deprecated - public static List batchProcessor(final SQSEvent event, - final boolean suppressException, - final Class> handler, - final Class... nonRetryableExceptions) { - - SqsMessageHandler handlerInstance = instantiatedHandler(handler); - return batchProcessor(event, suppressException, handlerInstance, false, nonRetryableExceptions); - } - - /** - * @deprecated - * @see software.amazon.lambda.powertools.batch in powertools-batch module. - * Will be removed in V2. - * - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

- * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

- * - *

- * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a message, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - * - *

- * - *

- * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

- * - *

- * If you dont want the utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress - * it, Refer {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)} - *

- * - *

- * If you want certain exceptions to be treated as permanent failures, i.e. exceptions where the result of retrying will - * always be a failure and want these can be immediately moved to the dead letter queue associated to the source SQS queue, - * you can use nonRetryableExceptions parameter to configure such exceptions. - *

- * Make sure function execution role has sqs:GetQueueAttributes permission on source SQS queue and sqs:SendMessage, - * sqs:SendMessageBatch permission for configured DLQ. - *

- * If you want such messages to be deleted instead, set deleteNonRetryableMessageFromQueue to true. - *

- * If there is no DLQ configured on source SQS queue and {@link SqsBatch#nonRetryableExceptions()} attribute is set, if - * nonRetryableExceptions occurs from {@link SqsMessageHandler}, such exceptions will still be treated as temporary - * exceptions and the message will be moved back to source SQS queue for reprocessing. The same behaviour will occur if - * for some reason the utility is unable to move the message to the DLQ. An example of this could be because the function - * is missing the correct permissions. - *

- * - * @param event {@link SQSEvent} received by lambda function. - * @param suppressException if this is set to true, No {@link SQSBatchProcessingException} is thrown even on failed - * messages. - * @param handler Class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @param deleteNonRetryableMessageFromQueue If messages with nonRetryableExceptions are to be deleted from SQS queue. - * @param nonRetryableExceptions exception classes that are to be treated as permanent exceptions and to be moved - * to DLQ. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing. - * @see Amazon SQS dead-letter queues - */ - @SafeVarargs - @Deprecated - public static List batchProcessor(final SQSEvent event, - final boolean suppressException, - final Class> handler, - final boolean deleteNonRetryableMessageFromQueue, - final Class... nonRetryableExceptions) { - - SqsMessageHandler handlerInstance = instantiatedHandler(handler); - return batchProcessor(event, suppressException, handlerInstance, deleteNonRetryableMessageFromQueue, - nonRetryableExceptions); - } - - /** - * @deprecated - * @see software.amazon.lambda.powertools.batch in powertools-batch module. - * Will be removed in V2. - * - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

- * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

- * - *

- * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a messages, - * Utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - *

- * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

- * - *

- * If you dont want the utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress - * it, Refer {@link SqsUtils#batchProcessor(SQSEvent, boolean, SqsMessageHandler)} - *

- * - * @param event {@link SQSEvent} received by lambda function. - * @param handler Instance of class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message- - * @throws SQSBatchProcessingException if some messages fail during processing. - */ - @Deprecated - public static List batchProcessor(final SQSEvent event, - final SqsMessageHandler handler) { - return batchProcessor(event, false, handler); - } - - - /** - * @deprecated - * @see software.amazon.lambda.powertools.batch in powertools-batch module. - * Will be removed in V2. - * - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

- * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

- * - *

- * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a message, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - * - *

- * - *

- * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

- * - *

- * If you dont want the utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress - * it, Refer {@link SqsUtils#batchProcessor(SQSEvent, boolean, Class)} - *

- * - *

- * If you want certain exceptions to be treated as permanent failures, i.e. exceptions where the result of retrying will - * always be a failure and want these can be immediately moved to the dead letter queue associated to the source SQS queue, - * you can use nonRetryableExceptions parameter to configure such exceptions. - *

- * Make sure function execution role has sqs:GetQueueAttributes permission on source SQS queue and sqs:SendMessage, - * sqs:SendMessageBatch permission for configured DLQ. - *

- * If there is no DLQ configured on source SQS queue and {@link SqsBatch#nonRetryableExceptions()} attribute is set, if - * nonRetryableExceptions occurs from {@link SqsMessageHandler}, such exceptions will still be treated as temporary - * exceptions and the message will be moved back to source SQS queue for reprocessing.The same behaviour will occur if - * for some reason the utility is unable to moved the message to the DLQ. An example of this could be because the function - * is missing the correct permissions. - *

- * - * @param event {@link SQSEvent} received by lambda function. - * @param handler Instance of class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @param nonRetryableExceptions exception classes that are to be treated as permanent exceptions and to be moved - * to DLQ. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing. - * @see Amazon SQS dead-letter queues - */ - @SafeVarargs - @Deprecated - public static List batchProcessor(final SQSEvent event, - final SqsMessageHandler handler, - final Class... nonRetryableExceptions) { - return batchProcessor(event, false, handler, false, nonRetryableExceptions); - } - - - /** - * @deprecated - * @see software.amazon.lambda.powertools.batch in powertools-batch module. - * Will be removed in V2. - * - * This utility method is used to process each {@link SQSMessage} inside the received {@link SQSEvent} - * - *

- * The Utility will call {@link SqsMessageHandler#process(SQSMessage)} method for each {@link SQSMessage} - * in the received {@link SQSEvent} - *

- * - *

- * If any exception is thrown from {@link SqsMessageHandler#process(SQSMessage)} during processing of a messages, - * the utility will take care of deleting all the successful messages from SQS. When one or more single message fails - * processing due to exception thrown from {@link SqsMessageHandler#process(SQSMessage)} - * {@link SQSBatchProcessingException} is thrown with all the details of successful and failed messages. - *

- * Exception can also be suppressed if desired. - *

- * If all the messages are successfully processes, No SQS messages are deleted explicitly but is rather delegated to - * Lambda execution context for deletion. - *

- * - * @param event {@link SQSEvent} received by lambda function. - * @param suppressException if this is set to true, No {@link SQSBatchProcessingException} is thrown even on failed - * messages. - * @param handler Instance of class implementing {@link SqsMessageHandler} which will be called for each message in event. - * @return List of values returned by {@link SqsMessageHandler#process(SQSMessage)} while processing each message. - * @throws SQSBatchProcessingException if some messages fail during processing and no suppression enabled. - */ - @Deprecated - public static List batchProcessor(final SQSEvent event, - final boolean suppressException, - final SqsMessageHandler handler) { - return batchProcessor(event, suppressException, handler, false); - - } - - /** - * @deprecated - * @see software.amazon.lambda.powertools.batch in powertools-batch module. - * Will be removed in V2. - */ - @SafeVarargs - @Deprecated - public static List batchProcessor(final SQSEvent event, - final boolean suppressException, - final SqsMessageHandler handler, - final boolean deleteNonRetryableMessageFromQueue, - final Class... nonRetryableExceptions) { - final List handlerReturn = new ArrayList<>(); - - if (client == null) { - client = (SqsClient) SqsClient.builder() - .overrideConfiguration(ClientOverrideConfiguration.builder() - .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, - UserAgentConfigurator.getUserAgent(SQS)) - .build()); - } - - BatchContext batchContext = new BatchContext(client); - int offset = 0; - boolean failedBatch = false; - while (offset < event.getRecords().size() && !failedBatch) { - // Get the current message and advance to the next. Doing this here - // makes it easier for us to know where we are up to if we have to - // break out of here early. - SQSMessage message = event.getRecords().get(offset); - offset++; - - // If the batch hasn't failed, try process the message - try { - handlerReturn.add(handler.process(message)); - batchContext.addSuccess(message); - } catch (Exception e) { - - // Record the failure - batchContext.addFailure(message, e); - - // If we are trying to process a message that has a messageGroupId, we are on a FIFO queue. A failure - // now stops us from processing the rest of the batch; we break out of the loop leaving unprocessed - // messages in the queue - // https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting - String messageGroupId = message.getAttributes() != null ? - message.getAttributes().get(MESSAGE_GROUP_ID) : null; - if (messageGroupId != null) { - LOG.info( - "A message in a message batch with messageGroupId {} and messageId {} failed; failing the rest of the batch too" - , messageGroupId, message.getMessageId()); - failedBatch = true; - } - LOG.error("Encountered issue processing message: {}", message.getMessageId(), e); - } - } - - // If we have a FIFO batch failure, unprocessed messages will remain on the queue - // past the failed message. We have to add these to the errors - if (offset < event.getRecords().size()) { - event.getRecords() - .subList(offset, event.getRecords().size()) - .forEach(message -> - { - LOG.info("Skipping message {} as another message with a message group failed in this batch", - message.getMessageId()); - batchContext.addFailure(message, new SkippedMessageDueToFailedBatchException()); - }); - } - - batchContext.processSuccessAndHandleFailed(handlerReturn, suppressException, deleteNonRetryableMessageFromQueue, - nonRetryableExceptions); - return handlerReturn; - } - - private static SqsMessageHandler instantiatedHandler(final Class> handler) { - - try { - if (null == handler.getDeclaringClass()) { - return handler.getDeclaredConstructor().newInstance(); - } - - final Constructor> constructor = - handler.getDeclaredConstructor(handler.getDeclaringClass()); - constructor.setAccessible(true); - return constructor.newInstance(handler.getDeclaringClass().getDeclaredConstructor().newInstance()); - } catch (Exception e) { - LOG.error("Failed creating handler instance", e); - throw new RuntimeException("Unexpected error occurred. Please raise issue at " + - "https://github.com/aws-powertools/powertools-lambda-java/issues", e); - } - } - - private static SQSMessage clonedMessage(final SQSMessage sqsMessage) { - try { - return objectMapper - .readValue(objectMapper.writeValueAsString(sqsMessage), SQSMessage.class); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - public static ObjectMapper objectMapper() { - return objectMapper; - } - - public static S3Client s3Client() { - if (null == s3Client) { - s3Client = (S3Client) S3Client.builder() - .overrideConfiguration(ClientOverrideConfiguration.builder() - .putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_SUFFIX, - UserAgentConfigurator.getUserAgent(SQS)) - .build()); - SqsUtils.s3Client = S3Client.create(); - } - - return s3Client; - } -} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/BatchContext.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/BatchContext.java deleted file mode 100644 index 70cf04fb8..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/BatchContext.java +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs.internal; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static java.lang.String.format; -import static java.util.Optional.ofNullable; -import static java.util.stream.Collectors.toList; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.SdkBytes; -import software.amazon.awssdk.services.sqs.SqsClient; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequest; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequestEntry; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchResponse; -import software.amazon.awssdk.services.sqs.model.GetQueueAttributesRequest; -import software.amazon.awssdk.services.sqs.model.GetQueueAttributesResponse; -import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; -import software.amazon.awssdk.services.sqs.model.QueueAttributeName; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchResponse; -import software.amazon.lambda.powertools.sqs.SQSBatchProcessingException; -import software.amazon.lambda.powertools.sqs.SqsUtils; - -public final class BatchContext { - private static final Logger LOG = LoggerFactory.getLogger(BatchContext.class); - private static final Map QUEUE_ARN_TO_DLQ_URL_MAPPING = new HashMap<>(); - - private final Map messageToException = new HashMap<>(); - private final List success = new ArrayList<>(); - - private final SqsClient client; - - public BatchContext(SqsClient client) { - this.client = client; - } - - public void addSuccess(SQSMessage event) { - success.add(event); - } - - public void addFailure(SQSMessage event, Exception e) { - messageToException.put(event, e); - } - - @SafeVarargs - public final void processSuccessAndHandleFailed(final List successReturns, - final boolean suppressException, - final boolean deleteNonRetryableMessageFromQueue, - final Class... nonRetryableExceptions) { - if (hasFailures()) { - - List exceptions = new ArrayList<>(); - List failedMessages = new ArrayList<>(); - Map nonRetryableMessageToException = new HashMap<>(); - - if (nonRetryableExceptions.length == 0) { - exceptions.addAll(messageToException.values()); - failedMessages.addAll(messageToException.keySet()); - } else { - messageToException.forEach((sqsMessage, exception) -> - { - boolean nonRetryableException = isNonRetryableException(exception, nonRetryableExceptions); - - if (nonRetryableException) { - nonRetryableMessageToException.put(sqsMessage, exception); - } else { - exceptions.add(exception); - failedMessages.add(sqsMessage); - } - }); - } - - List messagesToBeDeleted = new ArrayList<>(success); - - if (!nonRetryableMessageToException.isEmpty() && deleteNonRetryableMessageFromQueue) { - messagesToBeDeleted.addAll(nonRetryableMessageToException.keySet()); - } else if (!nonRetryableMessageToException.isEmpty()) { - - boolean isMovedToDlq = moveNonRetryableMessagesToDlqIfConfigured(nonRetryableMessageToException); - - if (!isMovedToDlq) { - exceptions.addAll(nonRetryableMessageToException.values()); - failedMessages.addAll(nonRetryableMessageToException.keySet()); - } - } - - deleteMessagesFromQueue(messagesToBeDeleted); - - processFailedMessages(successReturns, suppressException, exceptions, failedMessages); - } - } - - private void processFailedMessages(List successReturns, - boolean suppressException, - List exceptions, - List failedMessages) { - if (failedMessages.isEmpty()) { - return; - } - - if (suppressException) { - List messageIds = failedMessages.stream(). - map(SQSMessage::getMessageId) - .collect(toList()); - - LOG.debug(format("[%d] records failed processing, but exceptions are suppressed. " + - "Failed messages %s", failedMessages.size(), messageIds)); - } else { - throw new SQSBatchProcessingException(exceptions, failedMessages, successReturns); - } - } - - private boolean isNonRetryableException(Exception exception, Class[] nonRetryableExceptions) { - return Arrays.stream(nonRetryableExceptions) - .anyMatch(aClass -> aClass.isInstance(exception)); - } - - private boolean moveNonRetryableMessagesToDlqIfConfigured( - Map nonRetryableMessageToException) { - Optional dlqUrl = fetchDlqUrl(nonRetryableMessageToException); - - if (!dlqUrl.isPresent()) { - return false; - } - - List dlqMessages = nonRetryableMessageToException.keySet().stream() - .map(sqsMessage -> - { - Map messageAttributesMap = new HashMap<>(); - - sqsMessage.getMessageAttributes().forEach((s, messageAttribute) -> - { - MessageAttributeValue.Builder builder = MessageAttributeValue.builder(); - - builder - .dataType(messageAttribute.getDataType()) - .stringValue(messageAttribute.getStringValue()); - - if (null != messageAttribute.getBinaryValue()) { - builder.binaryValue(SdkBytes.fromByteBuffer(messageAttribute.getBinaryValue())); - } - - messageAttributesMap.put(s, builder.build()); - }); - - return SendMessageBatchRequestEntry.builder() - .messageBody(sqsMessage.getBody()) - .id(sqsMessage.getMessageId()) - .messageAttributes(messageAttributesMap) - .build(); - }) - .collect(toList()); - - List sendMessageBatchResponses = batchRequest(dlqMessages, 10, entriesToSend -> - { - - SendMessageBatchResponse sendMessageBatchResponse = - client.sendMessageBatch(SendMessageBatchRequest.builder() - .entries(entriesToSend) - .queueUrl(dlqUrl.get()) - .build()); - - - LOG.debug("Response from send batch message to DLQ request {}", sendMessageBatchResponse); - - return sendMessageBatchResponse; - }); - - return sendMessageBatchResponses.stream() - .filter(response -> null != response && response.hasFailed()) - .peek(sendMessageBatchResponse -> LOG.error( - "Failed sending message to the DLQ. Entire batch will be re processed. Check if needed permissions are configured for the function. Response: {}", - sendMessageBatchResponse)) - .count() == 0; - } - - - private Optional fetchDlqUrl(Map nonRetryableMessageToException) { - return nonRetryableMessageToException.keySet().stream() - .findFirst() - .map(sqsMessage -> QUEUE_ARN_TO_DLQ_URL_MAPPING.computeIfAbsent(sqsMessage.getEventSourceArn(), - sourceArn -> - { - String queueUrl = url(sourceArn); - - GetQueueAttributesResponse queueAttributes = - client.getQueueAttributes(GetQueueAttributesRequest.builder() - .attributeNames(QueueAttributeName.REDRIVE_POLICY) - .queueUrl(queueUrl) - .build()); - - return ofNullable(queueAttributes.attributes().get(QueueAttributeName.REDRIVE_POLICY)) - .map(policy -> - { - try { - return SqsUtils.objectMapper().readTree(policy); - } catch (JsonProcessingException e) { - LOG.debug( - "Unable to parse Re drive policy for queue {}. Even if DLQ exists, failed messages will be send back to main queue.", - queueUrl, e); - return null; - } - }) - .map(node -> node.get("deadLetterTargetArn")) - .map(JsonNode::asText) - .map(this::url) - .orElse(null); - })); - } - - private boolean hasFailures() { - return !messageToException.isEmpty(); - } - - private void deleteMessagesFromQueue(final List messages) { - if (!messages.isEmpty()) { - - List entries = - messages.stream().map(m -> DeleteMessageBatchRequestEntry.builder() - .id(m.getMessageId()) - .receiptHandle(m.getReceiptHandle()) - .build()).collect(toList()); - - batchRequest(entries, 10, entriesToDelete -> - { - DeleteMessageBatchRequest request = DeleteMessageBatchRequest.builder() - .queueUrl(url(messages.get(0).getEventSourceArn())) - .entries(entriesToDelete) - .build(); - - DeleteMessageBatchResponse deleteMessageBatchResponse = client.deleteMessageBatch(request); - - LOG.debug("Response from delete request {}", deleteMessageBatchResponse); - - return deleteMessageBatchResponse; - }); - } - } - - private List batchRequest(final List listOFEntries, - final int size, - final Function, R> batchLogic) { - - return IntStream.range(0, listOFEntries.size()) - .filter(index -> index % size == 0) - .mapToObj(index -> listOFEntries.subList(index, Math.min(index + size, listOFEntries.size()))) - .map(batchLogic) - .collect(Collectors.toList()); - } - - private String url(String queueArn) { - String[] arnArray = queueArn.split(":"); - return String.format("https://sqs.%s.amazonaws.com/%s/%s", arnArray[3], arnArray[4], arnArray[5]); - } -} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsLargeMessageAspect.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsLargeMessageAspect.java deleted file mode 100644 index e5176e13a..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsLargeMessageAspect.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs.internal; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static java.lang.String.format; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; -import static software.amazon.lambda.powertools.sqs.SqsUtils.s3Client; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.s3.model.S3Exception; -import software.amazon.awssdk.utils.IoUtils; -import software.amazon.lambda.powertools.sqs.SqsLargeMessage; -import software.amazon.payloadoffloading.PayloadS3Pointer; - -@Aspect -public class SqsLargeMessageAspect { - - private static final Logger LOG = LoggerFactory.getLogger(SqsLargeMessageAspect.class); - - public static List processMessages(final List records) { - List s3Pointers = new ArrayList<>(); - for (SQSMessage sqsMessage : records) { - if (isBodyLargeMessagePointer(sqsMessage.getBody())) { - - PayloadS3Pointer s3Pointer = Optional.ofNullable(PayloadS3Pointer.fromJson(sqsMessage.getBody())) - .orElseThrow(() -> new FailedProcessingLargePayloadException( - format("Failed processing SQS body to extract S3 details. [ %s ].", - sqsMessage.getBody()))); - - ResponseInputStream s3Object = callS3Gracefully(s3Pointer, pointer -> - { - ResponseInputStream response = - s3Client().getObject(GetObjectRequest.builder() - .bucket(pointer.getS3BucketName()) - .key(pointer.getS3Key()) - .build()); - - LOG.debug("Object downloaded with key: " + s3Pointer.getS3Key()); - return response; - }); - - sqsMessage.setBody(readStringFromS3Object(s3Object, s3Pointer)); - s3Pointers.add(s3Pointer); - } - } - - return s3Pointers; - } - - private static boolean isBodyLargeMessagePointer(String record) { - return record.startsWith("[\"software.amazon.payloadoffloading.PayloadS3Pointer\""); - } - - private static String readStringFromS3Object(ResponseInputStream response, - PayloadS3Pointer s3Pointer) { - try (ResponseInputStream content = response) { - return IoUtils.toUtf8String(content); - } catch (IOException e) { - LOG.error("Error converting S3 object to String", e); - throw new FailedProcessingLargePayloadException( - format("Failed processing S3 record with [Bucket Name: %s Bucket Key: %s]", - s3Pointer.getS3BucketName(), s3Pointer.getS3Key()), e); - } - } - - public static void deleteMessage(PayloadS3Pointer s3Pointer) { - callS3Gracefully(s3Pointer, pointer -> - { - s3Client().deleteObject(DeleteObjectRequest.builder() - .bucket(pointer.getS3BucketName()) - .key(pointer.getS3Key()) - .build()); - LOG.info("Message deleted from S3: " + s3Pointer.toJson()); - return null; - }); - } - - private static R callS3Gracefully(final PayloadS3Pointer pointer, - final Function function) { - try { - return function.apply(pointer); - } catch (S3Exception e) { - LOG.error("A service exception", e); - throw new FailedProcessingLargePayloadException( - format("Failed processing S3 record with [Bucket Name: %s Bucket Key: %s]", - pointer.getS3BucketName(), pointer.getS3Key()), e); - } catch (SdkClientException e) { - LOG.error("Some sort of client exception", e); - throw new FailedProcessingLargePayloadException( - format("Failed processing S3 record with [Bucket Name: %s Bucket Key: %s]", - pointer.getS3BucketName(), pointer.getS3Key()), e); - } - } - - public static boolean placedOnSqsEventRequestHandler(ProceedingJoinPoint pjp) { - return pjp.getArgs().length == 2 - && pjp.getArgs()[0] instanceof SQSEvent - && pjp.getArgs()[1] instanceof Context; - } - - @SuppressWarnings({"EmptyMethod"}) - @Pointcut("@annotation(sqsLargeMessage)") - public void callAt(SqsLargeMessage sqsLargeMessage) { - } - - @Around(value = "callAt(sqsLargeMessage) && execution(@SqsLargeMessage * *.*(..))", argNames = "pjp,sqsLargeMessage") - public Object around(ProceedingJoinPoint pjp, - SqsLargeMessage sqsLargeMessage) throws Throwable { - Object[] proceedArgs = pjp.getArgs(); - - if (isHandlerMethod(pjp) - && placedOnSqsEventRequestHandler(pjp)) { - List pointersToDelete = rewriteMessages((SQSEvent) proceedArgs[0]); - - Object proceed = pjp.proceed(proceedArgs); - - if (sqsLargeMessage.deletePayloads()) { - pointersToDelete.forEach(SqsLargeMessageAspect::deleteMessage); - } - return proceed; - } - - return pjp.proceed(proceedArgs); - } - - private List rewriteMessages(SQSEvent sqsEvent) { - List records = sqsEvent.getRecords(); - return processMessages(records); - } - - public static class FailedProcessingLargePayloadException extends RuntimeException { - public FailedProcessingLargePayloadException(String message, Throwable cause) { - super(message, cause); - } - - public FailedProcessingLargePayloadException(String message) { - super(message); - } - } -} diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspect.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspect.java deleted file mode 100644 index ff0b5b014..000000000 --- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspect.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs.internal; - -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; -import static software.amazon.lambda.powertools.sqs.SqsUtils.batchProcessor; -import static software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect.placedOnSqsEventRequestHandler; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; -import software.amazon.lambda.powertools.sqs.SqsBatch; - -@Aspect -public class SqsMessageBatchProcessorAspect { - - @SuppressWarnings({"EmptyMethod"}) - @Pointcut("@annotation(sqsBatch)") - public void callAt(SqsBatch sqsBatch) { - } - - @Around(value = "callAt(sqsBatch) && execution(@SqsBatch * *.*(..))", argNames = "pjp,sqsBatch") - public Object around(ProceedingJoinPoint pjp, - SqsBatch sqsBatch) throws Throwable { - Object[] proceedArgs = pjp.getArgs(); - - if (isHandlerMethod(pjp) - && placedOnSqsEventRequestHandler(pjp)) { - - SQSEvent sqsEvent = (SQSEvent) proceedArgs[0]; - - batchProcessor(sqsEvent, - sqsBatch.suppressException(), - sqsBatch.value(), - sqsBatch.deleteNonRetryableMessageFromQueue(), - sqsBatch.nonRetryableExceptions()); - } - - return pjp.proceed(proceedArgs); - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsBatchProcessorTest.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsBatchProcessorTest.java deleted file mode 100644 index c0c334e78..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsBatchProcessorTest.java +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; -import static software.amazon.lambda.powertools.sqs.SqsUtils.batchProcessor; -import static software.amazon.lambda.powertools.sqs.SqsUtils.overrideSqsClient; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; -import software.amazon.awssdk.services.sqs.SqsClient; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequest; -import software.amazon.awssdk.services.sqs.model.GetQueueAttributesRequest; -import software.amazon.awssdk.services.sqs.model.GetQueueAttributesResponse; -import software.amazon.awssdk.services.sqs.model.QueueAttributeName; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest; - -class SqsUtilsBatchProcessorTest { - - private static final SqsClient sqsClient = mock(SqsClient.class); - private static final SqsClient interactionClient = mock(SqsClient.class); - private static final ObjectMapper MAPPER = new ObjectMapper(); - private SQSEvent event; - - @BeforeEach - void setUp() throws IOException { - reset(sqsClient, interactionClient); - event = MAPPER.readValue(this.getClass().getResource("/sampleSqsBatchEvent.json"), SQSEvent.class); - overrideSqsClient(sqsClient); - } - - @Test - void shouldBatchProcessAndNotDeleteMessagesWhenAllSuccess() { - List returnValues = batchProcessor(event, false, (message) -> - { - interactionClient.listQueues(); - return "Success"; - }); - - assertThat(returnValues) - .hasSize(2) - .containsExactly("Success", "Success"); - - verify(interactionClient, times(2)).listQueues(); - verifyNoInteractions(sqsClient); - } - - @ParameterizedTest - @ValueSource(classes = {SampleInnerSqsHandler.class, SampleSqsHandler.class}) - void shouldBatchProcessViaClassAndNotDeleteMessagesWhenAllSuccess( - Class> handler) { - List returnValues = batchProcessor(event, handler); - - assertThat(returnValues) - .hasSize(2) - .containsExactly("0", "1"); - - verifyNoInteractions(sqsClient); - } - - @Test - void shouldBatchProcessAndDeleteSuccessMessageOnPartialFailures() { - String failedId = "2e1424d4-f796-459a-8184-9c92662be6da"; - - SqsMessageHandler failedHandler = (message) -> - { - if (failedId.equals(message.getMessageId())) { - throw new RuntimeException("Failed processing"); - } - - interactionClient.listQueues(); - return "Success"; - }; - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(event, failedHandler)) - .satisfies(e -> - { - - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .contains(failedId); - - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("detailMessage") - .contains("Failed processing"); - }); - - verify(interactionClient).listQueues(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(DeleteMessageBatchRequest.class); - verify(sqsClient).deleteMessageBatch(captor.capture()); - - assertThat(captor.getValue()) - .hasFieldOrPropertyWithValue("queueUrl", "https://sqs.us-east-2.amazonaws.com/123456789012/my-queue"); - } - - @Test - void shouldBatchProcessAndFullFailuresInBatch() { - SqsMessageHandler failedHandler = (message) -> - { - throw new RuntimeException(message.getMessageId()); - }; - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(event, failedHandler)) - .satisfies(e -> - { - - assertThat(e.successMessageReturnValues()) - .isEmpty(); - - assertThat(e.getFailures()) - .hasSize(2) - .extracting("messageId") - .containsExactly("059f36b4-87a3-44ab-83d2-661975830a7d", - "2e1424d4-f796-459a-8184-9c92662be6da"); - - assertThat(e.getExceptions()) - .hasSize(2) - .extracting("detailMessage") - .containsExactly("059f36b4-87a3-44ab-83d2-661975830a7d", - "2e1424d4-f796-459a-8184-9c92662be6da"); - }); - - verifyNoInteractions(sqsClient); - } - - @Test - void shouldBatchProcessViaClassAndDeleteSuccessMessageOnPartialFailures() { - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(event, FailureSampleInnerSqsHandler.class)) - .satisfies(e -> - { - - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .contains("2e1424d4-f796-459a-8184-9c92662be6da"); - - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("detailMessage") - .contains("Failed processing"); - }); - - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - - @Test - void shouldBatchProcessAndSuppressExceptions() { - String failedId = "2e1424d4-f796-459a-8184-9c92662be6da"; - - SqsMessageHandler failedHandler = (message) -> - { - if (failedId.equals(message.getMessageId())) { - throw new RuntimeException("Failed processing"); - } - - interactionClient.listQueues(); - return "Success"; - }; - - List returnValues = batchProcessor(event, true, failedHandler); - - assertThat(returnValues) - .hasSize(1) - .contains("Success"); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessViaClassAndSuppressExceptions() { - List returnValues = batchProcessor(event, true, FailureSampleInnerSqsHandler.class); - - assertThat(returnValues) - .hasSize(1) - .contains("Success"); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessAndMoveNonRetryableExceptionToDlq() { - String failedId = "2e1424d4-f796-459a-8184-9c92662be6da"; - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "{\n" + - " \"deadLetterTargetArn\": \"arn:aws:sqs:us-east-2:123456789012:retry-queue\",\n" + - " \"maxReceiveCount\": 2\n" + - "}"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn( - GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - List batchProcessor = batchProcessor(event, (message) -> - { - if (failedId.equals(message.getMessageId())) { - throw new IllegalStateException("Failed processing"); - } - - interactionClient.listQueues(); - return "Success"; - }, IllegalStateException.class, IllegalArgumentException.class); - - assertThat(batchProcessor) - .hasSize(1); - - verify(sqsClient).sendMessageBatch(any(SendMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessAndDeleteNonRetryableException() { - String failedId = "2e1424d4-f796-459a-8184-9c92662be6da"; - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "{\n" + - " \"deadLetterTargetArn\": \"arn:aws:sqs:us-east-2:123456789012:retry-queue\",\n" + - " \"maxReceiveCount\": 2\n" + - "}"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn( - GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - List batchProcessor = batchProcessor(event, false, (message) -> - { - if (failedId.equals(message.getMessageId())) { - throw new IllegalStateException("Failed processing"); - } - - interactionClient.listQueues(); - return "Success"; - }, true, IllegalStateException.class, IllegalArgumentException.class); - - assertThat(batchProcessor) - .hasSize(1); - - verify(sqsClient, times(0)).sendMessageBatch(any(SendMessageBatchRequest.class)); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldDeleteSuccessfulMessageInBatchesOfT10orLess() throws IOException { - SQSEvent batch25Message = - MAPPER.readValue(this.getClass().getResource("/sampleSqsBatchEventBatchSize25.json"), SQSEvent.class); - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(batch25Message, FailureSampleInnerSqsHandler.class)) - .satisfies(e -> - { - - assertThat(e.successMessageReturnValues()) - .hasSize(24) - .contains("Success"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .contains("2e1424d4-f796-459a-8184-9c92662be6da"); - - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("detailMessage") - .contains("Failed processing"); - }); - - ArgumentCaptor captor = ArgumentCaptor.forClass(DeleteMessageBatchRequest.class); - - verify(sqsClient, times(3)).deleteMessageBatch(captor.capture()); - - assertThat(captor.getAllValues()) - .hasSize(3) - .flatMap(DeleteMessageBatchRequest::entries) - .hasSize(24); - } - - @Test - void shouldBatchProcessAndMoveNonRetryableExceptionToDlqInBatchesOfT10orLess() throws IOException { - SQSEvent batch25Message = - MAPPER.readValue(this.getClass().getResource("/sampleSqsBatchEventBatchSize25.json"), SQSEvent.class); - - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "{\n" + - " \"deadLetterTargetArn\": \"arn:aws:sqs:us-east-2:123456789012:retry-queue\",\n" + - " \"maxReceiveCount\": 2\n" + - "}"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn( - GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - List batchProcessor = batchProcessor(batch25Message, (message) -> - { - if ("2e1424d4-f796-459a-8184-9c92662be6da".equals(message.getMessageId())) { - interactionClient.listQueues(); - return "Success"; - } - - throw new IllegalStateException("Failed processing"); - }, IllegalStateException.class, IllegalArgumentException.class); - - assertThat(batchProcessor) - .hasSize(1); - - ArgumentCaptor captor = ArgumentCaptor.forClass(SendMessageBatchRequest.class); - - - verify(sqsClient, times(3)).sendMessageBatch(captor.capture()); - - assertThat(captor.getAllValues()) - .hasSize(3) - .flatMap(SendMessageBatchRequest::entries) - .hasSize(24); - } - - public class SampleInnerSqsHandler implements SqsMessageHandler { - private int counter; - - @Override - public String process(SQSMessage message) { - interactionClient.listQueues(); - return String.valueOf(counter++); - } - } - - public class FailureSampleInnerSqsHandler implements SqsMessageHandler { - @Override - public String process(SQSEvent.SQSMessage message) { - if ("2e1424d4-f796-459a-8184-9c92662be6da".equals(message.getMessageId())) { - throw new RuntimeException("Failed processing"); - } - - interactionClient.listQueues(); - return "Success"; - } - } -} \ No newline at end of file diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsFifoBatchProcessorTest.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsFifoBatchProcessorTest.java deleted file mode 100644 index bfc555405..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsFifoBatchProcessorTest.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static software.amazon.lambda.powertools.sqs.SqsUtils.batchProcessor; -import static software.amazon.lambda.powertools.sqs.SqsUtils.overrideSqsClient; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import com.amazonaws.services.lambda.runtime.tests.EventLoader; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoSession; -import org.mockito.quality.Strictness; -import software.amazon.awssdk.services.sqs.SqsClient; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequest; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequestEntry; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchResponse; - -public class SqsUtilsFifoBatchProcessorTest { - - private static SQSEvent sqsBatchEvent; - private MockitoSession session; - - @Mock - private SqsClient sqsClient; - - @Captor - private ArgumentCaptor deleteMessageBatchCaptor; - - public SqsUtilsFifoBatchProcessorTest() throws IOException { - sqsBatchEvent = EventLoader.loadSQSEvent("SqsFifoBatchEvent.json"); - } - - @BeforeEach - public void setup() { - // Establish a strict mocking session. This means that any - // calls to a mock that have not been stubbed will throw - this.session = Mockito.mockitoSession() - .strictness(Strictness.STRICT_STUBS) - .initMocks(this) - .startMocking(); - - overrideSqsClient(sqsClient); - } - - @AfterEach - public void tearDown() { - session.finishMocking(); - } - - @Test - public void processWholeBatch() { - // Act - AtomicInteger processedCount = new AtomicInteger(); - List results = batchProcessor(sqsBatchEvent, false, (message) -> - { - processedCount.getAndIncrement(); - return true; - }); - - // Assert - assertThat(processedCount.get()).isEqualTo(3); - assertThat(results.size()).isEqualTo(3); - } - - /** - * Check that a failure in the middle of the batch: - * - deletes the successful message explicitly from SQS - * - marks the failed and subsequent message as failed - * - does not delete the failed or subsequent message - */ - @Test - public void singleFailureInMiddleOfBatch() { - // Arrange - Mockito.when(sqsClient.deleteMessageBatch(deleteMessageBatchCaptor.capture())) - .thenReturn(DeleteMessageBatchResponse - .builder().build()); - - // Act - AtomicInteger processedCount = new AtomicInteger(); - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(sqsBatchEvent, false, (message) -> - { - int value = processedCount.getAndIncrement(); - if (value == 1) { - throw new RuntimeException("Whoops"); - } - return true; - })) - - // Assert - .isInstanceOf(SQSBatchProcessingException.class) - .satisfies(e -> - { - List failures = ((SQSBatchProcessingException) e).getFailures(); - assertThat(failures.size()).isEqualTo(2); - List failureIds = failures.stream() - .map(SQSEvent.SQSMessage::getMessageId) - .collect(Collectors.toList()); - assertThat(failureIds).contains(sqsBatchEvent.getRecords().get(1).getMessageId()); - assertThat(failureIds).contains(sqsBatchEvent.getRecords().get(2).getMessageId()); - }); - - DeleteMessageBatchRequest deleteRequest = deleteMessageBatchCaptor.getValue(); - List messageIds = deleteRequest.entries().stream() - .map(DeleteMessageBatchRequestEntry::id) - .collect(Collectors.toList()); - assertThat(deleteRequest.entries().size()).isEqualTo(1); - assertThat(messageIds.contains(sqsBatchEvent.getRecords().get(0).getMessageId())).isTrue(); - - } - - @Test - public void singleFailureAtEndOfBatch() { - - // Arrange - Mockito.when(sqsClient.deleteMessageBatch(deleteMessageBatchCaptor.capture())) - .thenReturn(DeleteMessageBatchResponse - .builder().build()); - - - // Act - AtomicInteger processedCount = new AtomicInteger(); - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(sqsBatchEvent, false, (message) -> - { - int value = processedCount.getAndIncrement(); - if (value == 2) { - throw new RuntimeException("Whoops"); - } - return true; - })); - - // Assert - DeleteMessageBatchRequest deleteRequest = deleteMessageBatchCaptor.getValue(); - List messageIds = deleteRequest.entries().stream() - .map(DeleteMessageBatchRequestEntry::id) - .collect(Collectors.toList()); - assertThat(deleteRequest.entries().size()).isEqualTo(2); - assertThat(messageIds.contains(sqsBatchEvent.getRecords().get(0).getMessageId())).isTrue(); - assertThat(messageIds.contains(sqsBatchEvent.getRecords().get(1).getMessageId())).isTrue(); - assertThat(messageIds.contains(sqsBatchEvent.getRecords().get(2).getMessageId())).isFalse(); - } - - @Test - public void messageFailureStopsGroupProcessing() { - String groupToFail = sqsBatchEvent.getRecords().get(0).getAttributes().get("MessageGroupId"); - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> batchProcessor(sqsBatchEvent, (message) -> - { - String groupId = message.getAttributes().get("MessageGroupId"); - if (groupId.equals(groupToFail)) { - throw new RuntimeException("Failed processing"); - } - return groupId; - })) - .satisfies(e -> - { - assertThat(e.successMessageReturnValues().size()).isEqualTo(0); - assertThat(e.successMessageReturnValues().contains(groupToFail)).isFalse(); - }); - } - -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsLargeMessageTest.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsLargeMessageTest.java deleted file mode 100644 index afc426976..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SqsUtilsLargeMessageTest.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; - -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Consumer; -import java.util.stream.Stream; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import software.amazon.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.http.AbortableInputStream; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.s3.model.S3Exception; -import software.amazon.awssdk.utils.StringInputStream; -import software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect; - -class SqsUtilsLargeMessageTest { - - private static final String BUCKET_NAME = "ms-extended-sqs-client"; - private static final String BUCKET_KEY = "c71eb2ae-37e0-4265-8909-32f4153faddf"; - @Mock - private S3Client s3Client; - - private static Stream exception() { - return Stream.of(Arguments.of(S3Exception.builder() - .message("Service Exception") - .build()), - Arguments.of(SdkClientException.builder() - .message("Client Exception") - .build())); - } - - @BeforeEach - void setUp() { - openMocks(this); - SqsUtils.overrideS3Client(s3Client); - } - - @Test - public void testLargeMessage() { - ResponseInputStream s3Response = - new ResponseInputStream<>(GetObjectResponse.builder().build(), - AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3Response); - - SQSEvent sqsEvent = messageWithBody( - "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + - "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"); - - Map sqsMessage = SqsUtils.enrichedMessageFromS3(sqsEvent, sqsMessages -> - { - Map someBusinessLogic = new HashMap<>(); - someBusinessLogic.put("Message", sqsMessages.get(0).getBody()); - return someBusinessLogic; - }); - - assertThat(sqsMessage) - .hasSize(1) - .containsEntry("Message", "A big message"); - - ArgumentCaptor delete = ArgumentCaptor.forClass(DeleteObjectRequest.class); - - verify(s3Client).deleteObject(delete.capture()); - - Assertions.assertThat(delete.getValue()) - .satisfies((Consumer) deleteObjectRequest -> - { - assertThat(deleteObjectRequest.bucket()) - .isEqualTo(BUCKET_NAME); - - assertThat(deleteObjectRequest.key()) - .isEqualTo(BUCKET_KEY); - }); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - public void testLargeMessageDeleteFromS3Toggle(boolean deleteS3Payload) { - ResponseInputStream s3Response = - new ResponseInputStream<>(GetObjectResponse.builder().build(), - AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); - - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3Response); - - SQSEvent sqsEvent = messageWithBody( - "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + - "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"); - - Map sqsMessage = SqsUtils.enrichedMessageFromS3(sqsEvent, deleteS3Payload, sqsMessages -> - { - Map someBusinessLogic = new HashMap<>(); - someBusinessLogic.put("Message", sqsMessages.get(0).getBody()); - return someBusinessLogic; - }); - - assertThat(sqsMessage) - .hasSize(1) - .containsEntry("Message", "A big message"); - if (deleteS3Payload) { - ArgumentCaptor delete = ArgumentCaptor.forClass(DeleteObjectRequest.class); - - verify(s3Client).deleteObject(delete.capture()); - - Assertions.assertThat(delete.getValue()) - .satisfies((Consumer) deleteObjectRequest -> - { - assertThat(deleteObjectRequest.bucket()) - .isEqualTo(BUCKET_NAME); - - assertThat(deleteObjectRequest.key()) - .isEqualTo(BUCKET_KEY); - }); - } else { - verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); - } - } - - @Test - public void shouldNotProcessSmallMessageBody() { - ResponseInputStream s3Response = - new ResponseInputStream<>(GetObjectResponse.builder().build(), - AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); - - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3Response); - - SQSEvent sqsEvent = messageWithBody("This is small message"); - - Map sqsMessage = SqsUtils.enrichedMessageFromS3(sqsEvent, sqsMessages -> - { - Map someBusinessLogic = new HashMap<>(); - someBusinessLogic.put("Message", sqsMessages.get(0).getBody()); - return someBusinessLogic; - }); - - assertThat(sqsMessage) - .containsEntry("Message", "This is small message"); - - verifyNoInteractions(s3Client); - } - - @ParameterizedTest - @MethodSource("exception") - public void shouldFailEntireBatchIfFailedDownloadingFromS3(RuntimeException exception) { - when(s3Client.getObject(any(GetObjectRequest.class))).thenThrow(exception); - - String messageBody = - "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + - "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; - SQSEvent sqsEvent = messageWithBody(messageBody); - - assertThatExceptionOfType(SqsLargeMessageAspect.FailedProcessingLargePayloadException.class) - .isThrownBy(() -> SqsUtils.enrichedMessageFromS3(sqsEvent, sqsMessages -> sqsMessages.get(0).getBody())) - .withCause(exception); - - verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); - } - - @Test - public void shouldFailEntireBatchIfFailedProcessingDownloadMessageFromS3() { - ResponseInputStream s3Response = - new ResponseInputStream<>(GetObjectResponse.builder().build(), - AbortableInputStream.create(new StringInputStream("test") { - @Override - public void close() throws IOException { - throw new IOException("Failed"); - } - })); - - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3Response); - - String messageBody = - "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + - "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; - SQSEvent sqsEvent = messageWithBody(messageBody); - - assertThatExceptionOfType(SqsLargeMessageAspect.FailedProcessingLargePayloadException.class) - .isThrownBy(() -> SqsUtils.enrichedMessageFromS3(sqsEvent, sqsMessages -> sqsMessages.get(0).getBody())) - .withCauseInstanceOf(IOException.class); - - verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); - } - - private SQSEvent messageWithBody(String messageBody) { - SQSMessage sqsMessage = new SQSMessage(); - sqsMessage.setBody(messageBody); - SQSEvent sqsEvent = new SQSEvent(); - sqsEvent.setRecords(singletonList(sqsMessage)); - return sqsEvent; - } -} \ No newline at end of file diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchFailureSuppressedHandler.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchFailureSuppressedHandler.java deleted file mode 100644 index 172179057..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchFailureSuppressedHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs.handlers; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - -public class PartialBatchFailureSuppressedHandler implements RequestHandler { - @Override - @SqsBatch(value = InnerMessageHandler.class, suppressException = true) - public String handleRequest(final SQSEvent sqsEvent, - final Context context) { - return "Success"; - } - - private class InnerMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - if ("2e1424d4-f796-459a-8184-9c92662be6da".equals(message.getMessageId())) { - throw new RuntimeException("2e1424d4-f796-459a-8184-9c92662be6da"); - } - - interactionClient.listQueues(); - return "Success"; - } - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchPartialFailureHandler.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchPartialFailureHandler.java deleted file mode 100644 index 6e3971269..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchPartialFailureHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs.handlers; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - -public class PartialBatchPartialFailureHandler implements RequestHandler { - @Override - @SqsBatch(InnerMessageHandler.class) - public String handleRequest(final SQSEvent sqsEvent, - final Context context) { - return "Success"; - } - - private class InnerMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - if ("2e1424d4-f796-459a-8184-9c92662be6da".equals(message.getMessageId())) { - throw new RuntimeException("2e1424d4-f796-459a-8184-9c92662be6da"); - } - - interactionClient.listQueues(); - return "Success"; - } - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchSuccessHandler.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchSuccessHandler.java deleted file mode 100644 index acfcd7109..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchSuccessHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs.handlers; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - -public class PartialBatchSuccessHandler implements RequestHandler { - @Override - @SqsBatch(InnerMessageHandler.class) - public String handleRequest(final SQSEvent sqsEvent, - final Context context) { - return "Success"; - } - - private class InnerMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - interactionClient.listQueues(); - return "Success"; - } - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandlerWithNonRetryableHandler.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandlerWithNonRetryableHandler.java deleted file mode 100644 index 74ff02e2c..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandlerWithNonRetryableHandler.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs.handlers; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - -public class SqsMessageHandlerWithNonRetryableHandler implements RequestHandler { - - @Override - @SqsBatch(value = InnerMessageHandler.class, nonRetryableExceptions = {IllegalStateException.class, - IllegalArgumentException.class}) - public String handleRequest(final SQSEvent sqsEvent, - final Context context) { - return "Success"; - } - - private class InnerMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - if (message.getMessageId().isEmpty()) { - throw new IllegalArgumentException("Invalid message and was moved to DLQ"); - } - - if ("2e1424d4-f796-459a-9696-9c92662ba5da".equals(message.getMessageId())) { - throw new RuntimeException("Invalid message and should be reprocessed"); - } - - interactionClient.listQueues(); - return "Success"; - } - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandlerWithNonRetryableHandlerWithDelete.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandlerWithNonRetryableHandlerWithDelete.java deleted file mode 100644 index 5b341880e..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandlerWithNonRetryableHandlerWithDelete.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs.handlers; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static software.amazon.lambda.powertools.sqs.internal.SqsMessageBatchProcessorAspectTest.interactionClient; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsMessageHandler; - -public class SqsMessageHandlerWithNonRetryableHandlerWithDelete implements RequestHandler { - - @Override - @SqsBatch(value = InnerMessageHandler.class, - nonRetryableExceptions = {IllegalStateException.class, IllegalArgumentException.class}, - deleteNonRetryableMessageFromQueue = true) - public String handleRequest(final SQSEvent sqsEvent, - final Context context) { - return "Success"; - } - - private class InnerMessageHandler implements SqsMessageHandler { - - @Override - public String process(SQSMessage message) { - if (message.getMessageId().isEmpty()) { - throw new IllegalArgumentException("Invalid message and was moved to DLQ"); - } - - if ("2e1424d4-f796-459a-9696-9c92662ba5da".equals(message.getMessageId())) { - throw new RuntimeException("Invalid message and should be reprocessed"); - } - - interactionClient.listQueues(); - return "Success"; - } - } -} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsNoDeleteMessageHandler.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsNoDeleteMessageHandler.java deleted file mode 100644 index e96dc5581..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsNoDeleteMessageHandler.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsLargeMessage; - -public class SqsNoDeleteMessageHandler implements RequestHandler { - - @Override - @SqsLargeMessage(deletePayloads = false) - public String handleRequest(SQSEvent sqsEvent, Context context) { - return sqsEvent.getRecords().get(0).getBody(); - } -} \ No newline at end of file diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsLargeMessageAspectTest.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsLargeMessageAspectTest.java deleted file mode 100644 index 535da11c7..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsLargeMessageAspectTest.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs.internal; - -import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; -import static software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect.FailedProcessingLargePayloadException; - -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.SQSEvent; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.function.Consumer; -import java.util.stream.Stream; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import software.amazon.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.http.AbortableInputStream; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.s3.model.S3Exception; -import software.amazon.awssdk.utils.StringInputStream; -import software.amazon.lambda.powertools.sqs.SqsUtils; -import software.amazon.lambda.powertools.sqs.handlers.LambdaHandlerApiGateway; -import software.amazon.lambda.powertools.sqs.handlers.SqsMessageHandler; -import software.amazon.lambda.powertools.sqs.handlers.SqsNoDeleteMessageHandler; - -public class SqsLargeMessageAspectTest { - - private static final String BUCKET_NAME = "bucketname"; - private static final String BUCKET_KEY = "c71eb2ae-37e0-4265-8909-32f4153faddf"; - private RequestHandler requestHandler; - @Mock - private Context context; - @Mock - private S3Client s3Client; - - private static Stream exception() { - return Stream.of(Arguments.of(S3Exception.builder() - .message("Service Exception") - .build()), - Arguments.of(SdkClientException.builder() - .message("Client Exception") - .build())); - } - - @BeforeEach - void setUp() { - openMocks(this); - setupContext(); - SqsUtils.overrideS3Client(s3Client); - requestHandler = new SqsMessageHandler(); - } - - @Test - public void testLargeMessage() { - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3ObjectWithLargeMessage()); - SQSEvent sqsEvent = messageWithBody( - "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + - "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"); - - String response = requestHandler.handleRequest(sqsEvent, context); - - assertThat(response) - .isEqualTo("A big message"); - - ArgumentCaptor delete = ArgumentCaptor.forClass(DeleteObjectRequest.class); - - verify(s3Client).deleteObject(delete.capture()); - - Assertions.assertThat(delete.getValue()) - .satisfies((Consumer) deleteObjectRequest -> - { - assertThat(deleteObjectRequest.bucket()) - .isEqualTo(BUCKET_NAME); - - assertThat(deleteObjectRequest.key()) - .isEqualTo(BUCKET_KEY); - }); - } - - @Test - public void shouldNotProcessSmallMessageBody() { - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3ObjectWithLargeMessage()); - - SQSEvent sqsEvent = messageWithBody("This is small message"); - - String response = requestHandler.handleRequest(sqsEvent, context); - - assertThat(response) - .isEqualTo("This is small message"); - - verifyNoInteractions(s3Client); - } - - @ParameterizedTest - @MethodSource("exception") - public void shouldFailEntireBatchIfFailedDownloadingFromS3(RuntimeException exception) { - when(s3Client.getObject(any(GetObjectRequest.class))).thenThrow(exception); - - String messageBody = - "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + - "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; - SQSEvent sqsEvent = messageWithBody(messageBody); - - assertThatExceptionOfType(FailedProcessingLargePayloadException.class) - .isThrownBy(() -> requestHandler.handleRequest(sqsEvent, context)) - .withCause(exception); - - verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); - } - - @Test - public void testLargeMessageWithDeletionOff() { - requestHandler = new SqsNoDeleteMessageHandler(); - - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3ObjectWithLargeMessage()); - SQSEvent sqsEvent = messageWithBody( - "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + - "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"); - - String response = requestHandler.handleRequest(sqsEvent, context); - - assertThat(response).isEqualTo("A big message"); - - verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); - } - - @Test - public void shouldFailEntireBatchIfFailedProcessingDownloadMessageFromS3() { - ResponseInputStream s3Response = - new ResponseInputStream<>(GetObjectResponse.builder().build(), - AbortableInputStream.create(new StringInputStream("test") { - @Override - public void close() throws IOException { - throw new IOException("Failed"); - } - })); - - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3Response); - - String messageBody = - "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + - "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; - SQSEvent sqsEvent = messageWithBody(messageBody); - - assertThatExceptionOfType(FailedProcessingLargePayloadException.class) - .isThrownBy(() -> requestHandler.handleRequest(sqsEvent, context)) - .withCauseInstanceOf(IOException.class); - - verify(s3Client, never()).deleteObject(any(DeleteObjectRequest.class)); - } - - @Test - public void shouldNotDoAnyProcessingWhenNotSqsEvent() { - LambdaHandlerApiGateway handler = new LambdaHandlerApiGateway(); - - String messageBody = - "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + - "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"; - - APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); - event.setBody(messageBody); - String response = handler.handleRequest(event, context); - - assertThat(response) - .isEqualTo(messageBody); - - verifyNoInteractions(s3Client); - } - - private ResponseInputStream s3ObjectWithLargeMessage() { - return new ResponseInputStream<>(GetObjectResponse.builder().build(), - AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); - } - - private SQSEvent messageWithBody(String messageBody) { - SQSMessage sqsMessage = new SQSMessage(); - sqsMessage.setBody(messageBody); - SQSEvent sqsEvent = new SQSEvent(); - sqsEvent.setRecords(singletonList(sqsMessage)); - return sqsEvent; - } - - private void setupContext() { - when(context.getFunctionName()).thenReturn("testFunction"); - when(context.getInvokedFunctionArn()).thenReturn("testArn"); - when(context.getFunctionVersion()).thenReturn("1"); - when(context.getMemoryLimitInMB()).thenReturn(10); - } -} \ No newline at end of file diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspectTest.java b/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspectTest.java deleted file mode 100644 index c0211cb83..000000000 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspectTest.java +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.sqs.internal; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; -import static software.amazon.lambda.powertools.sqs.SqsUtils.overrideSqsClient; - -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.SQSEvent; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.util.HashMap; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import software.amazon.awssdk.services.sqs.SqsClient; -import software.amazon.awssdk.services.sqs.model.BatchResultErrorEntry; -import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequest; -import software.amazon.awssdk.services.sqs.model.GetQueueAttributesRequest; -import software.amazon.awssdk.services.sqs.model.GetQueueAttributesResponse; -import software.amazon.awssdk.services.sqs.model.QueueAttributeName; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest; -import software.amazon.awssdk.services.sqs.model.SendMessageBatchResponse; -import software.amazon.lambda.powertools.sqs.SQSBatchProcessingException; -import software.amazon.lambda.powertools.sqs.handlers.LambdaHandlerApiGateway; -import software.amazon.lambda.powertools.sqs.handlers.PartialBatchFailureSuppressedHandler; -import software.amazon.lambda.powertools.sqs.handlers.PartialBatchPartialFailureHandler; -import software.amazon.lambda.powertools.sqs.handlers.PartialBatchSuccessHandler; -import software.amazon.lambda.powertools.sqs.handlers.SqsMessageHandlerWithNonRetryableHandler; -import software.amazon.lambda.powertools.sqs.handlers.SqsMessageHandlerWithNonRetryableHandlerWithDelete; - -public class SqsMessageBatchProcessorAspectTest { - public static final SqsClient interactionClient = mock(SqsClient.class); - private static final SqsClient sqsClient = mock(SqsClient.class); - private static final ObjectMapper MAPPER = new ObjectMapper(); - private final Context context = mock(Context.class); - private SQSEvent event; - private RequestHandler requestHandler; - - @BeforeEach - void setUp() throws IOException { - overrideSqsClient(sqsClient); - reset(interactionClient); - reset(sqsClient); - setupContext(); - event = MAPPER.readValue(this.getClass().getResource("/sampleSqsBatchEvent.json"), SQSEvent.class); - requestHandler = new PartialBatchSuccessHandler(); - } - - @Test - void shouldBatchProcessAllMessageSuccessfullyAndNotDeleteFromSQS() { - requestHandler.handleRequest(event, context); - - verify(interactionClient, times(2)).listQueues(); - verify(sqsClient, times(0)).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessMessageWithSuccessDeletedOnFailureInBatchFromSQS() { - requestHandler = new PartialBatchPartialFailureHandler(); - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> requestHandler.handleRequest(event, context)) - .satisfies(e -> - { - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("message") - .containsExactly("2e1424d4-f796-459a-8184-9c92662be6da"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .containsExactly("2e1424d4-f796-459a-8184-9c92662be6da"); - - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); - }); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessMessageWithSuccessDeletedOnFailureWithSuppressionInBatchFromSQS() { - requestHandler = new PartialBatchFailureSuppressedHandler(); - - requestHandler.handleRequest(event, context); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldNotTakeEffectOnNonSqsEventHandler() { - LambdaHandlerApiGateway handlerApiGateway = new LambdaHandlerApiGateway(); - - handlerApiGateway.handleRequest(mock(APIGatewayProxyRequestEvent.class), context); - - verifyNoInteractions(sqsClient); - } - - @Test - void shouldBatchProcessAndMoveNonRetryableExceptionToDlq() { - requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); - event.getRecords().get(0).setMessageId(""); - - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "{\n" + - " \"deadLetterTargetArn\": \"arn:aws:sqs:us-east-2:123456789012:retry-queue\",\n" + - " \"maxReceiveCount\": 2\n" + - "}"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn( - GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - requestHandler.handleRequest(event, context); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - verify(sqsClient).sendMessageBatch(any(SendMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessAndThrowExceptionForNonRetryableExceptionWhenMoveToDlqReturnFailedResponse() { - requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); - event.getRecords().get(0).setMessageId(""); - - when(sqsClient.sendMessageBatch(any(SendMessageBatchRequest.class))).thenReturn( - SendMessageBatchResponse.builder() - .failed(BatchResultErrorEntry.builder() - .message("Permission Error") - .code("KMS.AccessDeniedException") - .senderFault(true) - .build()) - .build()); - - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "{\n" + - " \"deadLetterTargetArn\": \"arn:aws:sqs:us-east-2:123456789012:retry-queue\",\n" + - " \"maxReceiveCount\": 2\n" + - "}"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn( - GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - Assertions.assertThatExceptionOfType(SQSBatchProcessingException.class). - isThrownBy(() -> requestHandler.handleRequest(event, context)); - - verify(interactionClient).listQueues(); - verify(sqsClient).sendMessageBatch(any(SendMessageBatchRequest.class)); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessAndDeleteNonRetryableExceptionMessage() { - requestHandler = new SqsMessageHandlerWithNonRetryableHandlerWithDelete(); - event.getRecords().get(0).setMessageId(""); - - requestHandler.handleRequest(event, context); - - verify(interactionClient).listQueues(); - ArgumentCaptor captor = ArgumentCaptor.forClass(DeleteMessageBatchRequest.class); - verify(sqsClient).deleteMessageBatch(captor.capture()); - verify(sqsClient, never()).sendMessageBatch(any(SendMessageBatchRequest.class)); - verify(sqsClient, never()).getQueueAttributes(any(GetQueueAttributesRequest.class)); - - assertThat(captor.getValue()) - .satisfies(deleteMessageBatchRequest -> assertThat(deleteMessageBatchRequest.entries()) - .hasSize(2) - .extracting("id") - .contains("", "2e1424d4-f796-459a-8184-9c92662be6da")); - } - - @Test - void shouldBatchProcessAndFailWithExceptionForNonRetryableExceptionAndNoDlq() { - requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); - - event.getRecords().get(0).setMessageId(""); - event.getRecords() - .forEach(sqsMessage -> sqsMessage.setEventSourceArn(sqsMessage.getEventSourceArn() + "-temp")); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn( - GetQueueAttributesResponse.builder() - .build()); - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> requestHandler.handleRequest(event, context)) - .satisfies(e -> - { - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("message") - .containsExactly("Invalid message and was moved to DLQ"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .containsExactly(""); - - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); - }); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - verify(sqsClient, never()).sendMessageBatch(any(SendMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessAndFailWithExceptionForNonRetryableExceptionWhenFailedParsingPolicy() { - requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); - event.getRecords().get(0).setMessageId(""); - event.getRecords() - .forEach(sqsMessage -> sqsMessage.setEventSourceArn(sqsMessage.getEventSourceArn() + "-temp-queue")); - - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "MalFormedRedrivePolicy"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn( - GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> requestHandler.handleRequest(event, context)) - .satisfies(e -> - { - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("message") - .containsExactly("Invalid message and was moved to DLQ"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .containsExactly(""); - - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); - }); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - verify(sqsClient, never()).sendMessageBatch(any(SendMessageBatchRequest.class)); - } - - @Test - void shouldBatchProcessAndMoveNonRetryableExceptionToDlqAndThrowException() throws IOException { - requestHandler = new SqsMessageHandlerWithNonRetryableHandler(); - event = MAPPER.readValue(this.getClass().getResource("/threeMessageSqsBatchEvent.json"), SQSEvent.class); - - event.getRecords().get(1).setMessageId(""); - - HashMap attributes = new HashMap<>(); - - attributes.put(QueueAttributeName.REDRIVE_POLICY, "{\n" + - " \"deadLetterTargetArn\": \"arn:aws:sqs:us-east-2:123456789012:retry-queue\",\n" + - " \"maxReceiveCount\": 2\n" + - "}"); - - when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenReturn( - GetQueueAttributesResponse.builder() - .attributes(attributes) - .build()); - - assertThatExceptionOfType(SQSBatchProcessingException.class) - .isThrownBy(() -> requestHandler.handleRequest(event, context)) - .satisfies(e -> - { - assertThat(e.getExceptions()) - .hasSize(1) - .extracting("message") - .containsExactly("Invalid message and should be reprocessed"); - - assertThat(e.getFailures()) - .hasSize(1) - .extracting("messageId") - .containsExactly("2e1424d4-f796-459a-9696-9c92662ba5da"); - - assertThat(e.successMessageReturnValues()) - .hasSize(1) - .contains("Success"); - }); - - verify(interactionClient).listQueues(); - verify(sqsClient).deleteMessageBatch(any(DeleteMessageBatchRequest.class)); - verify(sqsClient).sendMessageBatch(any(SendMessageBatchRequest.class)); - } - - private void setupContext() { - when(context.getFunctionName()).thenReturn("testFunction"); - when(context.getInvokedFunctionArn()).thenReturn("testArn"); - when(context.getFunctionVersion()).thenReturn("1"); - when(context.getMemoryLimitInMB()).thenReturn(10); - } -} \ No newline at end of file diff --git a/powertools-sqs/src/test/resources/SqsFifoBatchEvent.json b/powertools-sqs/src/test/resources/SqsFifoBatchEvent.json deleted file mode 100644 index 726e45ea1..000000000 --- a/powertools-sqs/src/test/resources/SqsFifoBatchEvent.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "Records": [ - { - "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082649183", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082649185", - "MessageGroupId": "Group1" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-east-1:906126917321:sqs-lambda-MySqsQueue-4DYWWJIE97N5", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6da", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649", - "MessageGroupId": "Group2" - }, - "messageAttributes": { - "Attribute3" : { - "binaryValue" : "MTEwMA==", - "dataType" : "Binary" - }, - "Attribute2" : { - "stringValue" : "123", - "dataType" : "Number" - }, - "Attribute1" : { - "stringValue" : "AttributeValue1", - "dataType" : "String" - } - }, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-east-1:906126917321:sqs-lambda-MySqsQueue-4DYWWJIE97N5", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-9696-9c92662ba5da", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649", - "MessageGroupId": "Group1" - }, - "messageAttributes": { - "Attribute2" : { - "stringValue" : "123", - "dataType" : "Number" - } - }, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-east-1:906126917321:sqs-lambda-MySqsQueue-4DYWWJIE97N5", - "awsRegion": "us-east-2" - } - ] -} \ No newline at end of file diff --git a/powertools-sqs/src/test/resources/sampleSqsBatchEvent.json b/powertools-sqs/src/test/resources/sampleSqsBatchEvent.json deleted file mode 100644 index 8a5fbf309..000000000 --- a/powertools-sqs/src/test/resources/sampleSqsBatchEvent.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "records": [ - { - "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082649183", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082649185" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6da", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - } - ] -} \ No newline at end of file diff --git a/powertools-sqs/src/test/resources/sampleSqsBatchEventBatchSize25.json b/powertools-sqs/src/test/resources/sampleSqsBatchEventBatchSize25.json deleted file mode 100644 index 31b8862ad..000000000 --- a/powertools-sqs/src/test/resources/sampleSqsBatchEventBatchSize25.json +++ /dev/null @@ -1,404 +0,0 @@ -{ - "records": [ - { - "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082649183", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082649185" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6da", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d3", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d4", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b5", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d6", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b7", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d8", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d9", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d10", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d11", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d12", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d13", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d14", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d15", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d16", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d17", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d18", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d19", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d20", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d21", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d22", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d23", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d24", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d25", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d26", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6d27", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - } - ] -} \ No newline at end of file diff --git a/powertools-sqs/src/test/resources/threeMessageSqsBatchEvent.json b/powertools-sqs/src/test/resources/threeMessageSqsBatchEvent.json deleted file mode 100644 index b3b61da3b..000000000 --- a/powertools-sqs/src/test/resources/threeMessageSqsBatchEvent.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "records": [ - { - "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082649183", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082649185" - }, - "messageAttributes": {}, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-1:906126917321:sqs-lambda-MySqsQueue-4DYWWJIE97N5", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-8184-9c92662be6da", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": { - "Attribute3" : { - "binaryValue" : "MTEwMA==", - "dataType" : "Binary" - }, - "Attribute2" : { - "stringValue" : "123", - "dataType" : "Number" - }, - "Attribute1" : { - "stringValue" : "AttributeValue1", - "dataType" : "String" - } - }, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-1:906126917321:sqs-lambda-MySqsQueue-4DYWWJIE97N5", - "awsRegion": "us-east-2" - }, - { - "messageId": "2e1424d4-f796-459a-9696-9c92662ba5da", - "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082650636", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082650649" - }, - "messageAttributes": { - "Attribute2" : { - "stringValue" : "123", - "dataType" : "Number" - } - }, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceArn": "arn:aws:sqs:us-east-1:906126917321:sqs-lambda-MySqsQueue-4DYWWJIE97N5", - "awsRegion": "us-east-2" - } - ] -} \ No newline at end of file diff --git a/powertools-test-suite/pom.xml b/powertools-test-suite/pom.xml deleted file mode 100644 index 50e473ad1..000000000 --- a/powertools-test-suite/pom.xml +++ /dev/null @@ -1,163 +0,0 @@ - - - - - 4.0.0 - - powertools-test-suite - jar - - - powertools-parent - software.amazon.lambda - 1.19.0-SNAPSHOT - - - Powertools for AWS Lambda (Java) library Test Suite - - A suite of tests for interactions between the various Powertools for AWS Lambda (Java) modules. - - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - - - - - software.amazon.lambda - powertools-core - - - org.apache.logging.log4j - log4j-jcl - - - com.amazonaws - aws-lambda-java-core - - - com.amazonaws - aws-lambda-java-events - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-sqs - - - - - org.junit.jupiter - junit-jupiter-api - test - - - org.junit.jupiter - junit-jupiter-engine - test - - - org.apache.commons - commons-lang3 - test - - - org.aspectj - aspectjweaver - test - - - org.assertj - assertj-core - test - - - org.skyscreamer - jsonassert - test - - - - - - - dev.aspectj - aspectj-maven-plugin - 1.13.1 - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-logging - - - software.amazon.lambda - powertools-tracing - - - software.amazon.lambda - powertools-sqs - - - - - - - compile - - - - - - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - - - \ No newline at end of file diff --git a/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/LoggingOrderTest.java b/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/LoggingOrderTest.java deleted file mode 100644 index 9e9c464a6..000000000 --- a/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/LoggingOrderTest.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.testsuite; - - -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonList; -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; -import com.amazonaws.xray.AWSXRay; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.channels.FileChannel; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.util.Map; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.ThreadContext; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import software.amazon.awssdk.core.ResponseInputStream; -import software.amazon.awssdk.http.AbortableInputStream; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor; -import software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect; -import software.amazon.lambda.powertools.sqs.SqsUtils; -import software.amazon.lambda.powertools.testsuite.handler.LoggingOrderMessageHandler; -import software.amazon.lambda.powertools.testsuite.handler.TracingLoggingStreamMessageHandler; - -public class LoggingOrderTest { - - private static final String BUCKET_NAME = "ms-extended-sqs-client"; - private static final String BUCKET_KEY = "c71eb2ae-37e0-4265-8909-32f4153faddf"; - - @Mock - private Context context; - - @Mock - private S3Client s3Client; - - @BeforeEach - void setUp() throws IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException { - openMocks(this); - SqsUtils.overrideS3Client(s3Client); - ThreadContext.clearAll(); - writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); - setupContext(); - //Make sure file is cleaned up before running full stack logging regression - FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); - resetLogLevel(Level.INFO); - AWSXRay.beginSegment(LoggingOrderTest.class.getName()); - } - - @AfterEach - void tearDown() { - AWSXRay.endSegment(); - } - - /** - * The SQSEvent payload will be altered by the @SqsLargeMessage annotation. Logging of the event should happen - * after the event has been altered - */ - @Test - public void testThatLoggingAnnotationActsLast() throws IOException { - ResponseInputStream s3Response = - new ResponseInputStream<>(GetObjectResponse.builder().build(), - AbortableInputStream.create(new ByteArrayInputStream("A big message".getBytes()))); - - when(s3Client.getObject(any(GetObjectRequest.class))).thenReturn(s3Response); - SQSEvent sqsEvent = messageWithBody( - "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"" + BUCKET_NAME + - "\",\"s3Key\":\"" + BUCKET_KEY + "\"}]"); - - LoggingOrderMessageHandler requestHandler = new LoggingOrderMessageHandler(); - requestHandler.handleRequest(sqsEvent, context); - - assertThat(Files.lines(Paths.get("target/logfile.json"))) - .hasSize(2) - .satisfies(line -> - { - Map actual = parseToMap(line.get(0)); - - String message = actual.get("message").toString(); - - assertThat(message) - .contains("A big message"); - }); - } - - @Test - public void testLoggingAnnotationActsAfterTracingForStreamingHandler() throws IOException { - - ByteArrayOutputStream output = new ByteArrayOutputStream(); - S3EventNotification s3EventNotification = s3EventNotification(); - - TracingLoggingStreamMessageHandler handler = new TracingLoggingStreamMessageHandler(); - handler.handleRequest(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(s3EventNotification)), - output, context); - - assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) - .isNotEmpty(); - } - - private void setupContext() { - when(context.getFunctionName()).thenReturn("testFunction"); - when(context.getInvokedFunctionArn()).thenReturn("testArn"); - when(context.getFunctionVersion()).thenReturn("1"); - when(context.getMemoryLimitInMB()).thenReturn(10); - when(context.getAwsRequestId()).thenReturn("RequestId"); - } - - private void resetLogLevel(Level level) - throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { - Method resetLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("resetLogLevels", Level.class); - resetLogLevels.setAccessible(true); - resetLogLevels.invoke(null, level); - writeStaticField(LambdaLoggingAspect.class, "LEVEL_AT_INITIALISATION", level, true); - } - - private Map parseToMap(String stringAsJson) { - try { - return new ObjectMapper().readValue(stringAsJson, Map.class); - } catch (JsonProcessingException e) { - fail("Failed parsing logger line " + stringAsJson); - return emptyMap(); - } - } - - private S3EventNotification s3EventNotification() { - S3EventNotification.S3EventNotificationRecord record = - new S3EventNotification.S3EventNotificationRecord("us-west-2", - "ObjectCreated:Put", - "aws:s3", - null, - "2.1", - new S3EventNotification.RequestParametersEntity("127.0.0.1"), - new S3EventNotification.ResponseElementsEntity("C3D13FE58DE4C810", - "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"), - new S3EventNotification.S3Entity("testConfigRule", - new S3EventNotification.S3BucketEntity("mybucket", - new S3EventNotification.UserIdentityEntity("A3NL1KOZZKExample"), - "arn:aws:s3:::mybucket"), - new S3EventNotification.S3ObjectEntity("HappyFace.jpg", - 1024L, - "d41d8cd98f00b204e9800998ecf8427e", - "096fKKXTRTtl3on89fVO.nfljtsv6qko", - "0055AED6DCD90281E5"), - "1.0"), - new S3EventNotification.UserIdentityEntity("AIDAJDPLRKLG7UEXAMPLE") - ); - - return new S3EventNotification(singletonList(record)); - } - - private SQSEvent messageWithBody(String messageBody) { - SQSEvent.SQSMessage sqsMessage = new SQSEvent.SQSMessage(); - sqsMessage.setBody(messageBody); - SQSEvent sqsEvent = new SQSEvent(); - sqsEvent.setRecords(singletonList(sqsMessage)); - return sqsEvent; - } -} \ No newline at end of file diff --git a/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/handler/LoggingOrderMessageHandler.java b/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/handler/LoggingOrderMessageHandler.java deleted file mode 100644 index 5592b1fd3..000000000 --- a/powertools-test-suite/src/test/java/software/amazon/lambda/powertools/testsuite/handler/LoggingOrderMessageHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * 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 - * 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. - * - */ - -package software.amazon.lambda.powertools.testsuite.handler; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.sqs.SqsLargeMessage; - -public class LoggingOrderMessageHandler implements RequestHandler { - - @Override - @SqsLargeMessage - @Logging(logEvent = true) - public String handleRequest(SQSEvent sqsEvent, Context context) { - return sqsEvent.getRecords().get(0).getBody(); - } -} diff --git a/powertools-tracing/pom.xml b/powertools-tracing/pom.xml index 26bc0aa16..a869679a8 100644 --- a/powertools-tracing/pom.xml +++ b/powertools-tracing/pom.xml @@ -24,41 +24,23 @@ powertools-parent software.amazon.lambda - 1.19.0-SNAPSHOT + 2.0.0-RC1 - Powertools for AWS Lambda (Java) library Tracing + Powertools for AWS Lambda (Java) - Tracing A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - + + org.aspectj + aspectjrt + provided + software.amazon.lambda - powertools-core + powertools-common software.amazon.awssdk @@ -68,10 +50,6 @@ com.amazonaws aws-lambda-java-core - - org.aspectj - aspectjrt - com.amazonaws aws-xray-recorder-sdk-core @@ -100,6 +78,21 @@ junit-jupiter-engine test + + org.mockito + mockito-core + test + + + org.slf4j + slf4j-simple + test + + + org.junit-pioneer + junit-pioneer + test + org.apache.commons commons-lang3 @@ -117,13 +110,100 @@ - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - \ No newline at end of file + + + generate-graalvm-files + + + org.mockito + mockito-subclass + 5.17.0 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-tracing,experimental-class-define-support + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + 5.17.0 + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.2 + true + + + test-native + + test + + test + + + + powertools-tracing + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --enable-url-protocols=http + --no-fallback + -Dorg.graalvm.nativeimage.imagecode=agent + -H:ClassInitialization=net.bytebuddy.ClassFileVersion:rerun + -H:ClassInitialization=net.bytebuddy.utility.dispatcher.JavaDispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.Invoker$Dispatcher:rerun + -H:ClassInitialization=net.bytebuddy.utility.GraalImageCode:rerun + -H:IncludeResources=version.properties + -H:IncludeResources=unreadable.properties + --initialize-at-build-time=org.slf4j.simple.SimpleLogger + --initialize-at-build-time=org.slf4j.LoggerFactory + --initialize-at-build-time=org.junit.Ignore + --initialize-at-build-time=java.lang.annotation.Annotation + --initialize-at-build-time=org.junit.runners.model.FrameworkField + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --verbose + --native-image-info + -H:+UnlockExperimentalVMOptions + -H:Log=registerResource:5 + + + + + + + + diff --git a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/Tracing.java b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/Tracing.java index 6f17a2e33..432e475a3 100644 --- a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/Tracing.java +++ b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/Tracing.java @@ -50,22 +50,6 @@ public @interface Tracing { String namespace() default ""; - /** - * @deprecated As of release 1.2.0, replaced by captureMode() - * in order to support different modes and support via - * environment variables - */ - @Deprecated - boolean captureResponse() default true; - - /** - * @deprecated As of release 1.2.0, replaced by captureMode() - * in order to support different modes and support via - * environment variables - */ - @Deprecated - boolean captureError() default true; - String segmentName() default ""; CaptureMode captureMode() default CaptureMode.ENVIRONMENT_VAR; diff --git a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/TracingUtils.java b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/TracingUtils.java index 47c4e7422..954ed7da4 100644 --- a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/TracingUtils.java +++ b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/TracingUtils.java @@ -14,7 +14,7 @@ package software.amazon.lambda.powertools.tracing; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.serviceName; import com.amazonaws.xray.AWSXRay; import com.amazonaws.xray.entities.Entity; diff --git a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspect.java b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspect.java index 62416fce6..a4a48532c 100644 --- a/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspect.java +++ b/powertools-tracing/src/main/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspect.java @@ -14,11 +14,11 @@ package software.amazon.lambda.powertools.tracing.internal; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.coldStartDone; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isColdStart; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isSamLocal; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.coldStartDone; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isColdStart; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isHandlerMethod; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isSamLocal; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.serviceName; import static software.amazon.lambda.powertools.tracing.TracingUtils.objectMapper; import com.amazonaws.xray.AWSXRay; @@ -83,9 +83,8 @@ public Object around(ProceedingJoinPoint pjp, private boolean captureResponse(Tracing powerToolsTracing) { switch (powerToolsTracing.captureMode()) { case ENVIRONMENT_VAR: - boolean captureResponse = environmentVariable("POWERTOOLS_TRACER_CAPTURE_RESPONSE"); - return isEnvironmentVariableSet("POWERTOOLS_TRACER_CAPTURE_RESPONSE") ? captureResponse : - powerToolsTracing.captureResponse(); + return isEnvironmentVariableSet("POWERTOOLS_TRACER_CAPTURE_RESPONSE") + && environmentVariable("POWERTOOLS_TRACER_CAPTURE_RESPONSE"); case RESPONSE: case RESPONSE_AND_ERROR: return true; @@ -98,9 +97,8 @@ private boolean captureResponse(Tracing powerToolsTracing) { private boolean captureError(Tracing powerToolsTracing) { switch (powerToolsTracing.captureMode()) { case ENVIRONMENT_VAR: - boolean captureError = environmentVariable("POWERTOOLS_TRACER_CAPTURE_ERROR"); - return isEnvironmentVariableSet("POWERTOOLS_TRACER_CAPTURE_ERROR") ? captureError : - powerToolsTracing.captureError(); + return isEnvironmentVariableSet("POWERTOOLS_TRACER_CAPTURE_ERROR") + && environmentVariable("POWERTOOLS_TRACER_CAPTURE_ERROR"); case ERROR: case RESPONSE_AND_ERROR: return true; diff --git a/powertools-tracing/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-tracing/jni-config.json b/powertools-tracing/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-tracing/jni-config.json new file mode 100644 index 000000000..2c4de0562 --- /dev/null +++ b/powertools-tracing/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-tracing/jni-config.json @@ -0,0 +1,26 @@ +[ +{ + "name":"java.lang.Boolean", + "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.String", + "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"org.apache.maven.surefire.booter.ForkedBooter", + "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] +}, +{ + "name":"sun.instrument.InstrumentationImpl", + "methods":[{"name":"","parameterTypes":["long","boolean","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] +}, +{ + "name":"sun.management.VMManagementImpl", + "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] +} +] diff --git a/powertools-tracing/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-tracing/reflect-config.json b/powertools-tracing/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-tracing/reflect-config.json new file mode 100644 index 000000000..94a514dee --- /dev/null +++ b/powertools-tracing/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-tracing/reflect-config.json @@ -0,0 +1,365 @@ +[ +{ + "name":"[Lcom.fasterxml.jackson.databind.deser.Deserializers;" +}, +{ + "name":"[Lcom.fasterxml.jackson.databind.ser.BeanSerializerModifier;" +}, +{ + "name":"[Lcom.fasterxml.jackson.databind.ser.Serializers;" +}, +{ + "name":"[Ljava.lang.StackTraceElement;" +}, +{ + "name":"[Ljava.lang.Throwable;" +}, +{ + "name":"com.amazonaws.services.lambda.runtime.Context", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getAwsRequestId","parameterTypes":[] }, {"name":"getClientContext","parameterTypes":[] }, {"name":"getFunctionName","parameterTypes":[] }, {"name":"getFunctionVersion","parameterTypes":[] }, {"name":"getIdentity","parameterTypes":[] }, {"name":"getInvokedFunctionArn","parameterTypes":[] }, {"name":"getLogGroupName","parameterTypes":[] }, {"name":"getLogStreamName","parameterTypes":[] }, {"name":"getLogger","parameterTypes":[] }, {"name":"getMemoryLimitInMB","parameterTypes":[] }, {"name":"getRemainingTimeInMillis","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.xray.entities.Cause", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.xray.entities.Entity", + "queryAllDeclaredMethods":true +}, +{ + "name":"com.amazonaws.xray.entities.EntityImpl", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"getAnnotations","parameterTypes":[] }, {"name":"getAws","parameterTypes":[] }, {"name":"getCause","parameterTypes":[] }, {"name":"getEndTime","parameterTypes":[] }, {"name":"getHttp","parameterTypes":[] }, {"name":"getId","parameterTypes":[] }, {"name":"getMetadata","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getNamespace","parameterTypes":[] }, {"name":"getParentId","parameterTypes":[] }, {"name":"getSql","parameterTypes":[] }, {"name":"getStartTime","parameterTypes":[] }, {"name":"getSubsegments","parameterTypes":[] }, {"name":"getTraceId","parameterTypes":[] }, {"name":"isError","parameterTypes":[] }, {"name":"isFault","parameterTypes":[] }, {"name":"isInProgress","parameterTypes":[] }, {"name":"isThrottle","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.xray.entities.Segment", + "queryAllDeclaredMethods":true +}, +{ + "name":"com.amazonaws.xray.entities.SegmentImpl", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getOrigin","parameterTypes":[] }, {"name":"getResourceArn","parameterTypes":[] }, {"name":"getService","parameterTypes":[] }, {"name":"getUser","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.xray.entities.Subsegment", + "queryAllDeclaredMethods":true +}, +{ + "name":"com.amazonaws.xray.entities.SubsegmentImpl", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getNamespace","parameterTypes":[] }, {"name":"getPrecursorIds","parameterTypes":[] }] +}, +{ + "name":"com.amazonaws.xray.strategy.sampling.manifest.SamplingRuleManifest", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setDefaultRule","parameterTypes":["com.amazonaws.xray.strategy.sampling.rule.SamplingRule"] }, {"name":"setRules","parameterTypes":["java.util.List"] }, {"name":"setVersion","parameterTypes":["int"] }] +}, +{ + "name":"com.amazonaws.xray.strategy.sampling.reservoir.Reservoir", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.xray.strategy.sampling.rule.SamplingRule", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setFixedTarget","parameterTypes":["int"] }, {"name":"setRate","parameterTypes":["float"] }] +}, +{ + "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.fasterxml.jackson.databind.ser.std.ToStringSerializer", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.tools.attach.VirtualMachine" +}, +{ + "name":"double", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.io.InputStream" +}, +{ + "name":"java.io.OutputStream" +}, +{ + "name":"java.io.Serializable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"handleRequest response"}] +}, +{ + "name":"java.lang.AutoCloseable", + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.Class", + "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] +}, +{ + "name":"java.lang.ClassLoader", + "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] +}, +{ + "name":"java.lang.Exception", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.Module", + "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] +}, +{ + "name":"java.lang.Object", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"handleRequest response"}], + "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"getHandleRequest response","parameterTypes":[] }, {"name":"isHandleRequest response","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] +}, +{ + "name":"java.lang.ProcessEnvironment", + "fields":[{"name":"theCaseInsensitiveEnvironment"}, {"name":"theEnvironment"}] +}, +{ + "name":"java.lang.ProcessHandle", + "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime", + "methods":[{"name":"version","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime$Version", + "methods":[{"name":"feature","parameterTypes":[] }] +}, +{ + "name":"java.lang.RuntimeException", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.StackTraceElement", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.StackWalker" +}, +{ + "name":"java.lang.String" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.Thread", + "fields":[{"name":"threadLocalRandomProbe"}], + "methods":[{"name":"getContextClassLoader","parameterTypes":[] }] +}, +{ + "name":"java.lang.Throwable", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"getCause","parameterTypes":[] }, {"name":"getLocalizedMessage","parameterTypes":[] }, {"name":"getMessage","parameterTypes":[] }, {"name":"getStackTrace","parameterTypes":[] }, {"name":"getSuppressed","parameterTypes":[] }] +}, +{ + "name":"java.lang.annotation.Retention", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.annotation.Target", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.invoke.MethodHandle", + "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles", + "methods":[{"name":"lookup","parameterTypes":[] }] +}, +{ + "name":"java.lang.invoke.MethodHandles$Lookup", + "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] +}, +{ + "name":"java.lang.invoke.MethodType", + "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] +}, +{ + "name":"java.lang.reflect.AccessibleObject", + "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] +}, +{ + "name":"java.lang.reflect.AnnotatedArrayType", + "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedType", + "methods":[{"name":"getType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Executable", + "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Parameter", + "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.security.SecureRandomParameters" +}, +{ + "name":"java.util.AbstractMap", + "fields":[{"name":"handleRequest response"}], + "methods":[{"name":"getHandleRequest response","parameterTypes":[] }, {"name":"isHandleRequest response","parameterTypes":[] }] +}, +{ + "name":"java.util.Collections$UnmodifiableMap", + "fields":[{"name":"m"}] +}, +{ + "name":"java.util.Map", + "fields":[{"name":"handleRequest response"}] +}, +{ + "name":"java.util.concurrent.ConcurrentHashMap", + "fields":[{"name":"handleRequest response"}], + "methods":[{"name":"getHandleRequest response","parameterTypes":[] }, {"name":"isHandleRequest response","parameterTypes":[] }] +}, +{ + "name":"java.util.concurrent.ConcurrentMap", + "fields":[{"name":"handleRequest response"}] +}, +{ + "name":"java.util.concurrent.ForkJoinTask", + "fields":[{"name":"aux"}, {"name":"status"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.Striped64", + "fields":[{"name":"base"}, {"name":"cellsBusy"}] +}, +{ + "name":"jdk.internal.misc.Unsafe" +}, + +{ + "name":"org.apache.commons.logging.LogFactory" +}, +{ + "name":"org.apache.commons.logging.impl.Jdk14Logger", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }, {"name":"setLogFactory","parameterTypes":["org.apache.commons.logging.LogFactory"] }] +}, +{ + "name":"org.apache.commons.logging.impl.Log4JLogger" +}, +{ + "name":"org.apache.commons.logging.impl.LogFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.apache.commons.logging.impl.WeakHashtable", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor", + "fields":[{"name":"IS_COLD_START"}] +}, +{ + "name":"software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabled", + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledExplicitlyForResponseAndError", + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForError", + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForResponse", + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForResponseWithCustomMapper", + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForResponseWithCustomMapper$ParentClass", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForStream", + "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForStreamWithNoMetaData", + "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledWithException", + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, +{ + "name":"software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledWithNoMetaData", + "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] +}, + +{ + "name":"software.amazon.lambda.powertools.tracing.nonhandler.PowerToolNonHandler", + "methods":[{"name":"doSomething","parameterTypes":[] }, {"name":"doSomethingCustomName","parameterTypes":[] }] +}, +{ + "name":"sun.reflect.ReflectionFactory", + "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] +}, +{ + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.security.SecureRandomParameters"] }] +}, +{ + "name":"sun.security.provider.SHA", + "methods":[{"name":"","parameterTypes":[] }] +} +] diff --git a/powertools-tracing/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-tracing/resource-config.json b/powertools-tracing/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-tracing/resource-config.json new file mode 100644 index 000000000..2ac72d18f --- /dev/null +++ b/powertools-tracing/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-tracing/resource-config.json @@ -0,0 +1,31 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/com.fasterxml.jackson.databind.Module\\E" + }, { + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.net.spi.URLStreamHandlerProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/org.apache.commons.logging.LogFactory\\E" + }, { + "pattern":"\\QMETA-INF/services/org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.configuration.Configuration\\E" + }, { + "pattern":"\\QMETA-INF/services/org.assertj.core.presentation.Representation\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\Qcom/amazonaws/xray/sdk.properties\\E" + }, { + "pattern":"\\Qcom/amazonaws/xray/strategy/sampling/DefaultSamplingRules.json\\E" + }, { + "pattern":"\\Qcommons-logging.properties\\E" + }, { + "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" + }]}, + "bundles":[] +} diff --git a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java index d61206886..1d108ed5f 100644 --- a/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java +++ b/powertools-tracing/src/test/java/software/amazon/lambda/powertools/tracing/internal/LambdaTracingAspectTest.java @@ -18,24 +18,25 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import com.amazonaws.xray.AWSXRay; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; + import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.SetEnvironmentVariable; import org.mockito.Mock; -import org.mockito.MockedStatic; -import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import com.amazonaws.xray.AWSXRay; + +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.tracing.handlers.PowerToolDisabled; import software.amazon.lambda.powertools.tracing.handlers.PowerToolDisabledForStream; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabled; @@ -47,9 +48,11 @@ import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledForStreamWithNoMetaData; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledWithException; import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledWithNoMetaData; -import software.amazon.lambda.powertools.tracing.handlers.PowerTracerToolEnabledWithNoMetaDataDeprecated; import software.amazon.lambda.powertools.tracing.nonhandler.PowerToolNonHandler; + +@SetEnvironmentVariable(key = "POWERTOOLS_TRACER_CAPTURE_RESPONSE", value = "false") +@SetEnvironmentVariable(key = "POWERTOOLS_TRACER_CAPTURE_ERROR", value = "false") class LambdaTracingAspectTest { private RequestHandler requestHandler; private RequestStreamHandler streamHandler; @@ -58,16 +61,6 @@ class LambdaTracingAspectTest { @Mock private Context context; - @BeforeAll - static void beforeAll() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_TRACER_CAPTURE_RESPONSE")).thenReturn(null); - mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_TRACER_CAPTURE_ERROR")).thenReturn(null); - mocked.when(() -> SystemWrapper.containsKey("POWERTOOLS_TRACER_CAPTURE_RESPONSE")).thenReturn(false); - mocked.when(() -> SystemWrapper.containsKey("POWERTOOLS_TRACER_CAPTURE_ERROR")).thenReturn(false); - } - } - @BeforeEach void setUp() throws IllegalAccessException { openMocks(this); @@ -114,6 +107,28 @@ void shouldCaptureNonHandlerMethodWithCustomSegmentName() { void shouldCaptureTraces() { requestHandler.handleRequest(new Object(), context); + assertThat(AWSXRay.getTraceEntity()) + .isNotNull(); + + assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) + .hasSize(1) + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "lambdaHandler"); + + assertThat(subsegment.getMetadata()) + .hasSize(0); + }); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_TRACER_CAPTURE_RESPONSE", value = "true") + void shouldCaptureTracesWithResponseMetadata() { + requestHandler.handleRequest(new Object(), context); + assertThat(AWSXRay.getTraceEntity()) .isNotNull(); @@ -133,6 +148,7 @@ void shouldCaptureTraces() { } @Test + @SetEnvironmentVariable(key = "POWERTOOLS_TRACER_CAPTURE_ERROR", value = "true") void shouldCaptureTracesWithExceptionMetaData() { requestHandler = new PowerTracerToolEnabledWithException(); @@ -164,6 +180,25 @@ void shouldCaptureTracesWithExceptionMetaData() { void shouldCaptureTracesForStream() throws IOException { streamHandler.handleRequest(new ByteArrayInputStream("test".getBytes()), new ByteArrayOutputStream(), context); + assertThat(AWSXRay.getTraceEntity()) + .isNotNull(); + + assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) + .hasSize(1) + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "streamHandler"); + }); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_TRACER_CAPTURE_RESPONSE", value = "true") + void shouldCaptureTracesForStreamWithResponseMetadata() throws IOException { + streamHandler.handleRequest(new ByteArrayInputStream("test".getBytes()), new ByteArrayOutputStream(), context); + assertThat(AWSXRay.getTraceEntity()) .isNotNull(); @@ -247,9 +282,8 @@ void shouldCaptureTracesForStreamWithNoMetadata() throws IOException { } @Test - void shouldCaptureTracesWithNoMetadataDeprecated() { - requestHandler = new PowerTracerToolEnabledWithNoMetaDataDeprecated(); - + @SetEnvironmentVariable(key = "POWERTOOLS_TRACER_CAPTURE_RESPONSE", value = "false") + void shouldNotCaptureTracesIfDisabledViaEnvironmentVariable() { requestHandler.handleRequest(new Object(), context); assertThat(AWSXRay.getTraceEntity()) @@ -262,7 +296,7 @@ void shouldCaptureTracesWithNoMetadataDeprecated() { assertThat(subsegment.getAnnotations()) .hasSize(2) .containsEntry("ColdStart", true) - .containsEntry("Service", "service_undefined"); + .containsEntry("Service", "lambdaHandler"); assertThat(subsegment.getMetadata()) .isEmpty(); @@ -270,56 +304,28 @@ void shouldCaptureTracesWithNoMetadataDeprecated() { } @Test - void shouldNotCaptureTracesIfDisabledViaEnvironmentVariable() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.containsKey("POWERTOOLS_TRACER_CAPTURE_RESPONSE")).thenReturn(true); - mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_TRACER_CAPTURE_RESPONSE")).thenReturn("false"); - - requestHandler.handleRequest(new Object(), context); - - assertThat(AWSXRay.getTraceEntity()) - .isNotNull(); - - assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) - .hasSize(1) - .allSatisfy(subsegment -> - { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "lambdaHandler"); - - assertThat(subsegment.getMetadata()) - .isEmpty(); - }); - } - } - - @Test + @SetEnvironmentVariable(key = "POWERTOOLS_TRACER_CAPTURE_RESPONSE", value = "false") void shouldCaptureTracesIfExplicitlyEnabledAndEnvironmentVariableIsDisabled() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_TRACER_CAPTURE_RESPONSE")).thenReturn("false"); - requestHandler = new PowerTracerToolEnabledForResponse(); - - requestHandler.handleRequest(new Object(), context); - - assertThat(AWSXRay.getTraceEntity()) - .isNotNull(); - - assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) - .hasSize(1) - .allSatisfy(subsegment -> - { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "lambdaHandler"); - - assertThat(subsegment.getMetadata()) - .hasSize(1) - .containsKey("lambdaHandler"); - }); - } + requestHandler = new PowerTracerToolEnabledForResponse(); + + requestHandler.handleRequest(new Object(), context); + + assertThat(AWSXRay.getTraceEntity()) + .isNotNull(); + + assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) + .hasSize(1) + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "lambdaHandler"); + + assertThat(subsegment.getMetadata()) + .hasSize(1) + .containsKey("lambdaHandler"); + }); } @Test @@ -350,95 +356,85 @@ void shouldCaptureTracesForSelfReferencingReturnTypesViaCustomMapper() { } @Test + @SetEnvironmentVariable(key = "POWERTOOLS_TRACER_CAPTURE_RESPONSE", value = "false") + @SetEnvironmentVariable(key = "POWERTOOLS_TRACER_CAPTURE_ERROR", value = "false") void shouldCaptureTracesIfExplicitlyEnabledBothAndEnvironmentVariableIsDisabled() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.containsKey("POWERTOOLS_TRACER_CAPTURE_RESPONSE")).thenReturn(true); - mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_TRACER_CAPTURE_RESPONSE")).thenReturn("false"); - mocked.when(() -> SystemWrapper.containsKey("POWERTOOLS_TRACER_CAPTURE_ERROR")).thenReturn(true); - mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_TRACER_CAPTURE_ERROR")).thenReturn("false"); - requestHandler = new PowerTracerToolEnabledExplicitlyForResponseAndError(); - - requestHandler.handleRequest(new Object(), context); - - assertThat(AWSXRay.getTraceEntity()) - .isNotNull(); - - assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) - .hasSize(1) - .allSatisfy(subsegment -> - { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "lambdaHandler"); - - assertThat(subsegment.getMetadata()) - .hasSize(1) - .containsKey("lambdaHandler"); - }); - } + requestHandler = new PowerTracerToolEnabledExplicitlyForResponseAndError(); + + requestHandler.handleRequest(new Object(), context); + + assertThat(AWSXRay.getTraceEntity()) + .isNotNull(); + + assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) + .hasSize(1) + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "lambdaHandler"); + + assertThat(subsegment.getMetadata()) + .hasSize(1) + .containsKey("lambdaHandler"); + }); } @Test + @SetEnvironmentVariable(key = "POWERTOOLS_TRACER_CAPTURE_ERROR", value = "false") void shouldNotCaptureTracesWithExceptionMetaDataIfDisabledViaEnvironmentVariable() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.containsKey("POWERTOOLS_TRACER_CAPTURE_ERROR")).thenReturn(true); - mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_TRACER_CAPTURE_ERROR")).thenReturn("false"); - requestHandler = new PowerTracerToolEnabledWithException(); - - Throwable throwable = catchThrowable(() -> requestHandler.handleRequest(new Object(), context)); - - assertThat(throwable) - .isInstanceOf(RuntimeException.class); - - assertThat(AWSXRay.getTraceEntity()) - .isNotNull(); - - assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) - .hasSize(1) - .allSatisfy(subsegment -> - { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "lambdaHandler"); - - assertThat(subsegment.getMetadata()) - .isEmpty(); - }); - } + requestHandler = new PowerTracerToolEnabledWithException(); + + Throwable throwable = catchThrowable(() -> requestHandler.handleRequest(new Object(), context)); + + assertThat(throwable) + .isInstanceOf(RuntimeException.class); + + assertThat(AWSXRay.getTraceEntity()) + .isNotNull(); + + assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) + .hasSize(1) + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "lambdaHandler"); + + assertThat(subsegment.getMetadata()) + .isEmpty(); + }); } @Test + @SetEnvironmentVariable(key = "POWERTOOLS_TRACER_CAPTURE_ERROR", value = "false") void shouldCaptureTracesWithExceptionMetaDataEnabledExplicitlyAndEnvironmentVariableDisabled() { - try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { - mocked.when(() -> SystemWrapper.getenv("POWERTOOLS_TRACER_CAPTURE_ERROR")).thenReturn("false"); - - requestHandler = new PowerTracerToolEnabledForError(); - - Throwable exception = catchThrowable(() -> requestHandler.handleRequest(new Object(), context)); - - assertThat(AWSXRay.getTraceEntity()) - .isNotNull(); - - assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) - .hasSize(1) - .allSatisfy(subsegment -> - { - assertThat(subsegment.getAnnotations()) - .hasSize(2) - .containsEntry("ColdStart", true) - .containsEntry("Service", "lambdaHandler"); - - assertThat(subsegment.getMetadata()) - .hasSize(1) - .containsKey("lambdaHandler"); - - assertThat(subsegment.getMetadata().get("lambdaHandler")) - .satisfies(stringObjectMap -> assertThat(stringObjectMap) - .containsEntry("handleRequest error", exception)); - }); - } + requestHandler = new PowerTracerToolEnabledForError(); + + Throwable exception = catchThrowable(() -> requestHandler.handleRequest(new Object(), context)); + + assertThat(AWSXRay.getTraceEntity()) + .isNotNull(); + + assertThat(AWSXRay.getTraceEntity().getSubsegmentsCopy()) + .hasSize(1) + .allSatisfy(subsegment -> + { + assertThat(subsegment.getAnnotations()) + .hasSize(2) + .containsEntry("ColdStart", true) + .containsEntry("Service", "lambdaHandler"); + + assertThat(subsegment.getMetadata()) + .hasSize(1) + .containsKey("lambdaHandler"); + + assertThat(subsegment.getMetadata().get("lambdaHandler")) + .satisfies(stringObjectMap -> assertThat(stringObjectMap) + .containsEntry("handleRequest error", exception)); + }); } private void setupContext() { diff --git a/powertools-tracing/src/test/resources/log4j2.xml b/powertools-tracing/src/test/resources/log4j2.xml index 108e32b75..030d11725 100644 --- a/powertools-tracing/src/test/resources/log4j2.xml +++ b/powertools-tracing/src/test/resources/log4j2.xml @@ -2,7 +2,7 @@ - + diff --git a/powertools-validation/pom.xml b/powertools-validation/pom.xml index 11ad1398f..8720509bb 100644 --- a/powertools-validation/pom.xml +++ b/powertools-validation/pom.xml @@ -24,41 +24,23 @@ powertools-parent software.amazon.lambda - 1.19.0-SNAPSHOT + 2.0.0-RC1 - Powertools for AWS Lambda (Java) validation library + Powertools for AWS Lambda (Java) - Validation Json schema validation for Lambda events and responses - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - + + org.aspectj + aspectjrt + provided + software.amazon.lambda - powertools-core + powertools-common software.amazon.lambda @@ -80,14 +62,10 @@ com.fasterxml.jackson.core jackson-databind - - org.aspectj - aspectjrt - com.networknt json-schema-validator - 1.0.87 + 1.5.1 com.amazonaws @@ -105,7 +83,21 @@ junit-jupiter-engine test - + + org.slf4j + slf4j-simple + test + + + org.mockito + mockito-core + test + + + com.amazonaws + aws-lambda-java-tests + test + org.apache.commons commons-lang3 @@ -127,14 +119,4 @@ test - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - \ No newline at end of file + diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationConfig.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationConfig.java index 143f0584d..3f643ab00 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationConfig.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationConfig.java @@ -47,9 +47,15 @@ public SpecVersion.VersionFlag getSchemaVersion() { } /** - * Set the version of the json schema specifications (default is V7) + * Set the version of the json schema specifications to use if $schema is not + * explicitly specified within the schema (default is V7). If $schema is + * explicitly specified within the schema is explicitly specified within the + * schema, the validator will use the specified dialect. * * @param version May be V4, V6, V7, V201909 or V202012 + * @see Declaring + * a Dialect */ public void setSchemaVersion(SpecVersion.VersionFlag version) { if (version != jsonSchemaVersion) { @@ -97,6 +103,6 @@ public ObjectMapper getObjectMapper() { } private static class ConfigHolder { - private final static ValidationConfig instance = new ValidationConfig(); + private static final ValidationConfig instance = new ValidationConfig(); } } diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java index 221e5fb1d..35b309f07 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationUtils.java @@ -21,18 +21,16 @@ import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.NullNode; import com.networknt.schema.JsonSchema; +import com.networknt.schema.SchemaLocation; import com.networknt.schema.SchemaValidatorsConfig; import com.networknt.schema.ValidationMessage; -import com.networknt.schema.uri.URITranslator; import io.burt.jmespath.Expression; import java.io.ByteArrayOutputStream; -import java.io.InputStream; import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -import software.amazon.lambda.powertools.validation.internal.ValidationAspect; /** * Validation utility, used to manually validate Json against Json Schema @@ -255,27 +253,26 @@ public static JsonSchema getJsonSchema(String schema, boolean validateSchema) { private static JsonSchema createJsonSchema(String schema) { JsonSchema jsonSchema; + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().formatAssertionsEnabled(true) + .preloadJsonSchemaRefMaxNestingDepth(10).build(); if (schema.startsWith(CLASSPATH)) { - String filePath = schema.substring(CLASSPATH.length()); - try (InputStream schemaStream = ValidationAspect.class.getResourceAsStream(filePath)) { - - SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - config.addUriTranslator(URITranslator.prefix("https://json-schema.org", "resource:")); - - jsonSchema = ValidationConfig.get().getFactory().getSchema(schemaStream, config); + try { + jsonSchema = ValidationConfig.get().getFactory().getSchema(SchemaLocation.of(schema), config); } catch (Exception e) { + String filePath = schema.substring(CLASSPATH.length()); throw new IllegalArgumentException( - "'" + schema + "' is invalid, verify '" + filePath + "' is in your classpath"); + "'" + schema + "' is invalid, verify '" + filePath + "' is in your classpath", e); } } else { - jsonSchema = ValidationConfig.get().getFactory().getSchema(schema); + jsonSchema = ValidationConfig.get().getFactory().getSchema(schema, config); } return jsonSchema; } private static void validateSchema(String schema, JsonSchema jsonSchema) { - String schemaId = ValidationConfig.get().getSchemaVersion().getId().replace("https://json-schema.org", ""); + String schemaId = jsonSchema.getValidationContext().getMetaSchema().getIri() + .replace("https://json-schema.org", "").replace("http://json-schema.org", ""); try { validate(jsonSchema.getSchemaNode(), getJsonSchema(CLASSPATH + schemaId)); diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java index 6055f8d58..68900d334 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java @@ -16,7 +16,7 @@ import static com.networknt.schema.SpecVersion.VersionFlag.V201909; import static java.nio.charset.StandardCharsets.UTF_8; -import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.placedOnRequestHandler; import static software.amazon.lambda.powertools.utilities.jmespath.Base64Function.decode; import static software.amazon.lambda.powertools.utilities.jmespath.Base64GZipFunction.decompress; import static software.amazon.lambda.powertools.validation.ValidationUtils.getJsonSchema; @@ -40,9 +40,16 @@ import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent; import com.amazonaws.services.lambda.runtime.events.RabbitMQEvent; import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; import com.amazonaws.services.lambda.runtime.events.SQSEvent; import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; +import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; import com.networknt.schema.JsonSchema; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -51,9 +58,10 @@ import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.validation.Validation; import software.amazon.lambda.powertools.validation.ValidationConfig; +import software.amazon.lambda.powertools.validation.ValidationException; /** - * Aspect for {@link Validation} annotation + * Aspect for {@link Validation} annotation. Internal to Powertools, use the annotation itself. */ @Aspect public class ValidationAspect { @@ -74,6 +82,12 @@ public Object around(ProceedingJoinPoint pjp, ValidationConfig.get().setSchemaVersion(validation.schemaVersion()); } + // we need this result object to be null at this point as validation of API events, if + // it fails, will catch the ValidationException and generate a 400 API response. This response + // will be stored in the result object to prevent executing the lambda + Object validationResult = null; + boolean failFast = false; + if (placedOnRequestHandler(pjp)) { validationNeeded = true; @@ -85,16 +99,19 @@ public Object around(ProceedingJoinPoint pjp, validate(obj, inboundJsonSchema, validation.envelope()); } else if (obj instanceof APIGatewayProxyRequestEvent) { APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) obj; - validate(event.getBody(), inboundJsonSchema); + validationResult = validateAPIGatewayProxyBody(event.getBody(), inboundJsonSchema, null, null); + failFast = true; } else if (obj instanceof APIGatewayV2HTTPEvent) { APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) obj; - validate(event.getBody(), inboundJsonSchema); + validationResult = validateAPIGatewayV2HTTPBody(event.getBody(), inboundJsonSchema, null, null); + failFast = true; } else if (obj instanceof SNSEvent) { SNSEvent event = (SNSEvent) obj; - event.getRecords().forEach(record -> validate(record.getSNS().getMessage(), inboundJsonSchema)); + event.getRecords() + .forEach(snsRecord -> validate(snsRecord.getSNS().getMessage(), inboundJsonSchema)); } else if (obj instanceof SQSEvent) { SQSEvent event = (SQSEvent) obj; - event.getRecords().forEach(record -> validate(record.getBody(), inboundJsonSchema)); + validationResult = validateSQSEventMessages(event.getRecords(), inboundJsonSchema); } else if (obj instanceof ScheduledEvent) { ScheduledEvent event = (ScheduledEvent) obj; validate(event.getDetail(), inboundJsonSchema); @@ -109,30 +126,32 @@ public Object around(ProceedingJoinPoint pjp, validate(event.getResourceProperties(), inboundJsonSchema); } else if (obj instanceof KinesisEvent) { KinesisEvent event = (KinesisEvent) obj; - event.getRecords() - .forEach(record -> validate(decode(record.getKinesis().getData()), inboundJsonSchema)); + validationResult = validateKinesisEventRecords(event.getRecords(), inboundJsonSchema); } else if (obj instanceof KinesisFirehoseEvent) { KinesisFirehoseEvent event = (KinesisFirehoseEvent) obj; - event.getRecords().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); + event.getRecords() + .forEach(eventRecord -> validate(decode(eventRecord.getData()), inboundJsonSchema)); } else if (obj instanceof KafkaEvent) { KafkaEvent event = (KafkaEvent) obj; event.getRecords().forEach((s, records) -> records.forEach( - record -> validate(decode(record.getValue()), inboundJsonSchema))); + eventRecord -> validate(decode(eventRecord.getValue()), inboundJsonSchema))); } else if (obj instanceof ActiveMQEvent) { ActiveMQEvent event = (ActiveMQEvent) obj; - event.getMessages().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); + event.getMessages().forEach(message -> validate(decode(message.getData()), inboundJsonSchema)); } else if (obj instanceof RabbitMQEvent) { RabbitMQEvent event = (RabbitMQEvent) obj; event.getRmqMessagesByQueue().forEach((s, records) -> records.forEach( - record -> validate(decode(record.getData()), inboundJsonSchema))); + message -> validate(decode(message.getData()), inboundJsonSchema))); } else if (obj instanceof KinesisAnalyticsFirehoseInputPreprocessingEvent) { KinesisAnalyticsFirehoseInputPreprocessingEvent event = (KinesisAnalyticsFirehoseInputPreprocessingEvent) obj; - event.getRecords().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); + event.getRecords() + .forEach(eventRecord -> validate(decode(eventRecord.getData()), inboundJsonSchema)); } else if (obj instanceof KinesisAnalyticsStreamsInputPreprocessingEvent) { KinesisAnalyticsStreamsInputPreprocessingEvent event = (KinesisAnalyticsStreamsInputPreprocessingEvent) obj; - event.getRecords().forEach(record -> validate(decode(record.getData()), inboundJsonSchema)); + event.getRecords() + .forEach(eventRecord -> validate(decode(eventRecord.getData()), inboundJsonSchema)); } else { LOG.warn("Unhandled event type {}, please use the 'envelope' parameter to specify what to validate", obj.getClass().getName()); @@ -140,33 +159,183 @@ record -> validate(decode(record.getData()), inboundJsonSchema))); } } - Object result = pjp.proceed(proceedArgs); - - if (validationNeeded && !validation.outboundSchema().isEmpty()) { - JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true); - - if (result instanceof APIGatewayProxyResponseEvent) { - APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result; - validate(response.getBody(), outboundJsonSchema); - } else if (result instanceof APIGatewayV2HTTPResponse) { - APIGatewayV2HTTPResponse response = (APIGatewayV2HTTPResponse) result; - validate(response.getBody(), outboundJsonSchema); - } else if (result instanceof APIGatewayV2WebSocketResponse) { - APIGatewayV2WebSocketResponse response = (APIGatewayV2WebSocketResponse) result; - validate(response.getBody(), outboundJsonSchema); - } else if (result instanceof ApplicationLoadBalancerResponseEvent) { - ApplicationLoadBalancerResponseEvent response = (ApplicationLoadBalancerResponseEvent) result; - validate(response.getBody(), outboundJsonSchema); - } else if (result instanceof KinesisAnalyticsInputPreprocessingResponse) { - KinesisAnalyticsInputPreprocessingResponse response = - (KinesisAnalyticsInputPreprocessingResponse) result; - response.getRecords().forEach(record -> validate(decode(record.getData()), outboundJsonSchema)); - } else { - LOG.warn("Unhandled response type {}, please use the 'envelope' parameter to specify what to validate", - result.getClass().getName()); + Object result; + + // don't execute the lambda if result was set by previous validation step and should fail fast + // in that case result should already hold a response with validation information + if (failFast && validationResult != null) { + LOG.error("Incoming API event's body failed inbound schema validation."); + return validationResult; + } else { + result = pjp.proceed(proceedArgs); + + if (validationResult != null && result != null) { + // in the case of batches (SQS, Kinesis), we copy the batch item failures to the result + if (result instanceof SQSBatchResponse && validationResult instanceof SQSBatchResponse) { + SQSBatchResponse validationResponse = (SQSBatchResponse) validationResult; + SQSBatchResponse response = (SQSBatchResponse) result; + if (response.getBatchItemFailures() == null) { + response.setBatchItemFailures(validationResponse.getBatchItemFailures()); + } else { + response.getBatchItemFailures().addAll(validationResponse.getBatchItemFailures()); + } + } else if (result instanceof StreamsEventResponse && validationResult instanceof StreamsEventResponse) { + StreamsEventResponse validationResponse = (StreamsEventResponse) validationResult; + StreamsEventResponse response = (StreamsEventResponse) result; + if (response.getBatchItemFailures() == null) { + response.setBatchItemFailures(validationResponse.getBatchItemFailures()); + } else { + response.getBatchItemFailures().addAll(validationResponse.getBatchItemFailures()); + } + } + } + + if (result != null && validationNeeded && !validation.outboundSchema().isEmpty()) { + JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true); + + Object overridenResponse = null; + // The normal behavior of @Validation is to throw an exception if response's validation fails. + // but in the case of APIGatewayProxyResponseEvent and APIGatewayV2HTTPResponse we want to return + // a 400 response with the validation errors instead of throwing an exception. + if (result instanceof APIGatewayProxyResponseEvent) { + APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result; + overridenResponse = + validateAPIGatewayProxyBody(response.getBody(), outboundJsonSchema, response.getHeaders(), + response.getMultiValueHeaders()); + } else if (result instanceof APIGatewayV2HTTPResponse) { + APIGatewayV2HTTPResponse response = (APIGatewayV2HTTPResponse) result; + overridenResponse = + validateAPIGatewayV2HTTPBody(response.getBody(), outboundJsonSchema, response.getHeaders(), + response.getMultiValueHeaders()); + // all type of below responses will throw an exception if validation fails + } else if (result instanceof APIGatewayV2WebSocketResponse) { + APIGatewayV2WebSocketResponse response = (APIGatewayV2WebSocketResponse) result; + validate(response.getBody(), outboundJsonSchema); + } else if (result instanceof ApplicationLoadBalancerResponseEvent) { + ApplicationLoadBalancerResponseEvent response = (ApplicationLoadBalancerResponseEvent) result; + validate(response.getBody(), outboundJsonSchema); + } else if (result instanceof KinesisAnalyticsInputPreprocessingResponse) { + KinesisAnalyticsInputPreprocessingResponse response = + (KinesisAnalyticsInputPreprocessingResponse) result; + response.getRecords().forEach(record -> validate(decode(record.getData()), outboundJsonSchema)); + } else { + LOG.warn( + "Unhandled response type {}, please use the 'envelope' parameter to specify what to validate", + result.getClass().getName()); + } + + if (overridenResponse != null) { + result = overridenResponse; + LOG.error("API response failed outbound schema validation."); + } } } return result; } + + /** + * Validate each Kinesis record body. If an error occurs, do not fail the whole batch but only add invalid items in BatchItemFailure. + * Note that the valid records will be decoded twice (during validation and within the handler by the user), which will slightly reduce performance. + * @param records Kinesis records + * @param inboundJsonSchema validation schema + * @return the stream response with items in failure + */ + private StreamsEventResponse validateKinesisEventRecords(List records, + JsonSchema inboundJsonSchema) { + StreamsEventResponse response = StreamsEventResponse.builder().withBatchItemFailures(new ArrayList<>()).build(); + + ListIterator listIterator = records.listIterator(); // using iterator to remove while browsing + while (listIterator.hasNext()) { + KinesisEvent.KinesisEventRecord eventRecord = listIterator.next(); + try { + validate(decode(eventRecord.getKinesis().getData()), inboundJsonSchema); + } catch (ValidationException e) { + LOG.error("Validation error on message {}: {}", eventRecord.getKinesis().getSequenceNumber(), + e.getMessage()); + listIterator.remove(); + response.getBatchItemFailures().add(StreamsEventResponse.BatchItemFailure.builder() + .withItemIdentifier(eventRecord.getKinesis().getSequenceNumber()).build()); + } + } + return response; + } + + /** + * Validate each SQS message body. If an error occurs, do not fail the whole batch but only add invalid items in BatchItemFailure. + * + * @param messages SQS messages + * @param inboundJsonSchema validation schema + * @return the SQS batch response + */ + private SQSBatchResponse validateSQSEventMessages(List messages, + JsonSchema inboundJsonSchema) { + SQSBatchResponse response = SQSBatchResponse.builder().withBatchItemFailures(new ArrayList<>()).build(); + ListIterator listIterator = messages.listIterator(); // using iterator to remove while browsing + while (listIterator.hasNext()) { + SQSEvent.SQSMessage message = listIterator.next(); + try { + validate(message.getBody(), inboundJsonSchema); + } catch (ValidationException e) { + LOG.error("Validation error on message {}: {}", message.getMessageId(), e.getMessage()); + listIterator.remove(); + response.getBatchItemFailures() + .add(SQSBatchResponse.BatchItemFailure.builder().withItemIdentifier(message.getMessageId()) + .build()); + } + } + return response; + } + + /** + * Validates the given body against the provided JsonSchema. If validation fails the ValidationException + * will be catched and transformed to a 400, bad request, API response + * + * @param body body of the event to validate + * @param jsonSchema validation schema + * @return null if validation passed, or a 400 response object otherwise + */ + private APIGatewayProxyResponseEvent validateAPIGatewayProxyBody(final String body, final JsonSchema jsonSchema, + final Map headers, + Map> multivalueHeaders) { + APIGatewayProxyResponseEvent result = null; + try { + validate(body, jsonSchema); + } catch (ValidationException e) { + LOG.error("There were validation errors: {}", e.getMessage()); + result = new APIGatewayProxyResponseEvent(); + result.setBody(e.getMessage()); + result.setHeaders(headers == null ? Collections.emptyMap() : headers); + result.setMultiValueHeaders(multivalueHeaders == null ? Collections.emptyMap() : multivalueHeaders); + result.setStatusCode(400); + result.setIsBase64Encoded(false); + } + return result; + } + + /** + * Validates the given body against the provided JsonSchema. If validation fails the ValidationException + * will be catched and transformed to a 400, bad request, API response + * + * @param body body of the event to validate + * @param jsonSchema validation schema + * @return null if validation passed, or a 400 response object otherwise + */ + private APIGatewayV2HTTPResponse validateAPIGatewayV2HTTPBody(final String body, final JsonSchema jsonSchema, + final Map headers, + Map> multivalueHeaders) { + APIGatewayV2HTTPResponse result = null; + try { + validate(body, jsonSchema); + } catch (ValidationException e) { + LOG.error("There were validation errors: {}", e.getMessage()); + result = new APIGatewayV2HTTPResponse(); + result.setBody(e.getMessage()); + result.setHeaders(headers == null ? Collections.emptyMap() : headers); + result.setMultiValueHeaders(multivalueHeaders == null ? Collections.emptyMap() : multivalueHeaders); + result.setStatusCode(400); + result.setIsBase64Encoded(false); + } + return result; + } } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/ValidationUtilsTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/ValidationUtilsTest.java index d80670669..73c3c6567 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/ValidationUtilsTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/ValidationUtilsTest.java @@ -48,7 +48,7 @@ public void testLoadSchemaV7OK() { ValidationConfig.get().setSchemaVersion(SpecVersion.VersionFlag.V7); JsonSchema jsonSchema = getJsonSchema("classpath:/schema_v7.json", true); assertThat(jsonSchema).isNotNull(); - assertThat(jsonSchema.getCurrentUri()).asString().isEqualTo("http://example.com/product.json"); + assertThat(jsonSchema.getId()).isEqualTo("http://example.com/product.json"); } @Test @@ -57,7 +57,7 @@ public void testLoadSchemaV7KO() { assertThatThrownBy(() -> getJsonSchema("classpath:/schema_v7_ko.json", true)) .isInstanceOf(IllegalArgumentException.class) .hasMessage( - "The schema classpath:/schema_v7_ko.json is not valid, it does not respect the specification /draft-07/schema"); + "The schema classpath:/schema_v7_ko.json is not valid, it does not respect the specification /draft-07/schema#"); } @Test diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/LambdaHandlerApiGateway.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java similarity index 54% rename from powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/LambdaHandlerApiGateway.java rename to powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java index 3bad9644f..74e8605a5 100644 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/LambdaHandlerApiGateway.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java @@ -12,21 +12,23 @@ * */ -package software.amazon.lambda.powertools.sqs.handlers; +package software.amazon.lambda.powertools.validation.handlers; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import software.amazon.lambda.powertools.sqs.SampleSqsHandler; -import software.amazon.lambda.powertools.sqs.SqsBatch; -import software.amazon.lambda.powertools.sqs.SqsLargeMessage; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -public class LambdaHandlerApiGateway implements RequestHandler { +import software.amazon.lambda.powertools.validation.Validation; +public class GenericSchemaV7APIGatewayProxyRequestEventHandler implements RequestHandler { + + @Validation(inboundSchema = "classpath:/schema_v7.json") @Override - @SqsLargeMessage - @SqsBatch(value = SampleSqsHandler.class) - public String handleRequest(APIGatewayProxyRequestEvent sqsEvent, Context context) { - return sqsEvent.getBody(); + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent(); + response.setBody("valid-test"); + response.setStatusCode(200); + return response; } -} +} \ No newline at end of file diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7StringHandler.java similarity index 92% rename from powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java rename to powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7StringHandler.java index 5b8343d1b..ab0645f29 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7StringHandler.java @@ -16,13 +16,14 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; + import software.amazon.lambda.powertools.validation.Validation; -public class GenericSchemaV7Handler implements RequestHandler { +public class GenericSchemaV7StringHandler implements RequestHandler { @Validation(inboundSchema = "classpath:/schema_v7.json") @Override public String handleRequest(T input, Context context) { return "OK"; } -} +} \ No newline at end of file diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/KinesisHandlerWithError.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/KinesisHandlerWithError.java new file mode 100644 index 000000000..e6e702fb6 --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/KinesisHandlerWithError.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.validation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; +import java.util.ArrayList; +import software.amazon.lambda.powertools.validation.Validation; + +public class KinesisHandlerWithError implements RequestHandler { + + @Override + @Validation(inboundSchema = "classpath:/schema_v7.json") + public StreamsEventResponse handleRequest(KinesisEvent input, Context context) { + StreamsEventResponse response = StreamsEventResponse.builder().withBatchItemFailures(new ArrayList<>()).build(); + assert input.getRecords().size() == 2; // invalid messages have been removed from the input + response.getBatchItemFailures().add(StreamsEventResponse.BatchItemFailure.builder().withItemIdentifier("1234").build()); + return response; + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSHandlerWithError.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSHandlerWithError.java new file mode 100644 index 000000000..23fceab5b --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/SQSHandlerWithError.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.validation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import java.util.ArrayList; +import software.amazon.lambda.powertools.validation.Validation; + +public class SQSHandlerWithError implements RequestHandler { + + @Override + @Validation(inboundSchema = "classpath:/schema_v7.json") + public SQSBatchResponse handleRequest(SQSEvent input, Context context) { + SQSBatchResponse response = SQSBatchResponse.builder().withBatchItemFailures(new ArrayList<>()).build(); + assert input.getRecords().size() == 2; // invalid messages have been removed from the input + response.getBatchItemFailures().add(SQSBatchResponse.BatchItemFailure.builder().withItemIdentifier("1234").build()); + return response; + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/StandardKinesisHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/StandardKinesisHandler.java new file mode 100644 index 000000000..1afc5c5ec --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/StandardKinesisHandler.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. + * 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 + * 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. + * + */ + +package software.amazon.lambda.powertools.validation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; +import software.amazon.lambda.powertools.validation.Validation; + +public class StandardKinesisHandler implements RequestHandler { + + @Override + @Validation(inboundSchema = "classpath:/schema_v7.json") + public StreamsEventResponse handleRequest(KinesisEvent input, Context context) { + StreamsEventResponse response = StreamsEventResponse.builder().build(); + assert input.getRecords().size() == 2; // invalid messages have been removed from the input + return response; + } +} diff --git a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/StandardSQSHandler.java similarity index 53% rename from powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandler.java rename to powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/StandardSQSHandler.java index de096679f..e0f0ece2d 100644 --- a/powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/SqsMessageHandler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/StandardSQSHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Copyright 2024 Amazon.com, Inc. or its affiliates. * 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 @@ -12,18 +12,21 @@ * */ -package software.amazon.lambda.powertools.sqs.handlers; +package software.amazon.lambda.powertools.validation.handlers; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import software.amazon.lambda.powertools.sqs.SqsLargeMessage; +import software.amazon.lambda.powertools.validation.Validation; -public class SqsMessageHandler implements RequestHandler { +public class StandardSQSHandler implements RequestHandler { @Override - @SqsLargeMessage - public String handleRequest(SQSEvent sqsEvent, Context context) { - return sqsEvent.getRecords().get(0).getBody(); + @Validation(inboundSchema = "classpath:/schema_v7.json") + public SQSBatchResponse handleRequest(SQSEvent input, Context context) { + SQSBatchResponse response = SQSBatchResponse.builder().build(); + assert input.getRecords().size() == 2; // invalid messages have been removed from the input + return response; } } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundAPIGatewayV2HTTPEventHandler.java similarity index 87% rename from powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java rename to powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundAPIGatewayV2HTTPEventHandler.java index fd5692884..b8c67b1eb 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundAPIGatewayV2HTTPEventHandler.java @@ -17,10 +17,12 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; + import software.amazon.lambda.powertools.validation.Validation; -public class ValidationInboundStringHandler implements RequestHandler { +public class ValidationInboundAPIGatewayV2HTTPEventHandler implements RequestHandler { private static final String schema = "{\n" + " \"$schema\": \"http://json-schema.org/draft-07/schema\",\n" + @@ -80,7 +82,10 @@ public class ValidationInboundStringHandler implements RequestHandler provideArguments(ExtensionContext context) { + + String body = "{id"; + + Map headers = new HashMap<>(); + headers.put("header1", "value1,value2,value3"); + Map> headersList = new HashMap<>(); + List headerValues = new ArrayList<>(); + headerValues.add("value1"); + headerValues.add("value2"); + headerValues.add("value3"); + headersList.put("header1", headerValues); + + final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent() + .withBody(body) + .withHeaders(headers) + .withMultiValueHeaders(headersList); + + APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); + apiGWV2HTTPResponse.setBody(body); + apiGWV2HTTPResponse.setHeaders(headers); + apiGWV2HTTPResponse.setMultiValueHeaders(headersList); + + return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse).map(Arguments::of); + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java index b634d6f8c..74803a05a 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java @@ -35,11 +35,6 @@ public Stream provideArguments(ExtensionContext context) { String body = "{id"; - final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent().withBody(body); - - APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); - apiGWV2HTTPResponse.setBody(body); - APIGatewayV2WebSocketResponse apiGWV2WebSocketResponse = new APIGatewayV2WebSocketResponse(); apiGWV2WebSocketResponse.setBody(body); @@ -53,7 +48,7 @@ public Stream provideArguments(ExtensionContext context) { KinesisAnalyticsInputPreprocessingResponse.Result.Ok, buffer)); kaipResponse.setRecords(records); - return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse, apiGWV2WebSocketResponse, albResponseEvent, + return Stream.of(apiGWV2WebSocketResponse, albResponseEvent, kaipResponse).map(Arguments::of); } } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index c8d5e7ade..42a18307e 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -17,11 +17,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.when; 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 com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent; @@ -35,12 +37,18 @@ import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent; import com.amazonaws.services.lambda.runtime.events.RabbitMQEvent; import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; import com.amazonaws.services.lambda.runtime.events.SQSEvent; import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; -import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; -import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; +import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; +import com.amazonaws.services.lambda.runtime.tests.annotations.Event; import com.networknt.schema.SpecVersion; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; @@ -55,14 +63,19 @@ import software.amazon.lambda.powertools.validation.Validation; import software.amazon.lambda.powertools.validation.ValidationConfig; import software.amazon.lambda.powertools.validation.ValidationException; -import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7Handler; +import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7APIGatewayProxyRequestEventHandler; +import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7StringHandler; +import software.amazon.lambda.powertools.validation.handlers.KinesisHandlerWithError; +import software.amazon.lambda.powertools.validation.handlers.SQSHandlerWithError; import software.amazon.lambda.powertools.validation.handlers.SQSWithCustomEnvelopeHandler; import software.amazon.lambda.powertools.validation.handlers.SQSWithWrongEnvelopeHandler; -import software.amazon.lambda.powertools.validation.handlers.ValidationInboundStringHandler; +import software.amazon.lambda.powertools.validation.handlers.StandardKinesisHandler; +import software.amazon.lambda.powertools.validation.handlers.StandardSQSHandler; +import software.amazon.lambda.powertools.validation.handlers.ValidationInboundAPIGatewayV2HTTPEventHandler; import software.amazon.lambda.powertools.validation.model.MyCustomEvent; -public class ValidationAspectTest { +class ValidationAspectTest { @Mock Validation validation; @@ -99,7 +112,7 @@ void setUp() { @ParameterizedTest @ArgumentsSource(ResponseEventsArgumentsProvider.class) - public void testValidateOutboundJsonSchema(Object object) throws Throwable { + void testValidateOutboundJsonSchemaWithExceptions(Object object) throws Throwable { when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); when(pjp.getSignature()).thenReturn(signature); when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); @@ -114,9 +127,50 @@ public void testValidateOutboundJsonSchema(Object object) throws Throwable { validationAspect.around(pjp, validation); }); } + + @ParameterizedTest + @ArgumentsSource(HandledResponseEventsArgumentsProvider.class) + void testValidateOutboundJsonSchemaWithHandledExceptions(Object object) throws Throwable { + when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); + when(pjp.getSignature()).thenReturn(signature); + when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); + Object[] args = {new Object(), context}; + when(pjp.getArgs()).thenReturn(args); + when(pjp.proceed(args)).thenReturn(object); + when(validation.inboundSchema()).thenReturn(""); + when(validation.outboundSchema()).thenReturn("classpath:/schema_v7.json"); + + Object response = validationAspect.around(pjp, validation); + assertThat(response).isInstanceOfAny(APIGatewayProxyResponseEvent.class, APIGatewayV2HTTPResponse.class); + + List headerValues = new ArrayList<>(); + headerValues.add("value1"); + headerValues.add("value2"); + headerValues.add("value3"); + + if (response instanceof APIGatewayProxyResponseEvent) { + assertThat(response).isInstanceOfSatisfying(APIGatewayProxyResponseEvent.class, t -> { + assertThat(t.getStatusCode()).isEqualTo(400); + assertThat(t.getBody()).isNotBlank(); + assertThat(t.getIsBase64Encoded()).isFalse(); + assertThat(t.getHeaders()).containsEntry("header1", "value1,value2,value3"); + assertThat(t.getMultiValueHeaders()).containsEntry("header1", headerValues); + }); + } else if (response instanceof APIGatewayV2HTTPResponse) { + assertThat(response).isInstanceOfSatisfying(APIGatewayV2HTTPResponse.class, t -> { + assertThat(t.getStatusCode()).isEqualTo(400); + assertThat(t.getBody()).isNotBlank(); + assertThat(t.getIsBase64Encoded()).isFalse(); + assertThat(t.getHeaders()).containsEntry("header1", "value1,value2,value3"); + assertThat(t.getMultiValueHeaders()).containsEntry("header1", headerValues); + }); + } else { + fail(); + } + } @Test - public void testValidateOutboundJsonSchema_APIGWV2() throws Throwable { + void testValidateOutboundJsonSchema_APIGWV2() throws Throwable { when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); when(pjp.getSignature()).thenReturn(signature); when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); @@ -136,100 +190,162 @@ public void testValidateOutboundJsonSchema_APIGWV2() throws Throwable { } @Test - public void validate_inputOK_schemaInClasspath_shouldValidate() { - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + void validate_inputOK_schemaInClasspath_shouldValidate() { + GenericSchemaV7APIGatewayProxyRequestEventHandler handler = new GenericSchemaV7APIGatewayProxyRequestEventHandler(); APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); event.setBody("{" + " \"id\": 1," + " \"name\": \"Lampshade\"," + " \"price\": 42" + "}"); - assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); + + + APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); + assertThat(response.getBody()).isEqualTo("valid-test"); + assertThat(response.getStatusCode()).isEqualTo(200); + } @Test - public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() { - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + void validate_inputKO_schemaInClasspath_shouldThrowValidationException() { + GenericSchemaV7APIGatewayProxyRequestEventHandler handler = new GenericSchemaV7APIGatewayProxyRequestEventHandler(); + + Map headers = new HashMap<>(); + headers.put("header1", "value1"); + Map> headersList = new HashMap<>(); + List headerValues = new ArrayList<>(); + headerValues.add("value1"); + headersList.put("header1", headerValues); + APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); event.setBody("{" + - " \"id\": 1," + - " \"name\": \"Lampshade\"," + - " \"price\": -2" + - "}"); + " \"id\": 1," + + " \"name\": \"Lampshade\"," + + " \"price\": -2" + + "}"); + event.setHeaders(headers); + event.setMultiValueHeaders(headersList); + // price is negative - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> handler.handleRequest(event, context)); + APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); + assertThat(response.getBody()).isNotBlank(); + assertThat(response.getStatusCode()).isEqualTo(400); + assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getMultiValueHeaders()).isEmpty(); } @Test - public void validate_inputOK_schemaInString_shouldValidate() { - ValidationInboundStringHandler handler = new ValidationInboundStringHandler(); + void validate_inputOK_schemaInString_shouldValidate() { + ValidationInboundAPIGatewayV2HTTPEventHandler handler = new ValidationInboundAPIGatewayV2HTTPEventHandler(); APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); event.setBody("{" + - " \"id\": 1," + - " \"name\": \"Lampshade\"," + - " \"price\": 42" + - "}"); - assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); + " \"id\": 1," + + " \"name\": \"Lampshade\"," + + " \"price\": 42" + + "}"); + + APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); + assertThat(response.getBody()).isEqualTo("valid-test"); + assertThat(response.getStatusCode()).isEqualTo(200); } + @Test - public void validate_inputKO_schemaInString_shouldThrowValidationException() { - ValidationInboundStringHandler handler = new ValidationInboundStringHandler(); + void validate_inputKO_schemaInString_shouldThrowValidationException() { + ValidationInboundAPIGatewayV2HTTPEventHandler handler = new ValidationInboundAPIGatewayV2HTTPEventHandler(); + + Map headers = new HashMap<>(); + headers.put("header1", "value1"); + APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); event.setBody("{" + - " \"id\": 1," + - " \"name\": \"Lampshade\"" + - "}"); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> handler.handleRequest(event, context)); - } + " \"id\": 1," + + " \"name\": \"Lampshade\"" + + "}"); + event.setHeaders(headers); - @Test - public void validate_SQS() { - PojoSerializer pojoSerializer = - LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); - SQSEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/sqs.json")); + APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); + assertThat(response.getBody()).isNotBlank(); + assertThat(response.getStatusCode()).isEqualTo(400); + assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getMultiValueHeaders()).isEmpty(); + } - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + @ParameterizedTest + @Event(value = "sqs.json", type = SQSEvent.class) + void validate_SQS(SQSEvent event) { + GenericSchemaV7StringHandler handler = new GenericSchemaV7StringHandler<>(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } - @Test - public void validate_SQS_CustomEnvelopeTakePrecedence() { - PojoSerializer pojoSerializer = - LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); - SQSEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/sqs_message.json")); + @ParameterizedTest + @Event(value = "sqs_invalid_messages.json", type = SQSEvent.class) + void validate_SQS_with_validation_partial_failure(SQSEvent event) { + StandardSQSHandler handler = new StandardSQSHandler(); + SQSBatchResponse response = handler.handleRequest(event, context); + assertThat(response.getBatchItemFailures()).hasSize(2); + assertThat(response.getBatchItemFailures().stream().map(SQSBatchResponse.BatchItemFailure::getItemIdentifier).collect( + Collectors.toList())).contains("d9144555-9a4f-4ec3-99a0-fc4e625a8db3", "d9144555-9a4f-4ec3-99a0-fc4e625a8db5"); + } + + @ParameterizedTest + @Event(value = "sqs_invalid_messages.json", type = SQSEvent.class) + void validate_SQS_with_partial_failure(SQSEvent event) { + SQSHandlerWithError handler = new SQSHandlerWithError(); + SQSBatchResponse response = handler.handleRequest(event, context); + assertThat(response.getBatchItemFailures()).hasSize(3); + assertThat(response.getBatchItemFailures().stream().map(SQSBatchResponse.BatchItemFailure::getItemIdentifier).collect( + Collectors.toList())).contains("d9144555-9a4f-4ec3-99a0-fc4e625a8db3", "d9144555-9a4f-4ec3-99a0-fc4e625a8db5", "1234"); + } + @ParameterizedTest + @Event(value = "sqs_message.json", type = SQSEvent.class) + void validate_SQS_CustomEnvelopeTakePrecedence(SQSEvent event) { SQSWithCustomEnvelopeHandler handler = new SQSWithCustomEnvelopeHandler(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } - @Test - public void validate_SQS_WrongEnvelope_shouldThrowValidationException() { - PojoSerializer pojoSerializer = - LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); - SQSEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/sqs_message.json")); - + @ParameterizedTest + @Event(value = "sqs_message.json", type = SQSEvent.class) + void validate_SQS_WrongEnvelope_shouldThrowValidationException(SQSEvent event) { SQSWithWrongEnvelopeHandler handler = new SQSWithWrongEnvelopeHandler(); assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> handler.handleRequest(event, context)); } - @Test - public void validate_Kinesis() { - PojoSerializer pojoSerializer = - LambdaEventSerializers.serializerFor(KinesisEvent.class, ClassLoader.getSystemClassLoader()); - KinesisEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/kinesis.json")); - - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + @ParameterizedTest + @Event(value = "kinesis.json", type = KinesisEvent.class) + void validate_Kinesis(KinesisEvent event) { + GenericSchemaV7StringHandler handler = new GenericSchemaV7StringHandler<>(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } + @ParameterizedTest + @Event(value = "kinesis_invalid_messages.json", type = KinesisEvent.class) + void validate_Kinesis_with_validation_partial_failure(KinesisEvent event) { + StandardKinesisHandler handler = new StandardKinesisHandler(); + StreamsEventResponse response = handler.handleRequest(event, context); + assertThat(response.getBatchItemFailures()).hasSize(2); + assertThat(response.getBatchItemFailures().stream().map(StreamsEventResponse.BatchItemFailure::getItemIdentifier).collect( + Collectors.toList())).contains("49545115243490985018280067714973144582180062593244200962", "49545115243490985018280067714973144582180062593244200964"); + } + + @ParameterizedTest + @Event(value = "kinesis_invalid_messages.json", type = KinesisEvent.class) + void validate_Kinesis_with_partial_failure(KinesisEvent event) { + KinesisHandlerWithError handler = new KinesisHandlerWithError(); + StreamsEventResponse response = handler.handleRequest(event, context); + assertThat(response.getBatchItemFailures()).hasSize(3); + assertThat(response.getBatchItemFailures().stream().map(StreamsEventResponse.BatchItemFailure::getItemIdentifier).collect( + Collectors.toList())).contains("49545115243490985018280067714973144582180062593244200962", "49545115243490985018280067714973144582180062593244200964", "1234"); + } + @ParameterizedTest @MethodSource("provideEventAndEventType") - public void validateEEvent(String jsonResource, Class eventClass) throws IOException { + void validateEEvent(String jsonResource, Class eventClass) throws IOException { Object event = ValidationConfig.get().getObjectMapper() .readValue(this.getClass().getResourceAsStream(jsonResource), eventClass); - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + GenericSchemaV7StringHandler handler = new GenericSchemaV7StringHandler<>(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } } diff --git a/powertools-validation/src/test/resources/kinesis_invalid_messages.json b/powertools-validation/src/test/resources/kinesis_invalid_messages.json new file mode 100644 index 000000000..3d805c4dd --- /dev/null +++ b/powertools-validation/src/test/resources/kinesis_invalid_messages.json @@ -0,0 +1,72 @@ +{ + "Records": [ + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "ewogICJpZCI6IDQzMjQyLAogICJuYW1lIjogIkZvb0JhciBYWSIsCiAgInByaWNlIjogMjU4Cn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-04", + "kinesisSchemaVersion": "1.0", + "data": "ewogICJpZCI6InN0cmluZ0lkIiwKICAibmFtZSI6ICJGb29CYXIgWFkiLAogICJwcmljZSI6IDI1OAp9", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200962", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-04", + "kinesisSchemaVersion": "1.0", + "data": "ewogICJpZCI6IDQyNSwKICAibmFtZSI6ICJCYXJGb28iLAogICJwcmljZSI6IDQzCn0=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200963", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + }, + { + "kinesis": { + "partitionKey": "partitionKey-04", + "kinesisSchemaVersion": "1.0", + "data": "ewogICJpZCI6MTIzNCwKICAibmFtZSI6ICJGb28iLAogICJwcmljZSI6IDI1OAp9", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200964", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + } + ] +} diff --git a/powertools-validation/src/test/resources/schema_v7.json b/powertools-validation/src/test/resources/schema_v7.json index f38272f2d..e382b8971 100644 --- a/powertools-validation/src/test/resources/schema_v7.json +++ b/powertools-validation/src/test/resources/schema_v7.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema", + "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://example.com/product.json", "type": "object", "title": "Product schema", diff --git a/powertools-validation/src/test/resources/schema_v7_ko.json b/powertools-validation/src/test/resources/schema_v7_ko.json index f54bcb3c7..aed187c34 100644 --- a/powertools-validation/src/test/resources/schema_v7_ko.json +++ b/powertools-validation/src/test/resources/schema_v7_ko.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema", + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "title": "Product schema", "description": "JSON schema to validate Products", diff --git a/powertools-validation/src/test/resources/sqs_invalid_messages.json b/powertools-validation/src/test/resources/sqs_invalid_messages.json new file mode 100644 index 000000000..aaec54bfd --- /dev/null +++ b/powertools-validation/src/test/resources/sqs_invalid_messages.json @@ -0,0 +1,72 @@ +{ + "Records": [ + { + "messageId": "d9144555-9a4f-4ec3-99a0-fc4e625a8db2", + "receiptHandle": "7kam5bfzbDsjtcjElvhSbxeLJbeey3A==", + "body": "{\n \"id\": 43242,\n \"name\": \"FooBar XY\",\n \"price\": 258\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975709495", + "SenderId": "AROAIFU457DVZ5L2J53F2", + "ApproximateFirstReceiveTimestamp": "1601975709499" + }, + "messageAttributes": { + }, + "md5OfBody": "0f96e88a291edb4429f2f7b9fdc3df96", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "d9144555-9a4f-4ec3-99a0-fc4e625a8db3", + "receiptHandle": "7kam5bfzbDsjtcjElvhSbxeLJbeey3A==", + "body": "{\n \"id\": 43245,\n \"name\": \"Foo\",\n \"price\": 258\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975709495", + "SenderId": "AROAIFU457DVZ5L2J53F2", + "ApproximateFirstReceiveTimestamp": "1601975709499" + }, + "messageAttributes": { + }, + "md5OfBody": "0f96e88a291edb4429f2f7b9fdc3df96", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "d9144555-9a4f-4ec3-99a0-fc4e625a8db4", + "receiptHandle": "7kam5bfzbDsjtcjElvhSbxeLJbeey3A==", + "body": "{\n \"id\": 43246,\n \"name\": \"FooBar XYZ\",\n \"price\": 258\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975709495", + "SenderId": "AROAIFU457DVZ5L2J53F2", + "ApproximateFirstReceiveTimestamp": "1601975709499" + }, + "messageAttributes": { + }, + "md5OfBody": "0f96e88a291edb4429f2f7b9fdc3df96", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + }, + { + "messageId": "d9144555-9a4f-4ec3-99a0-fc4e625a8db5", + "receiptHandle": "7kam5bfzbDsjtcjElvhSbxeLJbeey3A==", + "body": "{\n \"id\": \"stringId\",\n \"name\": \"FooBar XY\",\n \"price\": 258\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975709495", + "SenderId": "AROAIFU457DVZ5L2J53F2", + "ApproximateFirstReceiveTimestamp": "1601975709499" + }, + "messageAttributes": { + }, + "md5OfBody": "0f96e88a291edb4429f2f7b9fdc3df96", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index eca7e266f..9a42ebf16 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -8,26 +8,94 @@ https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html --> - + - + + + + + + + + + + + + + + + - - + + + + + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -40,39 +108,39 @@ + + + + + + + + + + + + - - - - - - + + - - + + - - + + - - - - - - - - @@ -86,7 +154,7 @@ - + @@ -101,16 +169,41 @@ + + + + + + + + + + + + + + + + + + + + - + - - + + + + + + + @@ -128,10 +221,6 @@ - - - - @@ -140,17 +229,13 @@ - - - - - - + + @@ -168,5 +253,5 @@ - - \ No newline at end of file + +