From 0c462964b7f27b3489f08cdd95a9bf0daec2bdbf Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Sep 2020 18:24:54 +0200
Subject: [PATCH 0001/1547] build(deps): bump junit-jupiter.version from 5.6.2
to 5.7.0 (#95)
Bumps `junit-jupiter.version` from 5.6.2 to 5.7.0.
Updates `junit-jupiter-api` from 5.6.2 to 5.7.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.6.2...r5.7.0)
Updates `junit-jupiter-engine` from 5.6.2 to 5.7.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.6.2...r5.7.0)
Updates `junit-jupiter-params` from 5.6.2 to 5.7.0
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.6.2...r5.7.0)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 6848ff7eb..4da459a35 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,7 +66,7 @@
3.2.03.2.11.6
- 5.6.2
+ 5.7.0
From 2117258982bb2cc296a436efb5c744cf21cbfa88 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Sep 2020 18:25:44 +0200
Subject: [PATCH 0002/1547] build(deps): bump aws-lambda-java-events from 3.2.0
to 3.3.0 (#96)
Bumps [aws-lambda-java-events](https://github.com/aws/aws-lambda-java-libs) from 3.2.0 to 3.3.0.
- [Release notes](https://github.com/aws/aws-lambda-java-libs/releases)
- [Commits](https://github.com/aws/aws-lambda-java-libs/commits)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 4da459a35..7df3f3fd3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,7 +56,7 @@
1.0.0UTF-81.2.1
- 3.2.0
+ 3.3.03.8.11.12.62.22.2
From 0c427a606e81ae0328f4fbf6ed7208c1e975a24b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2020 08:30:15 +0200
Subject: [PATCH 0003/1547] build(deps): bump jacoco-maven-plugin from 0.8.5 to
0.8.6 (#97)
Bumps [jacoco-maven-plugin](https://github.com/jacoco/jacoco) from 0.8.5 to 0.8.6.
- [Release notes](https://github.com/jacoco/jacoco/releases)
- [Commits](https://github.com/jacoco/jacoco/compare/v0.8.5...v0.8.6)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 7df3f3fd3..47a86b3da 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,7 +60,7 @@
3.8.11.12.62.22.2
- 0.8.5
+ 0.8.62.71.6.83.2.0
From 8ea74f85ee1d13327d0230abc59971ef8efeef12 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 19 Sep 2020 18:52:20 +0200
Subject: [PATCH 0004/1547] build(deps): bump mockito-core from 3.5.10 to
3.5.11 (#98)
Bumps [mockito-core](https://github.com/mockito/mockito) from 3.5.10 to 3.5.11.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.5.10...v3.5.11)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 47a86b3da..d8e567de2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -172,7 +172,7 @@
org.mockitomockito-core
- 3.5.10
+ 3.5.11test
From a22bf2d6b671251e99582b27243466dfe2389870 Mon Sep 17 00:00:00 2001
From: Pankaj Agrawal
Date: Tue, 22 Sep 2020 11:02:35 +0200
Subject: [PATCH 0005/1547] feat: Metrics utility (#91)
---
.github/workflows/build.yml | 2 +-
docs/content/core/metrics.mdx | 154 +++++++++++
docs/gatsby-config.js | 3 +-
example/HelloWorldFunction/pom.xml | 9 +
.../src/main/java/helloworld/App.java | 29 +-
.../src/main/java/helloworld/AppStream.java | 12 +-
pom.xml | 13 +
.../core/internal/LambdaHandlerProcessor.java | 23 +-
.../logging/internal/LambdaLoggingAspect.java | 22 +-
powertools-metrics/pom.xml | 101 +++++++
.../emf/model/MetricsLoggerHelper.java | 28 ++
.../powertools/metrics/PowertoolsMetrics.java | 50 ++++
.../metrics/PowertoolsMetricsLogger.java | 52 ++++
.../metrics/ValidationException.java | 8 +
.../metrics/internal/LambdaMetricsAspect.java | 118 ++++++++
.../metrics/PowertoolsMetricsLoggerTest.java | 74 ++++++
...ertoolsMetricsColdStartEnabledHandler.java | 21 ++
.../PowertoolsMetricsEnabledHandler.java | 21 ++
...PowertoolsMetricsEnabledStreamHandler.java | 23 ++
...sMetricsExceptionWhenNoMetricsHandler.java | 20 ++
.../PowertoolsMetricsNoDimensionsHandler.java | 25 ++
...etricsNoExceptionWhenNoMetricsHandler.java | 20 ++
...rtoolsMetricsTooManyDimensionsHandler.java | 28 ++
.../internal/LambdaMetricsAspectTest.java | 251 ++++++++++++++++++
.../tracing/internal/LambdaTracingAspect.java | 4 +-
25 files changed, 1072 insertions(+), 39 deletions(-)
create mode 100644 docs/content/core/metrics.mdx
create mode 100644 powertools-metrics/pom.xml
create mode 100644 powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java
create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/PowertoolsMetrics.java
create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/PowertoolsMetricsLogger.java
create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/ValidationException.java
create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java
create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/PowertoolsMetricsLoggerTest.java
create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java
create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java
create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java
create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java
create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java
create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java
create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java
create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bd1e6031d..3e8d0c321 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -27,7 +27,7 @@ jobs:
max-parallel: 4
matrix:
# test against latest update of each major Java version, as well as specific updates of LTS versions:
- java: [8, 8.0.192, 9.0.x, 10, 11.0.x, 11.0.3, 12, 13 ]
+ java: [8, 8.0.192, 11.0.x, 11.0.3, 12, 13 ]
name: Java ${{ matrix.java }}
env:
OS: ${{ matrix.os }}
diff --git a/docs/content/core/metrics.mdx b/docs/content/core/metrics.mdx
new file mode 100644
index 000000000..f5df760ee
--- /dev/null
+++ b/docs/content/core/metrics.mdx
@@ -0,0 +1,154 @@
+---
+title: Metrics
+description: Core utility
+---
+
+Metrics creates custom metrics asynchronously by logging metrics to standard output following Amazon CloudWatch Embedded Metric Format (EMF).
+
+These metrics can be visualized through [Amazon CloudWatch Console](https://console.aws.amazon.com/cloudwatch/).
+
+**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
+
+## Initialization
+
+Set `POWERTOOLS_SERVICE_NAME` and `POWERTOOLS_METRICS_NAMESPACE` env vars as a start - Here is an example using AWS Serverless Application Model (SAM)
+
+```yaml:title=template.yaml
+Resources:
+ HelloWorldFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ ...
+ Runtime: java8
+ Environment:
+ Variables:
+ POWERTOOLS_SERVICE_NAME: payment # highlight-line
+ POWERTOOLS_METRICS_NAMESPACE: ServerlessAirline # highlight-line
+```
+
+We recommend you use your application or main service as a metric namespace.
+You can explicitly set a namespace name an annotation variable `namespace` param or via `POWERTOOLS_METRICS_NAMESPACE` env var.
+
+This sets **namespace** key that will be used for all metrics.
+You can also pass a service name via `service` param or `POWERTOOLS_SERVICE_NAME` env var. This will create a dimension with the service name.
+
+```java:title=Handler.java
+package example;
+
+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.PowertoolsMetrics;
+import software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger;
+
+public class PowertoolsMetricsEnabledHandler implements RequestHandler
+
+ software.amazon.lambda
+ powertools-metrics
+ 0.2.0-beta
+ com.amazonawsaws-lambda-java-core
@@ -76,6 +81,10 @@
software.amazon.lambdapowertools-logging
+
+ software.amazon.lambda
+ powertools-metrics
+
diff --git a/example/HelloWorldFunction/src/main/java/helloworld/App.java b/example/HelloWorldFunction/src/main/java/helloworld/App.java
index 432a7aced..2444a0cb6 100644
--- a/example/HelloWorldFunction/src/main/java/helloworld/App.java
+++ b/example/HelloWorldFunction/src/main/java/helloworld/App.java
@@ -1,5 +1,13 @@
package helloworld;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
@@ -8,19 +16,16 @@
import com.amazonaws.xray.entities.Entity;
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.PowertoolsLogger;
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
+import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
import software.amazon.lambda.powertools.tracing.PowerTracer;
import software.amazon.lambda.powertools.tracing.PowertoolsTracing;
-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 static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
+import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.withSingleMetric;
import static software.amazon.lambda.powertools.tracing.PowerTracer.putMetadata;
import static software.amazon.lambda.powertools.tracing.PowerTracer.withEntitySubsegment;
@@ -33,12 +38,20 @@ public class App implements RequestHandler 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"));
+ });
+
PowertoolsLogger.appendKey("test", "willBeLogged");
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
diff --git a/example/HelloWorldFunction/src/main/java/helloworld/AppStream.java b/example/HelloWorldFunction/src/main/java/helloworld/AppStream.java
index 9405eee49..993da1e6d 100644
--- a/example/HelloWorldFunction/src/main/java/helloworld/AppStream.java
+++ b/example/HelloWorldFunction/src/main/java/helloworld/AppStream.java
@@ -1,20 +1,22 @@
package helloworld;
-import com.amazonaws.services.lambda.runtime.Context;
-import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import software.amazon.lambda.powertools.logging.PowertoolsLogging;
-
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import software.amazon.lambda.powertools.logging.PowertoolsLogging;
+import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
+
public class AppStream implements RequestStreamHandler {
private static final ObjectMapper mapper = new ObjectMapper();
@Override
@PowertoolsLogging(logEvent = true)
+ @PowertoolsMetrics(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/pom.xml b/pom.xml
index d8e567de2..2f04e62dc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,6 +31,7 @@
powertools-loggingpowertools-tracingpowertools-sqs
+ powertools-metrics
@@ -67,6 +68,7 @@
3.2.11.65.7.0
+ 1.0.0
@@ -143,6 +145,11 @@
aws-xray-recorder-sdk-aws-sdk-v2-instrumentor${aws.xray.recorder.version}
+
+ software.amazon.cloudwatchlogs
+ aws-embedded-metrics
+ ${aws-embedded-metrics.version}
+
@@ -175,6 +182,12 @@
3.5.11test
+
+ org.mockito
+ mockito-inline
+ 3.5.10
+ test
+ org.aspectjaspectjweaver
diff --git a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java
index e061e8edf..5475d89b6 100644
--- a/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java
+++ b/powertools-core/src/main/java/software/amazon/lambda/powertools/core/internal/LambdaHandlerProcessor.java
@@ -20,6 +20,10 @@
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.Optional;
+
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
public final class LambdaHandlerProcessor {
private static String SERVICE_NAME = null != System.getenv("POWERTOOLS_SERVICE_NAME")
@@ -48,12 +52,27 @@ public static boolean placedOnStreamHandler(final ProceedingJoinPoint pjp) {
&& pjp.getArgs()[2] instanceof Context;
}
+ public static Optional extractContext(final ProceedingJoinPoint pjp) {
+
+ if (isHandlerMethod(pjp)) {
+ if (placedOnRequestHandler(pjp)) {
+ return of((Context) pjp.getArgs()[1]);
+ }
+
+ if (placedOnStreamHandler(pjp)) {
+ return of((Context) pjp.getArgs()[2]);
+ }
+ }
+
+ return empty();
+ }
+
public static String serviceName() {
return SERVICE_NAME;
}
- public static Boolean isColdStart() {
- return IS_COLD_START;
+ public static boolean isColdStart() {
+ return IS_COLD_START == null;
}
public static void coldStartDone() {
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 3f56e9a2a..7c9b1a3e1 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
@@ -20,10 +20,8 @@
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Map;
-import java.util.Optional;
import java.util.Random;
-import com.amazonaws.services.lambda.runtime.Context;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
@@ -37,9 +35,8 @@
import org.aspectj.lang.annotation.Pointcut;
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
-import static java.util.Optional.empty;
-import static java.util.Optional.of;
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.placedOnRequestHandler;
@@ -82,7 +79,7 @@ public Object around(ProceedingJoinPoint pjp,
extractContext(pjp)
.ifPresent(context -> {
appendKeys(DefaultLambdaFields.values(context));
- appendKey("coldStart", null == isColdStart() ? "true" : "false");
+ appendKey("coldStart", isColdStart() ? "true" : "false");
appendKey("service", serviceName());
});
@@ -139,21 +136,6 @@ private double samplingRate(final PowertoolsLogging powertoolsLogging) {
return powertoolsLogging.samplingRate();
}
- private Optional extractContext(final ProceedingJoinPoint pjp) {
-
- if (isHandlerMethod(pjp)) {
- if (placedOnRequestHandler(pjp)) {
- return of((Context) pjp.getArgs()[1]);
- }
-
- if (placedOnStreamHandler(pjp)) {
- return of((Context) pjp.getArgs()[2]);
- }
- }
-
- return empty();
- }
-
private Object[] logEvent(final ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
diff --git a/powertools-metrics/pom.xml b/powertools-metrics/pom.xml
new file mode 100644
index 000000000..a753cc292
--- /dev/null
+++ b/powertools-metrics/pom.xml
@@ -0,0 +1,101 @@
+
+
+ 4.0.0
+
+ powertools-metrics
+ jar
+
+
+ powertools-parent
+ software.amazon.lambda
+ 0.2.0-beta
+
+
+ AWS Lambda Powertools Java library 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/awslabs/aws-lambda-powertools-java/issues
+
+
+ https://github.com/awslabs/aws-lambda-powertools-java.git
+
+
+
+ AWS Lambda Powertools 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-core
+
+
+ software.amazon.cloudwatchlogs
+ aws-embedded-metrics
+
+
+
+ org.aspectj
+ aspectjrt
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.apache.commons
+ commons-lang3
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-inline
+ test
+
+
+ org.aspectj
+ aspectjweaver
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+
\ 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
new file mode 100644
index 000000000..4c94b50f8
--- /dev/null
+++ b/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java
@@ -0,0 +1,28 @@
+package software.amazon.cloudwatchlogs.emf.model;
+
+import java.lang.reflect.Field;
+
+import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
+
+public final class MetricsLoggerHelper {
+ private MetricsLoggerHelper() {
+ }
+
+ public static boolean hasNoMetrics() {
+ return metricsContext().getRootNode().getAws().isEmpty();
+ }
+
+ public static long dimensionsCount() {
+ return metricsContext().getDimensions().size();
+ }
+
+ private 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/PowertoolsMetrics.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/PowertoolsMetrics.java
new file mode 100644
index 000000000..fa3cd9256
--- /dev/null
+++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/PowertoolsMetrics.java
@@ -0,0 +1,50 @@
+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 PowertoolsMetrics} is used to signal that the annotated method should be
+ * extended with PowertoolsMetrics functionality.
+ *
+ *
{@code PowertoolsMetrics} allows users to asynchronously create Amazon
+ * CloudWatch metrics by using the CloudWatch Embedded Metrics Format.
+ * {@code PowertoolsMetrics} manages the life-cycle of the MetricsLogger class,
+ * to simplify the user experience when used with AWS Lambda.
+ *
+ *
{@code PowertoolsMetrics} 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 PowertoolsMetrics} 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 @PowertoolsMetrics(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 @PowertoolsMetrics(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 @PowertoolsMetrics(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 @PowertoolsMetrics(namespace = "Namespace")}.
+ * If both are specified then the value of the annotation variable will be used.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface PowertoolsMetrics {
+ String namespace() default "";
+ String service() default "";
+ boolean captureColdStart() default false;
+ boolean raiseOnEmptyMetrics() default false;
+}
diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/PowertoolsMetricsLogger.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/PowertoolsMetricsLogger.java
new file mode 100644
index 000000000..8430e83f2
--- /dev/null
+++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/PowertoolsMetricsLogger.java
@@ -0,0 +1,52 @@
+package software.amazon.lambda.powertools.metrics;
+
+import java.util.function.Consumer;
+
+import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;
+import software.amazon.cloudwatchlogs.emf.model.Unit;
+
+/**
+ * A class used to retrieve the instance of the {@code MetricsLogger} used by
+ * {@code PowertoolsMetrics}.
+ *
+ * {@see PowertoolsMetrics}
+ */
+public final class PowertoolsMetricsLogger {
+ private static final MetricsLogger metricsLogger = new MetricsLogger();
+
+ private PowertoolsMetricsLogger() {
+ }
+
+ /**
+ * The instance of the {@code MetricsLogger} used by {@code PowertoolsMetrics}.
+ *
+ * @return The instance of the MetricsLogger used by PowertoolsMetrics.
+ */
+ public static MetricsLogger metricsLogger() {
+ return metricsLogger;
+ }
+
+ /**
+ * Add and immediately flush a single metric.
+ *
+ * @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) {
+ MetricsLogger metricsLogger = new MetricsLogger();
+ try {
+ metricsLogger.setNamespace(namespace);
+ metricsLogger.putMetric(name, value, unit);
+ logger.accept(metricsLogger);
+ } finally {
+ metricsLogger.flush();
+ }
+ }
+}
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
new file mode 100644
index 000000000..2da9a539c
--- /dev/null
+++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/ValidationException.java
@@ -0,0 +1,8 @@
+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/LambdaMetricsAspect.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java
new file mode 100644
index 000000000..df9af97c9
--- /dev/null
+++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java
@@ -0,0 +1,118 @@
+package software.amazon.lambda.powertools.metrics.internal;
+
+import java.lang.reflect.Field;
+import java.util.Optional;
+
+import com.amazonaws.services.lambda.runtime.Context;
+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.metrics.PowertoolsMetrics;
+import software.amazon.lambda.powertools.metrics.ValidationException;
+
+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.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.metrics.PowertoolsMetricsLogger.metricsLogger;
+import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.withSingleMetric;
+
+@Aspect
+public class LambdaMetricsAspect {
+ private static final String NAMESPACE = System.getenv("POWERTOOLS_METRICS_NAMESPACE");
+
+ @SuppressWarnings({"EmptyMethod"})
+ @Pointcut("@annotation(powertoolsMetrics)")
+ public void callAt(PowertoolsMetrics powertoolsMetrics) {
+ }
+
+ @Around(value = "callAt(powertoolsMetrics) && execution(@PowertoolsMetrics * *.*(..))", argNames = "pjp,powertoolsMetrics")
+ public Object around(ProceedingJoinPoint pjp,
+ PowertoolsMetrics powertoolsMetrics) throws Throwable {
+ Object[] proceedArgs = pjp.getArgs();
+
+ if (isHandlerMethod(pjp)
+ && (placedOnRequestHandler(pjp)
+ || placedOnStreamHandler(pjp))) {
+
+ MetricsLogger logger = metricsLogger();
+
+ logger.setNamespace(namespace(powertoolsMetrics))
+ .putDimensions(DimensionSet.of("Service", service(powertoolsMetrics)));
+
+ coldStartSingleMetricIfApplicable(pjp, powertoolsMetrics);
+
+ try {
+ Object proceed = pjp.proceed(proceedArgs);
+
+ coldStartDone();
+
+ validateBeforeFlushingMetrics(powertoolsMetrics);
+
+ logger.flush();
+ return proceed;
+
+ } finally {
+ refreshMetricsContext();
+ }
+ }
+
+ return pjp.proceed(proceedArgs);
+ }
+
+ private void coldStartSingleMetricIfApplicable(final ProceedingJoinPoint pjp,
+ final PowertoolsMetrics powertoolsMetrics) {
+ if (powertoolsMetrics.captureColdStart()
+ && isColdStart()) {
+
+ Optional contextOptional = extractContext(pjp);
+
+ if (contextOptional.isPresent()) {
+ Context context = contextOptional.orElseThrow(() -> new IllegalStateException("Context not found"));
+
+ withSingleMetric("ColdStart", 1, Unit.COUNT, namespace(powertoolsMetrics), (logger) ->
+ logger.setDimensions(DimensionSet.of("Service", service(powertoolsMetrics), "FunctionName", context.getFunctionName())));
+ }
+ }
+ }
+
+ private void validateBeforeFlushingMetrics(PowertoolsMetrics powertoolsMetrics) {
+ if (powertoolsMetrics.raiseOnEmptyMetrics() && hasNoMetrics()) {
+ throw new ValidationException("No metrics captured, at least one metrics must be emitted");
+ }
+
+ if (dimensionsCount() == 0 || dimensionsCount() > 9) {
+ throw new ValidationException(String.format("Number of Dimensions must be in range of 1-9." +
+ " Actual size: %d.", dimensionsCount()));
+ }
+ }
+
+ private String namespace(PowertoolsMetrics powertoolsMetrics) {
+ return !"".equals(powertoolsMetrics.namespace()) ? powertoolsMetrics.namespace() : NAMESPACE;
+ }
+
+ private String service(PowertoolsMetrics powertoolsMetrics) {
+ return !"".equals(powertoolsMetrics.service()) ? powertoolsMetrics.service() : serviceName();
+ }
+
+ // This can be simplified after this issues https://github.com/awslabs/aws-embedded-metrics-java/issues/35 is fixed
+ private static void refreshMetricsContext() {
+ try {
+ Field f = metricsLogger().getClass().getDeclaredField("context");
+ f.setAccessible(true);
+ f.set(metricsLogger(), new MetricsContext());
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/PowertoolsMetricsLoggerTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/PowertoolsMetricsLoggerTest.java
new file mode 100644
index 000000000..78ab99eca
--- /dev/null
+++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/PowertoolsMetricsLoggerTest.java
@@ -0,0 +1,74 @@
+package software.amazon.lambda.powertools.metrics;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Collections;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.model.DimensionSet;
+import software.amazon.cloudwatchlogs.emf.model.Unit;
+
+import static java.util.Collections.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mockStatic;
+
+class PowertoolsMetricsLoggerTest {
+
+ private final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ private final PrintStream originalOut = System.out;
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @BeforeEach
+ void setUp() {
+ System.setOut(new PrintStream(out));
+ }
+
+ @AfterEach
+ void tearDown() {
+ System.setOut(originalOut);
+ }
+
+ @BeforeAll
+ static void beforeAll() {
+ try (MockedStatic mocked = mockStatic(SystemWrapper.class)) {
+ mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
+ }
+ }
+
+ @Test
+ void singleMetricsCaptureUtility() {
+ try (MockedStatic mocked = mockStatic(SystemWrapper.class)) {
+ mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
+
+ PowertoolsMetricsLogger.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");
+ });
+ }
+ }
+
+ 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
new file mode 100644
index 000000000..793f0c68e
--- /dev/null
+++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java
@@ -0,0 +1,21 @@
+package software.amazon.lambda.powertools.metrics.handlers;
+
+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.PowertoolsMetrics;
+
+import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
+
+public class PowertoolsMetricsColdStartEnabledHandler implements RequestHandler {
+
+ @Override
+ @PowertoolsMetrics(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/PowertoolsMetricsEnabledHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java
new file mode 100644
index 000000000..adace6102
--- /dev/null
+++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java
@@ -0,0 +1,21 @@
+package software.amazon.lambda.powertools.metrics.handlers;
+
+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.PowertoolsMetrics;
+
+import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
+
+public class PowertoolsMetricsEnabledHandler implements RequestHandler {
+
+ @Override
+ @PowertoolsMetrics(namespace = "ExampleApplication", service = "booking")
+ 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/PowertoolsMetricsEnabledStreamHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java
new file mode 100644
index 000000000..942869c39
--- /dev/null
+++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java
@@ -0,0 +1,23 @@
+package software.amazon.lambda.powertools.metrics.handlers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
+import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;
+import software.amazon.cloudwatchlogs.emf.model.Unit;
+import software.amazon.lambda.powertools.metrics.PowertoolsMetrics;
+
+import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
+
+public class PowertoolsMetricsEnabledStreamHandler implements RequestStreamHandler {
+
+ @Override
+ @PowertoolsMetrics(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
new file mode 100644
index 000000000..47e882c0e
--- /dev/null
+++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java
@@ -0,0 +1,20 @@
+package software.amazon.lambda.powertools.metrics.handlers;
+
+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.PowertoolsMetrics;
+
+import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
+
+public class PowertoolsMetricsExceptionWhenNoMetricsHandler implements RequestHandler {
+
+ @Override
+ @PowertoolsMetrics(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
new file mode 100644
index 000000000..50a331708
--- /dev/null
+++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java
@@ -0,0 +1,25 @@
+package software.amazon.lambda.powertools.metrics.handlers;
+
+import java.util.function.IntConsumer;
+import java.util.stream.IntStream;
+
+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.PowertoolsMetrics;
+
+import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
+
+public class PowertoolsMetricsNoDimensionsHandler implements RequestHandler {
+
+ @Override
+ @PowertoolsMetrics(namespace = "ExampleApplication", service = "booking", captureColdStart = true)
+ public Object handleRequest(Object input, Context context) {
+ MetricsLogger metricsLogger = metricsLogger();
+ metricsLogger.setDimensions();
+
+ 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
new file mode 100644
index 000000000..0ca6c422e
--- /dev/null
+++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java
@@ -0,0 +1,20 @@
+package software.amazon.lambda.powertools.metrics.handlers;
+
+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.PowertoolsMetrics;
+
+import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
+
+public class PowertoolsMetricsNoExceptionWhenNoMetricsHandler implements RequestHandler {
+
+ @Override
+ @PowertoolsMetrics(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
new file mode 100644
index 000000000..40d94d3d7
--- /dev/null
+++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java
@@ -0,0 +1,28 @@
+package software.amazon.lambda.powertools.metrics.handlers;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+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.PowertoolsMetrics;
+
+import static software.amazon.lambda.powertools.metrics.PowertoolsMetricsLogger.metricsLogger;
+
+public class PowertoolsMetricsTooManyDimensionsHandler implements RequestHandler {
+
+ @Override
+ @PowertoolsMetrics
+ 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/internal/LambdaMetricsAspectTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java
new file mode 100644
index 000000000..c39f08eed
--- /dev/null
+++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java
@@ -0,0 +1,251 @@
+package software.amazon.lambda.powertools.metrics.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Map;
+
+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 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.ValidationException;
+import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsColdStartEnabledHandler;
+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 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;
+
+public class LambdaMetricsAspectTest {
+ @Mock
+ private Context context;
+
+ private final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ private final PrintStream originalOut = System.out;
+ private final ObjectMapper mapper = new ObjectMapper();
+ private RequestHandler requestHandler;
+ private RequestStreamHandler streamHandler;
+
+
+ @BeforeAll
+ static void beforeAll() {
+ try (MockedStatic mocked = mockStatic(SystemWrapper.class)) {
+ mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
+ }
+ }
+
+ @BeforeEach
+ void setUp() throws IllegalAccessException {
+ openMocks(this);
+ setupContext();
+ writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true);
+ System.setOut(new PrintStream(out));
+ requestHandler = new PowertoolsMetricsEnabledHandler();
+ }
+
+ @AfterEach
+ void tearDown() {
+ System.setOut(originalOut);
+ }
+
+ @Test
+ public void metricsWithoutColdStart() {
+ try (MockedStatic mocked = mockStatic(SystemWrapper.class)) {
+ mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
+ requestHandler.handleRequest("input", context);
+
+ assertThat(out.toString())
+ .satisfies(s -> {
+ Map logAsJson = readAsJson(s);
+
+ assertThat(logAsJson)
+ .containsEntry("Metric1", 1.0)
+ .containsEntry("Service", "booking")
+ .containsKey("_aws");
+ });
+ }
+ }
+
+ @Test
+ public void metricsWithColdStart() {
+ requestHandler = new PowertoolsMetricsColdStartEnabledHandler();
+
+ try (MockedStatic mocked = mockStatic(SystemWrapper.class)) {
+
+ mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
+ requestHandler.handleRequest("input", context);
+
+ assertThat(out.toString().split("\n"))
+ .hasSize(2)
+ .satisfies(s -> {
+ Map logAsJson = readAsJson(s[0]);
+
+ assertThat(logAsJson)
+ .doesNotContainKey("Metric1")
+ .containsEntry("ColdStart", 1.0)
+ .containsEntry("Service", "booking")
+ .containsKey("_aws");
+
+ logAsJson = readAsJson(s[1]);
+
+ assertThat(logAsJson)
+ .doesNotContainKey("ColdStart")
+ .containsEntry("Metric1", 1.0)
+ .containsEntry("Service", "booking")
+ .containsKey("_aws");
+ });
+ }
+ }
+
+ @Test
+ public void noColdStartMetricsWhenColdStartDone() {
+ requestHandler = new PowertoolsMetricsColdStartEnabledHandler();
+
+ try (MockedStatic mocked = mockStatic(SystemWrapper.class)) {
+ mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
+ 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")
+ .containsKey("_aws");
+
+ logAsJson = readAsJson(s[1]);
+
+ assertThat(logAsJson)
+ .doesNotContainKey("ColdStart")
+ .containsEntry("Metric1", 1.0)
+ .containsEntry("Service", "booking")
+ .containsKey("_aws");
+
+ logAsJson = readAsJson(s[2]);
+
+ assertThat(logAsJson)
+ .doesNotContainKey("ColdStart")
+ .containsEntry("Metric1", 1.0)
+ .containsEntry("Service", "booking")
+ .containsKey("_aws");
+ });
+ }
+ }
+
+ @Test
+ public void metricsWithStreamHandler() throws IOException {
+ streamHandler = new PowertoolsMetricsEnabledStreamHandler();
+
+ try (MockedStatic mocked = mockStatic(SystemWrapper.class)) {
+ mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
+
+ streamHandler.handleRequest(new ByteArrayInputStream(new byte[]{}), new ByteArrayOutputStream(), context);
+
+ assertThat(out.toString())
+ .satisfies(s -> {
+ Map logAsJson = readAsJson(s);
+
+ assertThat(logAsJson)
+ .containsEntry("Metric1", 1.0)
+ .containsEntry("Service", "booking")
+ .containsKey("_aws");
+ });
+ }
+ }
+
+ @Test
+ public void exceptionWhenNoMetricsEmitted() {
+ requestHandler = new PowertoolsMetricsExceptionWhenNoMetricsHandler();
+ try (MockedStatic mocked = mockStatic(SystemWrapper.class)) {
+ mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
+
+ assertThatExceptionOfType(ValidationException.class)
+ .isThrownBy(() -> requestHandler.handleRequest("input", context))
+ .withMessage("No metrics captured, at least one metrics must be emitted");
+ }
+ }
+
+ @Test
+ public void noExceptionWhenNoMetricsEmitted() {
+ requestHandler = new PowertoolsMetricsNoExceptionWhenNoMetricsHandler();
+
+ try (MockedStatic mocked = mockStatic(SystemWrapper.class)) {
+ mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
+ requestHandler.handleRequest("input", context);
+
+ assertThat(out.toString())
+ .satisfies(s -> {
+ Map logAsJson = readAsJson(s);
+
+ assertThat(logAsJson)
+ .containsEntry("Service", "booking")
+ .doesNotContainKey("_aws");
+ });
+ }
+ }
+
+ @Test
+ public void exceptionWhenNoDimensionsSet() {
+ requestHandler = new PowertoolsMetricsNoDimensionsHandler();
+ try (MockedStatic mocked = mockStatic(SystemWrapper.class)) {
+ mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
+
+ assertThatExceptionOfType(ValidationException.class)
+ .isThrownBy(() -> requestHandler.handleRequest("input", context))
+ .withMessage("Number of Dimensions must be in range of 1-9. Actual size: 0.");
+ }
+ }
+
+ @Test
+ public void exceptionWhenTooManyDimensionsSet() {
+ requestHandler = new PowertoolsMetricsTooManyDimensionsHandler();
+
+ try (MockedStatic mocked = mockStatic(SystemWrapper.class)) {
+ mocked.when(() -> SystemWrapper.getenv("AWS_EMF_ENVIRONMENT")).thenReturn("Lambda");
+
+ assertThatExceptionOfType(ValidationException.class)
+ .isThrownBy(() -> requestHandler.handleRequest("input", context))
+ .withMessage("Number of Dimensions must be in range of 1-9. Actual size: 14.");
+ }
+ }
+
+ private void setupContext() {
+ when(context.getFunctionName()).thenReturn("testFunction");
+ when(context.getInvokedFunctionArn()).thenReturn("testArn");
+ when(context.getFunctionVersion()).thenReturn("1");
+ when(context.getMemoryLimitInMB()).thenReturn(10);
+ }
+
+ private Map readAsJson(String s) {
+ try {
+ return mapper.readValue(s, Map.class);
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ }
+ return emptyMap();
+ }
+}
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 e24165824..54ef9a824 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
@@ -31,7 +31,7 @@
@Aspect
public final class LambdaTracingAspect {
- @SuppressWarnings({"EmptyMethod", "unused"})
+ @SuppressWarnings({"EmptyMethod"})
@Pointcut("@annotation(powerToolsTracing)")
public void callAt(PowertoolsTracing powerToolsTracing) {
}
@@ -45,7 +45,7 @@ public Object around(ProceedingJoinPoint pjp,
segment.setNamespace(namespace(powerToolsTracing));
if (placedOnHandlerMethod(pjp)) {
- segment.putAnnotation("ColdStart", isColdStart() == null);
+ segment.putAnnotation("ColdStart", isColdStart());
}
try {
From f99db35a14d402c6d7ad6aca34328985bbfdd7d9 Mon Sep 17 00:00:00 2001
From: Pankaj Agrawal
Date: Tue, 22 Sep 2020 11:15:06 +0200
Subject: [PATCH 0006/1547] ci: preparing release v0.3.0-beta (#100)
---
README.md | 4 ++--
docs/content/index.mdx | 4 ++--
docs/content/utilities/sqs_large_message_handling.mdx | 2 +-
example/HelloWorldFunction/pom.xml | 6 +++---
pom.xml | 2 +-
powertools-core/pom.xml | 2 +-
powertools-logging/pom.xml | 2 +-
powertools-metrics/pom.xml | 2 +-
powertools-sqs/pom.xml | 2 +-
powertools-tracing/pom.xml | 2 +-
10 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/README.md b/README.md
index b247f55b8..a82a03d10 100644
--- a/README.md
+++ b/README.md
@@ -17,12 +17,12 @@ Powertools is available in Maven Central. You can use your favourite dependency
software.amazon.lambdapowertools-tracing
- 0.2.0-beta
+ 0.3.0-betasoftware.amazon.lambdapowertools-logging
- 0.2.0-beta
+ 0.3.0-beta
...
diff --git a/docs/content/index.mdx b/docs/content/index.mdx
index 7ae3dee7b..49dfd4bef 100644
--- a/docs/content/index.mdx
+++ b/docs/content/index.mdx
@@ -16,12 +16,12 @@ Powertools dependencies are available in Maven Central. You can use your favouri
software.amazon.lambdapowertools-tracing
- 0.2.0-beta
+ 0.3.0-betasoftware.amazon.lambdapowertools-logging
- 0.2.0-beta
+ 0.3.0-beta
...
diff --git a/docs/content/utilities/sqs_large_message_handling.mdx b/docs/content/utilities/sqs_large_message_handling.mdx
index f2f75515d..dca2830f9 100644
--- a/docs/content/utilities/sqs_large_message_handling.mdx
+++ b/docs/content/utilities/sqs_large_message_handling.mdx
@@ -29,7 +29,7 @@ To install this utility, add the following dependency to your project.
software.amazon.lambdapowertools-sqs
- 0.2.0-beta
+ 0.3.0-beta
```
diff --git a/example/HelloWorldFunction/pom.xml b/example/HelloWorldFunction/pom.xml
index d739279d7..ab7e5035e 100644
--- a/example/HelloWorldFunction/pom.xml
+++ b/example/HelloWorldFunction/pom.xml
@@ -16,17 +16,17 @@
software.amazon.lambdapowertools-tracing
- 0.2.0-beta
+ 0.3.0-betasoftware.amazon.lambdapowertools-logging
- 0.2.0-beta
+ 0.3.0-betasoftware.amazon.lambdapowertools-metrics
- 0.2.0-beta
+ 0.3.0-betacom.amazonaws
diff --git a/pom.xml b/pom.xml
index 2f04e62dc..5df5397cd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
software.amazon.lambdapowertools-parent
- 0.2.0-beta
+ 0.3.0-betapomAWS Lambda Powertools Java library Parent
diff --git a/powertools-core/pom.xml b/powertools-core/pom.xml
index 97b566084..ff369004c 100644
--- a/powertools-core/pom.xml
+++ b/powertools-core/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.2.0-beta
+ 0.3.0-betaAWS Lambda Powertools Java library Core
diff --git a/powertools-logging/pom.xml b/powertools-logging/pom.xml
index f14217e79..ed5c098e6 100644
--- a/powertools-logging/pom.xml
+++ b/powertools-logging/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.2.0-beta
+ 0.3.0-betaAWS Lambda Powertools Java library Logging
diff --git a/powertools-metrics/pom.xml b/powertools-metrics/pom.xml
index a753cc292..5780e5ce6 100644
--- a/powertools-metrics/pom.xml
+++ b/powertools-metrics/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.2.0-beta
+ 0.3.0-betaAWS Lambda Powertools Java library Metrics
diff --git a/powertools-sqs/pom.xml b/powertools-sqs/pom.xml
index 2af9d2b40..cf3496867 100644
--- a/powertools-sqs/pom.xml
+++ b/powertools-sqs/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.2.0-beta
+ 0.3.0-betaAWS Lambda Powertools Java library SQS
diff --git a/powertools-tracing/pom.xml b/powertools-tracing/pom.xml
index 2d26417be..fdbb13854 100644
--- a/powertools-tracing/pom.xml
+++ b/powertools-tracing/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.2.0-beta
+ 0.3.0-betaAWS Lambda Powertools Java library Tracing
From 799aa3cefc447b0c0725e8fe1c2df98cfc0b50de Mon Sep 17 00:00:00 2001
From: Mark Sailes <45629314+msailes@users.noreply.github.com>
Date: Wed, 23 Sep 2020 11:37:24 +0100
Subject: [PATCH 0007/1547] docs: fixes to the documentation (#102)
---
README.md | 9 +++++++++
docs/content/core/metrics.mdx | 26 ++++++++++++++------------
2 files changed, 23 insertions(+), 12 deletions(-)
diff --git a/README.md b/README.md
index a82a03d10..71d5ca616 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,11 @@ Powertools is available in Maven Central. You can use your favourite dependency
powertools-logging0.3.0-beta
+
+ software.amazon.lambda
+ powertools-metrics
+ 0.3.0-beta
+
...
```
@@ -51,6 +56,10 @@ And configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lambd
software.amazon.lambdapowertools-tracing
+
+ software.amazon.lambda
+ powertools-metrics
+
diff --git a/docs/content/core/metrics.mdx b/docs/content/core/metrics.mdx
index f5df760ee..f9f33078a 100644
--- a/docs/content/core/metrics.mdx
+++ b/docs/content/core/metrics.mdx
@@ -65,7 +65,7 @@ You can initialize Metrics anywhere in your code as many times as you need - It'
You can create metrics using `putMetric`, and manually create dimensions for all your aggregate metrics using `add_dimension`.
-```java:title=app.py
+```java:title=Handler.java
public class PowertoolsMetricsEnabledHandler implements RequestHandler {
MetricsLogger metricsLogger = PowertoolsMetricsLogger.metricsLogger();
@@ -73,10 +73,10 @@ public class PowertoolsMetricsEnabledHandler implements RequestHandlerThis will not be available during metrics visualization - Use dimensions for this purpose
-```javv:title=Handler.java
+```java:title=Handler.java
@PowertoolsMetrics(namespace = "ServerlessAirline", service = "payment")
public APIGatewayProxyResponseEvent handleRequest(Object input, Context context) {
metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT);
- metricsLogger().putMetadata("booking_id", "1234567890"); # highlight-line
-
+ metricsLogger().putMetadata("booking_id", "1234567890"); // highlight-line
...
+}
```
This will be available in CloudWatch Logs to ease operations on high cardinal data.
@@ -131,9 +131,10 @@ If metrics are provided, and any of the following criteria are not met, `Validat
If you want to ensure that at least one metric is emitted, you can pass `raiseOnEmptyMetrics = true` to the **@PowertoolsMetrics** annotation:
```java:title=Handler.java
- @PowertoolsMetrics(raiseOnEmptyMetrics = true)
- public Object handleRequest(Object input, Context context) {
- ...
+@PowertoolsMetrics(raiseOnEmptyMetrics = true)
+public Object handleRequest(Object input, Context context) {
+...
+}
```
## Capturing cold start metric
@@ -141,9 +142,10 @@ If you want to ensure that at least one metric is emitted, you can pass `raiseOn
You can capture cold start metrics automatically with `@PowertoolsMetrics` via the `captureColdStart` variable.
```java:title=Handler.java
- @PowertoolsMetrics(captureColdStart = true)
- public Object handleRequest(Object input, Context context) {
- ...
+@PowertoolsMetrics(captureColdStart = true)
+public Object handleRequest(Object input, Context context) {
+...
+}
```
If it's a cold start invocation, this feature will:
From 9a9b09726bef1a9e06f6429f3d764abde255f740 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 24 Sep 2020 09:36:06 +0100
Subject: [PATCH 0008/1547] build(deps): bump aws-lambda-java-events from 3.3.0
to 3.3.1 (#104)
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 5df5397cd..4198f7039 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,7 +57,7 @@
1.0.0UTF-81.2.1
- 3.3.0
+ 3.3.13.8.11.12.62.22.2
From e7465c3be5bd5498397db6ce13697ae519d6ba33 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 24 Sep 2020 11:26:34 +0200
Subject: [PATCH 0009/1547] build(deps): bump mockito-inline from 3.5.10 to
3.5.11 (#103)
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 4198f7039..6a4fe8a8b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -185,7 +185,7 @@
org.mockitomockito-inline
- 3.5.10
+ 3.5.11test
From cc4e38c7bb906269e85e90ec601bc7840ffad36f Mon Sep 17 00:00:00 2001
From: Mark Sailes <45629314+msailes@users.noreply.github.com>
Date: Thu, 24 Sep 2020 10:33:30 +0100
Subject: [PATCH 0010/1547] fix: Removing v1 Java SDK dependencies for X-Ray
(#105)
---
pom.xml | 10 ----------
powertools-tracing/pom.xml | 8 --------
2 files changed, 18 deletions(-)
diff --git a/pom.xml b/pom.xml
index 6a4fe8a8b..c8d990fb9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -125,16 +125,6 @@
aws-xray-recorder-sdk-core${aws.xray.recorder.version}
-
- com.amazonaws
- aws-xray-recorder-sdk-aws-sdk
- ${aws.xray.recorder.version}
-
-
- com.amazonaws
- aws-xray-recorder-sdk-aws-sdk-instrumentor
- ${aws.xray.recorder.version}
- com.amazonawsaws-xray-recorder-sdk-aws-sdk-v2
diff --git a/powertools-tracing/pom.xml b/powertools-tracing/pom.xml
index fdbb13854..dac03f5fc 100644
--- a/powertools-tracing/pom.xml
+++ b/powertools-tracing/pom.xml
@@ -70,14 +70,6 @@
com.amazonawsaws-xray-recorder-sdk-core
-
- com.amazonaws
- aws-xray-recorder-sdk-aws-sdk
-
-
- com.amazonaws
- aws-xray-recorder-sdk-aws-sdk-instrumentor
- com.amazonawsaws-xray-recorder-sdk-aws-sdk-v2
From fb3195ac6e2a4e6aeadbb97daebd2ca39414293f Mon Sep 17 00:00:00 2001
From: Mark Sailes <45629314+msailes@users.noreply.github.com>
Date: Thu, 24 Sep 2020 12:34:34 +0100
Subject: [PATCH 0011/1547] fix: Removing Log4J dependencies from the tracing
module. (#106)
---
powertools-tracing/pom.xml | 8 --------
1 file changed, 8 deletions(-)
diff --git a/powertools-tracing/pom.xml b/powertools-tracing/pom.xml
index dac03f5fc..ffa553cd6 100644
--- a/powertools-tracing/pom.xml
+++ b/powertools-tracing/pom.xml
@@ -54,14 +54,6 @@
com.fasterxml.jackson.corejackson-databind
-
- org.apache.logging.log4j
- log4j-core
-
-
- org.apache.logging.log4j
- log4j-api
- org.aspectjaspectjrt
From 63b0461fd488e7166b2885db028436109b060d23 Mon Sep 17 00:00:00 2001
From: Mark Sailes <45629314+msailes@users.noreply.github.com>
Date: Thu, 24 Sep 2020 13:41:23 +0100
Subject: [PATCH 0012/1547] build(deps): bump payloadoffloading-common from
1.0.0 to 1.1.0 (#108)
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index c8d990fb9..f9b14dffb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,7 +54,7 @@
1.9.62.14.42.7.1
- 1.0.0
+ 1.1.0UTF-81.2.13.3.1
From fd6f6aa2db2d337fdcb49aabcc4a2e79abb58d5d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 25 Sep 2020 08:58:28 +0200
Subject: [PATCH 0013/1547] build(deps): bump mockito-inline from 3.5.11 to
3.5.13 (#110)
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index f9b14dffb..15201e076 100644
--- a/pom.xml
+++ b/pom.xml
@@ -175,7 +175,7 @@
org.mockitomockito-inline
- 3.5.11
+ 3.5.13test
From 166d29de165925e1b95069b294e4269459d74127 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 25 Sep 2020 08:58:49 +0200
Subject: [PATCH 0014/1547] build(deps): bump mockito-core from 3.5.11 to
3.5.13 (#109)
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 15201e076..e1c1ec282 100644
--- a/pom.xml
+++ b/pom.xml
@@ -169,7 +169,7 @@
org.mockitomockito-core
- 3.5.11
+ 3.5.13test
From e05c29102b98c6da9c315d1a455a7c6e5f21b557 Mon Sep 17 00:00:00 2001
From: Mark Sailes <45629314+msailes@users.noreply.github.com>
Date: Fri, 25 Sep 2020 12:55:22 +0100
Subject: [PATCH 0015/1547] ci: preparing release v0.3.1-beta (#107)
---
README.md | 6 +++---
docs/content/index.mdx | 4 ++--
docs/content/utilities/sqs_large_message_handling.mdx | 2 +-
example/HelloWorldFunction/pom.xml | 6 +++---
pom.xml | 2 +-
powertools-core/pom.xml | 2 +-
powertools-logging/pom.xml | 2 +-
powertools-metrics/pom.xml | 2 +-
powertools-sqs/pom.xml | 2 +-
powertools-tracing/pom.xml | 2 +-
10 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/README.md b/README.md
index 71d5ca616..f37e6ca7f 100644
--- a/README.md
+++ b/README.md
@@ -17,17 +17,17 @@ Powertools is available in Maven Central. You can use your favourite dependency
software.amazon.lambdapowertools-tracing
- 0.3.0-beta
+ 0.3.1-betasoftware.amazon.lambdapowertools-logging
- 0.3.0-beta
+ 0.3.1-betasoftware.amazon.lambdapowertools-metrics
- 0.3.0-beta
+ 0.3.1-beta
...
diff --git a/docs/content/index.mdx b/docs/content/index.mdx
index 49dfd4bef..6a9282683 100644
--- a/docs/content/index.mdx
+++ b/docs/content/index.mdx
@@ -16,12 +16,12 @@ Powertools dependencies are available in Maven Central. You can use your favouri
software.amazon.lambdapowertools-tracing
- 0.3.0-beta
+ 0.3.1-betasoftware.amazon.lambdapowertools-logging
- 0.3.0-beta
+ 0.3.1-beta
...
diff --git a/docs/content/utilities/sqs_large_message_handling.mdx b/docs/content/utilities/sqs_large_message_handling.mdx
index dca2830f9..eb41f1c39 100644
--- a/docs/content/utilities/sqs_large_message_handling.mdx
+++ b/docs/content/utilities/sqs_large_message_handling.mdx
@@ -29,7 +29,7 @@ To install this utility, add the following dependency to your project.
software.amazon.lambdapowertools-sqs
- 0.3.0-beta
+ 0.3.1-beta
```
diff --git a/example/HelloWorldFunction/pom.xml b/example/HelloWorldFunction/pom.xml
index ab7e5035e..a2ee6e448 100644
--- a/example/HelloWorldFunction/pom.xml
+++ b/example/HelloWorldFunction/pom.xml
@@ -16,17 +16,17 @@
software.amazon.lambdapowertools-tracing
- 0.3.0-beta
+ 0.3.1-betasoftware.amazon.lambdapowertools-logging
- 0.3.0-beta
+ 0.3.1-betasoftware.amazon.lambdapowertools-metrics
- 0.3.0-beta
+ 0.3.1-betacom.amazonaws
diff --git a/pom.xml b/pom.xml
index e1c1ec282..a7009e3e0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
software.amazon.lambdapowertools-parent
- 0.3.0-beta
+ 0.3.1-betapomAWS Lambda Powertools Java library Parent
diff --git a/powertools-core/pom.xml b/powertools-core/pom.xml
index ff369004c..b82b38ff8 100644
--- a/powertools-core/pom.xml
+++ b/powertools-core/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.3.0-beta
+ 0.3.1-betaAWS Lambda Powertools Java library Core
diff --git a/powertools-logging/pom.xml b/powertools-logging/pom.xml
index ed5c098e6..4e521cc9a 100644
--- a/powertools-logging/pom.xml
+++ b/powertools-logging/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.3.0-beta
+ 0.3.1-betaAWS Lambda Powertools Java library Logging
diff --git a/powertools-metrics/pom.xml b/powertools-metrics/pom.xml
index 5780e5ce6..b9b344887 100644
--- a/powertools-metrics/pom.xml
+++ b/powertools-metrics/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.3.0-beta
+ 0.3.1-betaAWS Lambda Powertools Java library Metrics
diff --git a/powertools-sqs/pom.xml b/powertools-sqs/pom.xml
index cf3496867..992b281af 100644
--- a/powertools-sqs/pom.xml
+++ b/powertools-sqs/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.3.0-beta
+ 0.3.1-betaAWS Lambda Powertools Java library SQS
diff --git a/powertools-tracing/pom.xml b/powertools-tracing/pom.xml
index ffa553cd6..f5a573487 100644
--- a/powertools-tracing/pom.xml
+++ b/powertools-tracing/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.3.0-beta
+ 0.3.1-betaAWS Lambda Powertools Java library Tracing
From 8001e72ebd95812a8a1bd76f03ca4ef4efdd7269 Mon Sep 17 00:00:00 2001
From: Ranjan Bhandari
Date: Fri, 25 Sep 2020 15:55:19 -0400
Subject: [PATCH 0016/1547] feat: integration with CloudWatch ServiceLens #88
(#111)
---
docs/content/core/logging.mdx | 2 +-
powertools-logging/pom.xml | 5 ++++
.../logging/internal/LambdaLoggingAspect.java | 13 +++++++++++
.../logging/internal/SystemWrapper.java | 10 ++++++++
.../internal/LambdaLoggingAspectTest.java | 23 +++++++++++++++++--
5 files changed, 50 insertions(+), 3 deletions(-)
create mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/SystemWrapper.java
diff --git a/docs/content/core/logging.mdx b/docs/content/core/logging.mdx
index 865b73432..a5e33c352 100644
--- a/docs/content/core/logging.mdx
+++ b/docs/content/core/logging.mdx
@@ -69,7 +69,7 @@ Key | Type | Example | Description
**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
## Capturing context Lambda info
diff --git a/powertools-logging/pom.xml b/powertools-logging/pom.xml
index 4e521cc9a..038f6a31d 100644
--- a/powertools-logging/pom.xml
+++ b/powertools-logging/pom.xml
@@ -88,6 +88,11 @@
mockito-coretest
+
+ org.mockito
+ mockito-inline
+ test
+ org.aspectjaspectjweaver
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 7c9b1a3e1..ec3f64165 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
@@ -20,6 +20,7 @@
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Map;
+import java.util.Optional;
import java.util.Random;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -35,6 +36,8 @@
import org.aspectj.lang.annotation.Pointcut;
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
+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.isColdStart;
@@ -44,6 +47,7 @@
import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.serviceName;
import static software.amazon.lambda.powertools.logging.PowertoolsLogger.appendKey;
import static software.amazon.lambda.powertools.logging.PowertoolsLogger.appendKeys;
+import static software.amazon.lambda.powertools.logging.internal.SystemWrapper.getenv;
@Aspect
public final class LambdaLoggingAspect {
@@ -83,6 +87,7 @@ public Object around(ProceedingJoinPoint pjp,
appendKey("service", serviceName());
});
+ getXrayTraceId().ifPresent(xRayTraceId -> appendKey("xray_trace_id", xRayTraceId));
if (powertoolsLogging.logEvent()) {
proceedArgs = logEvent(pjp);
@@ -179,4 +184,12 @@ private Object[] logFromInputStream(final ProceedingJoinPoint pjp) {
private Logger logger(final ProceedingJoinPoint pjp) {
return LogManager.getLogger(pjp.getSignature().getDeclaringType());
}
+
+ private static Optional getXrayTraceId() {
+ final String X_AMZN_TRACE_ID = getenv("_X_AMZN_TRACE_ID");
+ if(X_AMZN_TRACE_ID != null) {
+ return ofNullable(X_AMZN_TRACE_ID.split(";")[0].replace("Root=", ""));
+ }
+ return empty();
+ }
}
diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/SystemWrapper.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/SystemWrapper.java
new file mode 100644
index 000000000..c521fe77f
--- /dev/null
+++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/SystemWrapper.java
@@ -0,0 +1,10 @@
+package software.amazon.lambda.powertools.logging.internal;
+
+class SystemWrapper {
+ private SystemWrapper() {
+ }
+
+ public static String getenv(String name) {
+ return System.getenv(name);
+ }
+}
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 c99fdabe5..91e76a154 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
@@ -28,6 +28,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor;
import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabled;
import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabledForStream;
@@ -38,8 +40,10 @@
import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
-import static org.mockito.MockitoAnnotations.initMocks;
+import static org.mockito.MockitoAnnotations.openMocks;
+import static software.amazon.lambda.powertools.logging.internal.SystemWrapper.getenv;
class LambdaLoggingAspectTest {
@@ -52,7 +56,7 @@ class LambdaLoggingAspectTest {
@BeforeEach
void setUp() throws IllegalAccessException {
- initMocks(this);
+ openMocks(this);
ThreadContext.clearAll();
writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true);
setupContext();
@@ -172,6 +176,21 @@ void shouldLogServiceNameWhenEnvVarSet() throws IllegalAccessException {
.containsEntry("service", "testService");
}
+ @Test
+ void shouldLogxRayTraceIdEnvVarSet() {
+ String xRayTraceId = "1-5759e988-bd862e3fe1be46a994272793";
+
+ try (MockedStatic mocked = mockStatic(SystemWrapper.class)) {
+ mocked.when(() -> getenv("_X_AMZN_TRACE_ID")).thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1\"");
+
+ requestHandler.handleRequest(new Object(), context);
+
+ assertThat(ThreadContext.getImmutableContext())
+ .hasSize(EXPECTED_CONTEXT_SIZE + 1)
+ .containsEntry("xray_trace_id", xRayTraceId);
+ }
+ }
+
private void setupContext() {
when(context.getFunctionName()).thenReturn("testFunction");
when(context.getInvokedFunctionArn()).thenReturn("testArn");
From 4c5738989d3fd000607053b09cba3cc22be8ee4a Mon Sep 17 00:00:00 2001
From: Pankaj Agrawal
Date: Mon, 28 Sep 2020 14:29:17 +0200
Subject: [PATCH 0017/1547] fix: Log event via object mapper and not depend on
toString (#113)
Co-authored-by: Pankaj Agrawal
---
pom.xml | 6 +
powertools-logging/pom.xml | 10 ++
.../logging/internal/LambdaLoggingAspect.java | 21 +++-
.../core/layout/LambdaJsonLayoutTest.java | 4 +-
.../logging/handlers/PowerLogToolEnabled.java | 2 +-
.../internal/LambdaLoggingAspectTest.java | 106 +++++++++++++++---
.../test/resources/s3EventNotification.json | 38 +++++++
7 files changed, 166 insertions(+), 21 deletions(-)
create mode 100644 powertools-logging/src/test/resources/s3EventNotification.json
diff --git a/pom.xml b/pom.xml
index a7009e3e0..623a81e38 100644
--- a/pom.xml
+++ b/pom.xml
@@ -190,6 +190,12 @@
3.17.2test
+
+ org.skyscreamer
+ jsonassert
+ 1.5.0
+ test
+ org.aspectjaspectjtools
diff --git a/powertools-logging/pom.xml b/powertools-logging/pom.xml
index 038f6a31d..f2b210636 100644
--- a/powertools-logging/pom.xml
+++ b/powertools-logging/pom.xml
@@ -103,6 +103,16 @@
assertj-coretest
+
+ com.amazonaws
+ aws-lambda-java-events
+ test
+
+
+ org.skyscreamer
+ jsonassert
+ test
+
\ No newline at end of file
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 ec3f64165..8414d5307 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
@@ -23,6 +23,7 @@
import java.util.Optional;
import java.util.Random;
+import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
@@ -37,6 +38,7 @@
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
import static java.util.Optional.empty;
+import static java.util.Optional.of;
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;
@@ -147,7 +149,8 @@ private Object[] logEvent(final ProceedingJoinPoint pjp) {
if (isHandlerMethod(pjp)) {
if (placedOnRequestHandler(pjp)) {
Logger log = logger(pjp);
- log.info(pjp.getArgs()[0]);
+ asJson(pjp, pjp.getArgs()[0])
+ .ifPresent(log::info);
}
if (placedOnStreamHandler(pjp)) {
@@ -171,7 +174,9 @@ private Object[] logFromInputStream(final ProceedingJoinPoint pjp) {
args[0] = new ByteArrayInputStream(bytes);
Logger log = logger(pjp);
- log.info(MAPPER.readValue(bytes, Map.class));
+
+ asJson(pjp, MAPPER.readValue(bytes, Map.class))
+ .ifPresent(log::info);
} catch (IOException e) {
Logger log = logger(pjp);
@@ -181,6 +186,16 @@ private Object[] logFromInputStream(final ProceedingJoinPoint pjp) {
return args;
}
+ private Optional asJson(final ProceedingJoinPoint pjp,
+ final Object target) {
+ try {
+ return ofNullable(MAPPER.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());
}
@@ -188,7 +203,7 @@ private Logger logger(final ProceedingJoinPoint pjp) {
private static Optional getXrayTraceId() {
final String X_AMZN_TRACE_ID = getenv("_X_AMZN_TRACE_ID");
if(X_AMZN_TRACE_ID != null) {
- return ofNullable(X_AMZN_TRACE_ID.split(";")[0].replace("Root=", ""));
+ return of(X_AMZN_TRACE_ID.split(";")[0].replace("Root=", ""));
}
return empty();
}
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
index 4db93fd34..5fc0398d1 100644
--- 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
@@ -39,7 +39,7 @@
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.initMocks;
+import static org.mockito.MockitoAnnotations.openMocks;
class LambdaJsonLayoutTest {
@@ -50,7 +50,7 @@ class LambdaJsonLayoutTest {
@BeforeEach
void setUp() throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
- initMocks(this);
+ 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();
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/PowerLogToolEnabled.java
index 5678c0e95..097d26756 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/PowerLogToolEnabled.java
@@ -20,7 +20,7 @@
import software.amazon.lambda.powertools.logging.PowertoolsLogging;
public class PowerLogToolEnabled implements RequestHandler {
- private final Logger LOG = LogManager.getLogger(PowerToolLogEventEnabled.class);
+ private final Logger LOG = LogManager.getLogger(PowerLogToolEnabled.class);
@Override
@PowertoolsLogging
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 91e76a154..71f1dcec9 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
@@ -13,23 +13,33 @@
*/
package software.amazon.lambda.powertools.logging.internal;
+import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
import java.util.Map;
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.models.s3.S3EventNotification;
+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.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
-import org.mockito.Mockito;
import software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor;
import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabled;
import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabledForStream;
@@ -38,11 +48,23 @@
import software.amazon.lambda.powertools.logging.handlers.PowerToolLogEventEnabled;
import software.amazon.lambda.powertools.logging.handlers.PowerToolLogEventEnabledForStream;
+import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.RequestParametersEntity;
+import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.ResponseElementsEntity;
+import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.S3BucketEntity;
+import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.S3Entity;
+import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.S3EventNotificationRecord;
+import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.S3ObjectEntity;
+import static com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.UserIdentityEntity;
+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.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.logging.internal.SystemWrapper.getenv;
class LambdaLoggingAspectTest {
@@ -55,13 +77,16 @@ class LambdaLoggingAspectTest {
private Context context;
@BeforeEach
- void setUp() throws IllegalAccessException {
+ void setUp() throws IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException {
openMocks(this);
ThreadContext.clearAll();
writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true);
setupContext();
requestHandler = new PowerLogToolEnabled();
requestStreamHandler = new PowerLogToolEnabledForStream();
+ //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
@@ -140,30 +165,41 @@ void shouldHaveNoEffectIfNotUsedOnLambdaHandler() {
}
@Test
- void shouldLogEventForHandler() {
+ void shouldLogEventForHandler() throws IOException, JSONException {
requestHandler = new PowerToolLogEventEnabled();
+ S3EventNotification s3EventNotification = s3EventNotification();
- requestHandler.handleRequest(new Object(), context);
+ requestHandler.handleRequest(s3EventNotification, context);
- assertThat(ThreadContext.getImmutableContext())
- .hasSize(EXPECTED_CONTEXT_SIZE);
+ Map log = parseToMap(Files.lines(Paths.get("target/logfile.json")).collect(joining()));
+
+ String event = (String) log.get("message");
+
+ String expectEvent = new BufferedReader(new InputStreamReader(this.getClass().getResourceAsStream("/s3EventNotification.json")))
+ .lines().collect(joining("\n"));
+
+ assertEquals(expectEvent, event, false);
}
@Test
- void shouldLogEventForStreamAndLambdaStreamIsValid() throws IOException {
+ void shouldLogEventForStreamAndLambdaStreamIsValid() throws IOException, JSONException {
requestStreamHandler = new PowerToolLogEventEnabledForStream();
ByteArrayOutputStream output = new ByteArrayOutputStream();
+ S3EventNotification s3EventNotification = s3EventNotification();
- Map testPayload = new HashMap<>();
- testPayload.put("test", "payload");
-
- requestStreamHandler.handleRequest(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(testPayload)), output, context);
+ requestStreamHandler.handleRequest(new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(s3EventNotification)), output, context);
assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8))
- .isEqualTo("{\"test\":\"payload\"}");
+ .isNotEmpty();
- assertThat(ThreadContext.getImmutableContext())
- .hasSize(EXPECTED_CONTEXT_SIZE);
+ Map log = parseToMap(Files.lines(Paths.get("target/logfile.json")).collect(joining()));
+
+ String event = (String) log.get("message");
+
+ String expectEvent = new BufferedReader(new InputStreamReader(this.getClass().getResourceAsStream("/s3EventNotification.json")))
+ .lines().collect(joining("\n"));
+
+ assertEquals(expectEvent, event, false);
}
@Test
@@ -197,4 +233,44 @@ private void setupContext() {
when(context.getFunctionVersion()).thenReturn("1");
when(context.getMemoryLimitInMB()).thenReturn(10);
}
+
+ 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 S3EventNotification s3EventNotification() {
+ S3EventNotificationRecord record = new S3EventNotificationRecord("us-west-2",
+ "ObjectCreated:Put",
+ "aws:s3",
+ null,
+ "2.1",
+ new RequestParametersEntity("127.0.0.1"),
+ new ResponseElementsEntity("C3D13FE58DE4C810", "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"),
+ new S3Entity("testConfigRule",
+ new S3BucketEntity("mybucket",
+ new UserIdentityEntity("A3NL1KOZZKExample"),
+ "arn:aws:s3:::mybucket"),
+ new S3ObjectEntity("HappyFace.jpg",
+ 1024L,
+ "d41d8cd98f00b204e9800998ecf8427e",
+ "096fKKXTRTtl3on89fVO.nfljtsv6qko",
+ "0055AED6DCD90281E5"),
+ "1.0"),
+ new 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/resources/s3EventNotification.json b/powertools-logging/src/test/resources/s3EventNotification.json
new file mode 100644
index 000000000..feb88ec02
--- /dev/null
+++ b/powertools-logging/src/test/resources/s3EventNotification.json
@@ -0,0 +1,38 @@
+{
+ "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
From 8ea687738e84356f6fd79b80d3802f5f243e8200 Mon Sep 17 00:00:00 2001
From: Pankaj Agrawal
Date: Thu, 1 Oct 2020 12:39:47 +0200
Subject: [PATCH 0018/1547] ci: Automerge PR from dependabot (#115)
---
.github/workflows/build.yml | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3e8d0c321..5f76940f5 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -41,3 +41,25 @@ jobs:
java-version: ${{ matrix.java }}
- name: Build with Maven
run: mvn -B package --file pom.xml
+
+ auto-merge:
+ runs-on: ubuntu-latest
+ needs: [ build ]
+ if: github.base_ref == 'master' && github.actor == 'dependabot[bot]'
+ steps:
+ - uses: actions/github-script@0.2.0
+ with:
+ script: |
+ github.pullRequests.createReview({
+ owner: context.payload.repository.owner.login,
+ repo: context.payload.repository.name,
+ pull_number: context.payload.pull_request.number,
+ event: 'APPROVE'
+ })
+ github.pullRequests.merge({
+ owner: context.payload.repository.owner.login,
+ repo: context.payload.repository.name,
+ pull_number: context.payload.pull_request.number,
+ merge_method: 'squash'
+ })
+ github-token: ${{ secrets.AUTOMERGE }}
From b57786bad03dbb695cada2b858cee20a6e7e2279 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 1 Oct 2020 17:09:39 +0200
Subject: [PATCH 0019/1547] build(deps): bump aws-embedded-metrics from 1.0.0
to 1.0.1 (#114)
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 623a81e38..dc9086be8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,7 +68,7 @@
3.2.11.65.7.0
- 1.0.0
+ 1.0.1
From a93b984bc0569ae63ba9cc62012a1b79e4df6012 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?=
Date: Thu, 1 Oct 2020 17:11:01 +0200
Subject: [PATCH 0020/1547] powertools-parameters module (#90)
---
.gitignore | 1 +
docs/content/utilities/parameters.mdx | 255 +++++++++++
docs/gatsby-config.js | 3 +-
example/HelloWorldFunction/pom.xml | 7 +-
.../src/main/java/helloworld/AppParams.java | 82 ++++
.../src/main/java/helloworld/MyObject.java | 34 ++
example/template.yaml | 96 ++++-
pom.xml | 17 +-
powertools-parameters/pom.xml | 94 ++++
.../powertools/parameters/BaseProvider.java | 231 ++++++++++
.../powertools/parameters/ParamManager.java | 102 +++++
.../powertools/parameters/ParamProvider.java | 25 ++
.../powertools/parameters/SSMProvider.java | 293 +++++++++++++
.../parameters/SecretsProvider.java | 200 +++++++++
.../parameters/cache/CacheManager.java | 57 +++
.../parameters/cache/DataStore.java | 60 +++
.../exception/TransformationException.java | 24 ++
.../transform/Base64Transformer.java | 33 ++
.../transform/BasicTransformer.java | 29 ++
.../parameters/transform/JsonTransformer.java | 37 ++
.../transform/TransformationManager.java | 85 ++++
.../parameters/transform/Transformer.java | 43 ++
.../parameters/BaseProviderTest.java | 403 ++++++++++++++++++
.../ParamManagerIntegrationTest.java | 119 ++++++
.../parameters/SSMProviderTest.java | 167 ++++++++
.../parameters/SecretsProviderTest.java | 78 ++++
.../parameters/cache/CacheManagerTest.java | 104 +++++
.../parameters/cache/DataStoreTest.java | 72 ++++
.../transform/Base64TransformerTest.java | 42 ++
.../transform/JsonTransformerTest.java | 56 +++
.../transform/ObjectToDeserialize.java | 48 +++
.../transform/TransformationManagerTest.java | 85 ++++
powertools-tracing/pom.xml | 8 +
33 files changed, 2983 insertions(+), 7 deletions(-)
create mode 100644 docs/content/utilities/parameters.mdx
create mode 100644 example/HelloWorldFunction/src/main/java/helloworld/AppParams.java
create mode 100644 example/HelloWorldFunction/src/main/java/helloworld/MyObject.java
create mode 100644 powertools-parameters/pom.xml
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamProvider.java
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SSMProvider.java
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SecretsProvider.java
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/CacheManager.java
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/DataStore.java
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/TransformationException.java
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Base64Transformer.java
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/BasicTransformer.java
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformer.java
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/TransformationManager.java
create mode 100644 powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Transformer.java
create mode 100644 powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java
create mode 100644 powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java
create mode 100644 powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java
create mode 100644 powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SecretsProviderTest.java
create mode 100644 powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/CacheManagerTest.java
create mode 100644 powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/DataStoreTest.java
create mode 100644 powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/Base64TransformerTest.java
create mode 100644 powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformerTest.java
create mode 100644 powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/ObjectToDeserialize.java
create mode 100644 powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/TransformationManagerTest.java
diff --git a/.gitignore b/.gitignore
index 01a12d57f..0755a9b56 100644
--- a/.gitignore
+++ b/.gitignore
@@ -98,3 +98,4 @@ docs/.cache
docs/public
/example/.aws-sam/
/example/HelloWorldFunction/.aws-sam/
+samconfig.toml
diff --git a/docs/content/utilities/parameters.mdx b/docs/content/utilities/parameters.mdx
new file mode 100644
index 000000000..2a9d44d65
--- /dev/null
+++ b/docs/content/utilities/parameters.mdx
@@ -0,0 +1,255 @@
+---
+title: Parameters
+description: Utility
+---
+
+import Note from "../../src/components/Note"
+
+The parameters utility provides 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) or
+[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/). It also provides a base class to create your parameter provider implementation.
+
+**Key features**
+
+* Retrieve one or multiple parameters from the underlying provider
+* Cache parameter values for a given amount of time (defaults to 5 seconds)
+* Transform parameter values from JSON or base 64 encoded strings
+
+**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`
+
+## SSM Parameter Store
+
+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.
+
+```java:title=AppWithSSM.java
+
+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");
+
+}
+```
+
+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
+ SsmClient client = SsmClient.builder().region(Region.EU_CENTRAL_1).build();
+ SSMProvider ssmProvider = ParamManager.getSsmProvider(client);
+```
+### Additional arguments
+
+The AWS Systems Manager Parameter Store provider supports two additional arguments for the `get()` and `getMultiple()` methods:
+
+| Option | Default | Description |
+|---------------|---------|-------------|
+| **withDecryption()** | `False` | Will automatically decrypt the parameter. |
+| **recursive()** | `False` | For `getMultiple()` only, will fetch all parameter values recursively based on a path prefix. |
+
+**Example:**
+
+```java:title=AppWithSSM.java
+
+public class AppWithSSM implements RequestHandler {
+ // Get an instance of the SSM Provider
+ SSMProvider ssmProvider = ParamManager.getSsmProvider();
+
+ // Retrieve a single parameter and decrypt it
+ String value = ssmProvider.withDecryption().get("/my/parameter");
+
+ // Retrieve multiple parameters recursively from a path prefix
+ Map values = ssmProvider.recursive().getMultiple("/my/path/prefix");
+
+}
+```
+
+## Secrets Manager
+
+```java:title=AppWithSecrets.java
+
+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");
+
+}
+```
+
+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
+ SecretsManagerClient client = SecretsManagerClient.builder().region(Region.EU_CENTRAL_1).build();
+ SecretsProvider secretsProvider = ParamManager.getSecretsProvider(client);
+```
+
+## Advanced configuration
+
+### Caching
+
+By default, all parameters and their corresponding values are cached for 5 seconds.
+
+You can customize this default value using:
+```java
+ provider.defaultMaxAge(int, ChronoUnit)
+```
+
+You can also customize this value for each parameter with:
+```java
+ provider.withMaxAge(int, ChronoUnit).get()
+```
+
+### Transform values
+
+Parameter values can be transformed using ```withTransformation(transformerClass)```.
+Base64 and JSON transformations are provided:
+
+```java
+ String value = provider
+ .withTransformation(Transformer.base64)
+ .get("/my/parameter/b64");
+```
+
+For more complex transformation, you need to specify how to deserialize:
+
+```java
+ MyObj object = provider
+ .withTransformation(Transformer.json)
+ .get("/my/parameter/json", MyObj.class);
+```
+
+**Note**: ```SSMProvider.getMultiple()``` does not support transformation and will return simple Strings.
+
+**Write 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:
+
+```java:title=XmlTransformer.java
+public class XmlTransformer implements Transformer {
+
+ private final XmlMapper mapper = new XmlMapper();
+
+ @Override
+ public T applyTransformation(String value, Class targetClass) throws TransformationException {
+ try {
+ return mapper.readValue(value, targetClass);
+ } catch (IOException e) {
+ throw new TransformationException(e);
+ }
+ }
+}
+```
+
+Then use it like this:
+
+```java
+MyObj object = provider
+ .withTransformation(XmlTransformer.class)
+ .get("/my/parameter/xml", MyObj.class);
+```
+
+### Fluent API
+
+To simplify the use of the library, you can chain all method calls before a get.
+
+**Example:**
+
+```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
+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.
+
+Here is an example implementation using S3 as a custom parameter store:
+
+```java:title=S3Provider.java
+public class S3Provider extends BaseProvider {
+
+ private final S3Client client;
+ private String bucket;
+
+ S3Provider(CacheManager cacheManager) {
+ this(cacheManager, S3Client.create());
+ }
+
+ S3Provider(CacheManager cacheManager, S3Client client) {
+ super(cacheManager);
+ this.client = client;
+ }
+
+ public S3Provider withBucket(String bucket) {
+ this.bucket = bucket;
+ return this;
+ }
+
+ @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();
+ }
+
+ @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;
+ }
+
+ @Override
+ protected void resetToDefaults() {
+ super.resetToDefaults();
+ bucket = null;
+ }
+
+}
+```
+And then use it like this :
+
+```java
+S3Provider provider = new S3Provider(ParamManager.getCacheManager());
+provider.setTransformationManager(ParamManager.getTransformationManager()); // optional, needed for transformations
+String value = provider.withBucket("myBucket").get("myKey");
+```
\ No newline at end of file
diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js
index 503e8327a..d3cd330a1 100644
--- a/docs/gatsby-config.js
+++ b/docs/gatsby-config.js
@@ -28,7 +28,8 @@ module.exports = {
'core/metrics'
],
'Utilities': [
- 'utilities/sqs_large_message_handling'
+ 'utilities/sqs_large_message_handling',
+ 'utilities/parameters'
],
},
navConfig: {
diff --git a/example/HelloWorldFunction/pom.xml b/example/HelloWorldFunction/pom.xml
index a2ee6e448..778254672 100644
--- a/example/HelloWorldFunction/pom.xml
+++ b/example/HelloWorldFunction/pom.xml
@@ -28,6 +28,11 @@
powertools-metrics0.3.1-beta
+
+ software.amazon.lambda
+ powertools-parameters
+ 0.3.1-beta
+ com.amazonawsaws-lambda-java-core
@@ -51,7 +56,7 @@
com.fasterxml.jackson.corejackson-databind
- 2.9.10.5
+ 2.11.2
diff --git a/example/HelloWorldFunction/src/main/java/helloworld/AppParams.java b/example/HelloWorldFunction/src/main/java/helloworld/AppParams.java
new file mode 100644
index 000000000..7633263f3
--- /dev/null
+++ b/example/HelloWorldFunction/src/main/java/helloworld/AppParams.java
@@ -0,0 +1,82 @@
+package helloworld;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import org.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 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 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;
+
+public class AppParams implements RequestHandler {
+
+ Logger log = LogManager.getLogger();
+
+ SSMProvider ssmProvider = ParamManager.getSsmProvider();
+ SecretsProvider secretsProvider = ParamManager.getSecretsProvider();
+
+ String simplevalue = ssmProvider.defaultMaxAge(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()))) {
+ return br.lines().collect(Collectors.joining(System.lineSeparator()));
+ }
+ }
+}
diff --git a/example/HelloWorldFunction/src/main/java/helloworld/MyObject.java b/example/HelloWorldFunction/src/main/java/helloworld/MyObject.java
new file mode 100644
index 000000000..3c416971e
--- /dev/null
+++ b/example/HelloWorldFunction/src/main/java/helloworld/MyObject.java
@@ -0,0 +1,34 @@
+package helloworld;
+
+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/example/template.yaml b/example/template.yaml
index 32f6461e9..a7dd1b8be 100644
--- a/example/template.yaml
+++ b/example/template.yaml
@@ -48,6 +48,84 @@ Resources:
Path: /hellostream
Method: get
+ HelloWorldParamsFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: HelloWorldFunction
+ Handler: helloworld.AppParams::handleRequest
+ Runtime: java8
+ MemorySize: 512
+ Tracing: Active
+ 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: /helloparams
+ 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:
# 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
@@ -58,6 +136,18 @@ Outputs:
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
+
+ HelloWorldStreamApi:
+ Description: "API Gateway endpoint URL for Prod stage for Hello World stream function"
+ Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hellostream/"
+ HelloWorldStreamFunction:
+ Description: "Hello World Stream Lambda Function ARN"
+ Value: !GetAtt HelloWorldStreamFunction.Arn
+
+ HelloWorldParamsApi:
+ Description: "API Gateway endpoint URL for Prod stage for Hello World params function"
+ Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/helloparams/"
+ HelloWorldParamsFunction:
+ Description: "Hello World Params Lambda Function ARN"
+ Value: !GetAtt HelloWorldParamsFunction.Arn
+
diff --git a/pom.xml b/pom.xml
index dc9086be8..9dd4347df 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,6 +32,7 @@
powertools-tracingpowertools-sqspowertools-metrics
+ powertools-parameters
@@ -52,7 +53,7 @@
2.13.32.11.21.9.6
- 2.14.4
+ 2.14.102.7.11.1.0UTF-8
@@ -95,6 +96,13 @@
aws-lambda-java-events${lambda.events.version}
+
+ software.amazon.awssdk
+ bom
+ ${aws.sdk.version}
+ pom
+ import
+ software.amazon.payloadoffloadingpayloadoffloading-common
@@ -125,6 +133,11 @@
aws-xray-recorder-sdk-core${aws.xray.recorder.version}
+
+ com.amazonaws
+ aws-xray-recorder-sdk-aws-sdk-core
+ ${aws.xray.recorder.version}
+ com.amazonawsaws-xray-recorder-sdk-aws-sdk-v2
@@ -407,4 +420,4 @@
-
\ No newline at end of file
+
diff --git a/powertools-parameters/pom.xml b/powertools-parameters/pom.xml
new file mode 100644
index 000000000..ec53cb412
--- /dev/null
+++ b/powertools-parameters/pom.xml
@@ -0,0 +1,94 @@
+
+
+ 4.0.0
+
+
+ powertools-parent
+ software.amazon.lambda
+ 0.3.1-beta
+
+
+ powertools-parameters
+
+ AWS Lambda Powertools 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/awslabs/aws-lambda-powertools-java/issues
+
+
+ https://github.com/awslabs/aws-lambda-powertools-java.git
+
+
+
+ AWS Lambda Powertools team
+ Amazon Web Services
+ https://aws.amazon.com/
+
+
+
+
+
+ ossrh
+ https://aws.oss.sonatype.org/content/repositories/snapshots
+
+
+
+
+
+ software.amazon.awssdk
+ ssm
+
+
+ software.amazon.awssdk
+ secretsmanager
+
+
+ software.amazon.awssdk
+ apache-client
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ org.aspectj
+ aspectjrt
+ compile
+
+
+
+
+ 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.assertj
+ assertj-core
+ test
+
+
+
+
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
new file mode 100644
index 000000000..0ff2b4b3d
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/BaseProvider.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2020 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 software.amazon.awssdk.annotations.NotThreadSafe;
+import software.amazon.lambda.powertools.parameters.cache.CacheManager;
+import software.amazon.lambda.powertools.parameters.exception.TransformationException;
+import software.amazon.lambda.powertools.parameters.transform.BasicTransformer;
+import software.amazon.lambda.powertools.parameters.transform.TransformationManager;
+import software.amazon.lambda.powertools.parameters.transform.Transformer;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Map;
+
+/**
+ * Base class for all parameter providers.
+ */
+@NotThreadSafe
+public abstract class BaseProvider implements ParamProvider {
+
+ protected final CacheManager cacheManager;
+ private TransformationManager transformationManager;
+ private Clock clock;
+
+ public BaseProvider(CacheManager cacheManager) {
+ this.cacheManager = cacheManager;
+ }
+
+ /**
+ * Retrieve the parameter value from the underlying parameter store.
+ * Abstract: Implement this method in a child class of {@link BaseProvider}
+ *
+ * @param key key of the parameter
+ * @return the value of the parameter identified by the key
+ */
+ protected abstract String getValue(String key);
+
+ /**
+ * Retrieve multiple parameter values from the underlying parameter store.
+ * Abstract: Implement this method in a child class of {@link BaseProvider}
+ *
+ * @param path
+ * @return
+ */
+ 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,
+ * 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
+ * can lead to unwanted cache time for some parameters.
+ *
+ * @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.withMaxAge(10, SECONDS).get("key")
).
+ */
+ public BaseProvider withMaxAge(int maxAge, ChronoUnit unit) {
+ cacheManager.setExpirationTime(Duration.of(maxAge, unit));
+ return this;
+ }
+
+ /**
+ * Builder method to call before {@link #get(String)} (Optional) or {@link #get(String, Class)} (Mandatory).
+ * to provide a {@link Transformer} that will transform the String parameter into something else (String, Object, ...)
+ *
+ * {@link software.amazon.lambda.powertools.parameters.transform.Base64Transformer} and {@link software.amazon.lambda.powertools.parameters.transform.JsonTransformer}
+ * are provided for respectively base64 and json content. You can also write your own (see {@link Transformer}).
+ *
+ * Not Thread Safe: calling this method simultaneously by several threads
+ * can lead to errors (one Transformer for the wrong target type)
+ *
+ * @param transformerClass Class of the transformer to apply. For convenience, you can use {@link Transformer#json} or {@link Transformer#base64} shortcuts.
+ * @return the provider itself in order to chain calls (eg.
).
+ */
+ protected BaseProvider withTransformation(Class extends Transformer> transformerClass) {
+ if (transformationManager == null) {
+ throw new IllegalStateException("Trying to add transformation while no TransformationManager has been provided.");
+ }
+ transformationManager.setTransformer(transformerClass);
+ return this;
+ }
+
+ /**
+ * Retrieve multiple parameter values either from the underlying store or a cached value (if not expired).
+ * Cache all values with the 'path' as the key and also individually to be able to {@link #get(String)} a single value later
+ * Does not support transformation.
+ *
+ * @param path path of the parameter
+ * @return a map containing parameters keys and values. The key is a subpart of the path
+ * eg. getMultiple("/foo/bar") will retrieve [key="baz", value="valuebaz"] for parameter "/foo/bar/baz"
+ */
+ @Override
+ public Map getMultiple(String path) {
+ try {
+ return (Map) cacheManager.getIfNotExpired(path, now()).orElseGet(() -> {
+ Map params = getMultipleValues(path);
+
+ cacheManager.putInCache(path, params);
+
+ params.forEach((k, v) -> cacheManager.putInCache(path + "/" + k, v));
+
+ return params;
+ });
+ } finally {
+ resetToDefaults();
+ }
+ }
+
+ /**
+ * Get the value of a parameter, either from the underlying store or a cached value (if not expired).
+ * Using this method, you can apply a basic transformation (to String).
+ * Set a {@link BasicTransformer} with {@link #withTransformation(Class)}.
+ * If you need a more complex transformation (to Object), use {@link #get(String, Class)} method instead of this one.
+ *
+ * @param key key of the parameter
+ * @return the String value of the parameter
+ * @throws IllegalStateException if a wrong transformer class is provided through {@link #withTransformation(Class)}. Needs to be a {@link BasicTransformer}.
+ * @throws TransformationException if the transformation could not be done, because of a wrong format or an error during transformation.
+ */
+ @Override
+ public String get(final String key) {
+ try {
+ return (String) cacheManager.getIfNotExpired(key, now()).orElseGet(() -> {
+ String value = getValue(key);
+
+ String transformedValue = value;
+ if (transformationManager != null && transformationManager.shouldTransform()) {
+ transformedValue = transformationManager.performBasicTransformation(value);
+ }
+
+ cacheManager.putInCache(key, transformedValue);
+
+ return transformedValue;
+ });
+ } finally {
+ // in all case, we reset options to default, for next call
+ resetToDefaults();
+ }
+ }
+
+ /**
+ * Get the value of a parameter, either from the underlying store or a cached value (if not expired).
+ * Using this method, you must apply a transformation (eg. json/xml to Object).
+ * Set a {@link Transformer} with {@link #withTransformation(Class)}.
+ * If you need a simpler transformation (to String), use {@link #get(String)} method instead of this one.
+ *
+ * @param key key of the parameter
+ * @param targetClass class of the target Object (after transformation)
+ * @return the Object (T) value of the parameter
+ * @throws IllegalStateException if no transformation class was provided through {@link #withTransformation(Class)}
+ * @throws TransformationException if the transformation could not be done, because of a wrong format or an error during transformation.
+ */
+ @Override
+ public T get(final String key, final Class targetClass) {
+ try {
+ return (T) cacheManager.getIfNotExpired(key, now()).orElseGet(() -> {
+ String value = getValue(key);
+
+ if (transformationManager == null) {
+ throw new IllegalStateException("Trying to transform value while no TransformationManager has been provided.");
+ }
+ T transformedValue = transformationManager.performComplexTransformation(value, targetClass);
+
+ cacheManager.putInCache(key, transformedValue);
+
+ return transformedValue;
+ });
+ } finally {
+ // in all case, we reset options to default, for next call
+ resetToDefaults();
+ }
+ }
+
+ protected Instant now() {
+ if (clock == null) {
+ clock = Clock.systemDefaultZone();
+ }
+ return clock.instant();
+ }
+
+ protected void resetToDefaults() {
+ cacheManager.resetExpirationTime();
+ if (transformationManager != null) {
+ transformationManager.setTransformer(null);
+ }
+ }
+
+ protected void setTransformationManager(TransformationManager transformationManager) {
+ this.transformationManager = transformationManager;
+ }
+
+ /**
+ * For test purpose
+ * @param clock
+ */
+ void setClock(Clock clock) {
+ this.clock = clock;
+ }
+}
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
new file mode 100644
index 000000000..f2b50425b
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamManager.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2020 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 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();
+
+ private static SecretsProvider secretsProvider;
+ private static SSMProvider ssmProvider;
+
+ /**
+ * 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() {
+ if (secretsProvider == null) {
+ secretsProvider = SecretsProvider.builder()
+ .withCacheManager(cacheManager)
+ .withTransformationManager(transformationManager)
+ .build();
+ }
+ return secretsProvider;
+ }
+
+ /**
+ * 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() {
+ if (ssmProvider == null) {
+ ssmProvider = SSMProvider.builder()
+ .withCacheManager(cacheManager)
+ .withTransformationManager(transformationManager)
+ .build();
+ }
+ return ssmProvider;
+ }
+
+ /**
+ * 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) {
+ if (secretsProvider == null) {
+ secretsProvider = SecretsProvider.builder()
+ .withClient(client)
+ .withCacheManager(cacheManager)
+ .withTransformationManager(transformationManager)
+ .build();
+ }
+ return secretsProvider;
+ }
+
+ /**
+ * 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) {
+ if (ssmProvider == null) {
+ ssmProvider = SSMProvider.builder()
+ .withClient(client)
+ .withCacheManager(cacheManager)
+ .withTransformationManager(transformationManager)
+ .build();
+ }
+ return ssmProvider;
+ }
+
+ public static CacheManager getCacheManager() {
+ return cacheManager;
+ }
+
+ public static TransformationManager getTransformationManager() {
+ return transformationManager;
+ }
+}
diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamProvider.java
new file mode 100644
index 000000000..b496ed4f3
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/ParamProvider.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 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.Map;
+
+public interface ParamProvider {
+
+ Map getMultiple(String path);
+
+ String get(String key);
+
+ T get(String key, Class targetClass);
+}
diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SSMProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SSMProvider.java
new file mode 100644
index 000000000..838ae650a
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SSMProvider.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2020 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 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.parameters.cache.CacheManager;
+import software.amazon.lambda.powertools.parameters.transform.TransformationManager;
+import software.amazon.lambda.powertools.parameters.transform.Transformer;
+
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * AWS System Manager Parameter Store Provider
+ *
+ * Samples:
+ *
+ * SSMProvider provider = ParamManager.getSsmProvider();
+ *
+ * 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);
+ *
+ * // Get a decrypted value, and store it in the cache
+ * String value = provider.withDecryption().get("key");
+ *
+ * // Get multiple parameter values starting with the same path
+ * Map params = provider.getMultiple("/path/to/paramters");
+ * >>> /path/to/parameters/key1 -> value1
+ * >>> /path/to/parameters/key2 -> value2
+ *
+ * // Get multiple parameter values starting with the same path and recursively
+ * Map params = provider.recursive().getMultiple("/path/to/paramters");
+ * >>> /path/to/parameters/key1 -> value1
+ * >>> /path/to/parameters/key2 -> value2
+ * >>> /path/to/parameters/others/key3 -> value3
+ *
+ */
+public class SSMProvider extends BaseProvider {
+
+ private final SsmClient client;
+
+ private boolean decrypt = false;
+ private boolean recursive = false;
+
+ /**
+ * Default constructor with default {@link SsmClient}.
+ * Use when you don't need to customize region or any other attribute of the client.
+ *
+ * Use the {@link SSMProvider.Builder} to create an instance of it.
+ */
+ SSMProvider(CacheManager cacheManager) {
+ this(cacheManager, SsmClient.create());
+ }
+
+ /**
+ * 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.
+ *
+ * @param client custom client you would like to use.
+ */
+ SSMProvider(CacheManager cacheManager, SsmClient client) {
+ super(cacheManager);
+ this.client = client;
+ }
+
+ /**
+ * Retrieve the parameter value from the AWS System Manager Parameter Store.
+ *
+ * @param key key of the parameter
+ * @return the value of the parameter identified by the key
+ */
+ @Override
+ public String getValue(String key) {
+ GetParameterRequest request = GetParameterRequest.builder()
+ .name(key)
+ .withDecryption(decrypt)
+ .build();
+ 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 extends Transformer> transformerClass) {
+ super.withTransformation(transformerClass);
+ return this;
+ }
+
+ /**
+ * Tells System Manager Parameter Store to decrypt the parameter value.
+ * By default, parameter values are not decrypted.
+ * Valid both for get and getMultiple.
+ *
+ * @return the provider itself in order to chain calls (eg.
provider.withDecryption().get("key")
).
+ */
+ public SSMProvider withDecryption() {
+ this.decrypt = true;
+ return this;
+ }
+
+ /**
+ * Tells System Manager Parameter Store to retrieve all parameters starting with a path (all levels)
+ * Only used with {@link #getMultiple(String)}.
+ *
+ * @return the provider itself in order to chain calls (eg.
provider.recursive().getMultiple("key")
).
+ */
+ public SSMProvider recursive() {
+ this.recursive = true;
+ return this;
+ }
+
+ /**
+ * Retrieve multiple parameter values from AWS System Manager Parameter Store.
+ * Retrieve all parameters starting with the path provided in parameter.
+ * eg. getMultiple("/foo/bar") will retrieve /foo/bar/baz, foo/bar/biz
+ * Using {@link #recursive()}, getMultiple("/foo/bar") will retrieve /foo/bar/baz, foo/bar/biz and foo/bar/buz/boz
+ * Cache all values with the 'path' as the key and also individually to be able to {@link #get(String)} a single value later
+ * Does not support transformation.
+ *
+ * @param path path of the parameter
+ * @return a map containing parameters keys and values. The key is a subpart of the path
+ * eg. getMultiple("/foo/bar") will retrieve [key="baz", value="valuebaz"] for parameter "/foo/bar/baz"
+ */
+ @Override
+ protected Map getMultipleValues(String path) {
+ return getMultipleBis(path, null);
+ }
+
+ /**
+ * Recursive method to deal with pagination (nextToken)
+ */
+ private Map getMultipleBis(String path, String nextToken) {
+ GetParametersByPathRequest request = GetParametersByPathRequest.builder()
+ .path(path)
+ .withDecryption(decrypt)
+ .recursive(recursive)
+ .nextToken(nextToken)
+ .build();
+
+ Map params = new HashMap<>();
+
+ // not using the client.getParametersByPathPaginator() as hardly testable
+ GetParametersByPathResponse res = client.getParametersByPath(request);
+ if (res.hasParameters()) {
+ res.parameters().forEach(parameter -> {
+ /* Standardize the parameter name
+ The parameter name returned by SSM will contained the full path.
+ However, for readability, we should return only the part after
+ the path.
+ */
+ String name = parameter.name();
+ if (name.startsWith(path)) {
+ name = name.replaceFirst(path, "");
+ }
+ name = name.replaceFirst("/", "");
+ params.put(name, parameter.value());
+ });
+ }
+
+ if (!StringUtils.isEmpty(res.nextToken())) {
+ params.putAll(getMultipleBis(path, res.nextToken()));
+ }
+
+ return params;
+ }
+
+ @Override
+ protected void resetToDefaults() {
+ super.resetToDefaults();
+ recursive = false;
+ decrypt = false;
+ }
+
+ /**
+ * Create a builder that can be used to configure and create a {@link SSMProvider}.
+ *
+ * @return a new instance of {@link SSMProvider.Builder}
+ */
+ public static SSMProvider.Builder builder() {
+ return new SSMProvider.Builder();
+ }
+
+ static class Builder {
+ private SsmClient client;
+ private CacheManager cacheManager;
+ private TransformationManager transformationManager;
+
+ /**
+ * 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) {
+ provider = new SSMProvider(cacheManager, client);
+ } else {
+ provider = new SSMProvider(cacheManager);
+ }
+ 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/src/main/java/software/amazon/lambda/powertools/parameters/SecretsProvider.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SecretsProvider.java
new file mode 100644
index 000000000..13c5b6b22
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/SecretsProvider.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2020 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 software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
+import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
+import software.amazon.lambda.powertools.parameters.cache.CacheManager;
+import software.amazon.lambda.powertools.parameters.transform.TransformationManager;
+import software.amazon.lambda.powertools.parameters.transform.Transformer;
+
+import java.time.temporal.ChronoUnit;
+import java.util.Base64;
+import java.util.Map;
+
+/**
+ * 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;
+
+ /**
+ * Default constructor with default {@link SecretsManagerClient}.
+ * Use when you don't need to customize region or any other attribute of the client.
+ *
+ * Use the {@link Builder} to create an instance of it.
+ */
+ SecretsProvider(CacheManager cacheManager) {
+ this(cacheManager, SecretsManagerClient.create());
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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()));
+ }
+ 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 extends Transformer> transformerClass) {
+ super.withTransformation(transformerClass);
+ return this;
+ }
+
+ /**
+ * 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();
+ }
+
+ static class Builder {
+
+ private SecretsManagerClient client;
+ private CacheManager cacheManager;
+ private TransformationManager transformationManager;
+
+ /**
+ * 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) {
+ provider = new SecretsProvider(cacheManager, client);
+ } else {
+ provider = new SecretsProvider(cacheManager);
+ }
+ 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/cache/CacheManager.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/CacheManager.java
new file mode 100644
index 000000000..687337a96
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/CacheManager.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 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.cache;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
+
+import static java.time.temporal.ChronoUnit.SECONDS;
+
+public class CacheManager {
+ static final Duration DEFAULT_MAX_AGE_SECS = Duration.of(5, SECONDS);
+
+ private final DataStore store;
+ private Duration defaultMaxAge = DEFAULT_MAX_AGE_SECS;
+ private Duration maxAge = defaultMaxAge;
+
+ public CacheManager() {
+ store = new DataStore();
+ }
+
+ public Optional getIfNotExpired(String key, Instant now) {
+ if (store.hasExpired(key, now)) {
+ return Optional.empty();
+ }
+ return Optional.of((T) store.get(key));
+ }
+
+ public void setExpirationTime(Duration duration) {
+ this.maxAge = duration;
+ }
+
+ public void setDefaultExpirationTime(Duration duration) {
+ this.defaultMaxAge = duration;
+ this.maxAge = duration;
+ }
+
+ public void putInCache(String key, T value) {
+ store.put(key, value, Clock.systemDefaultZone().instant().plus(maxAge));
+ }
+
+ public void resetExpirationTime() {
+ maxAge = defaultMaxAge;
+ }
+}
diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/DataStore.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/DataStore.java
new file mode 100644
index 000000000..351ba054d
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/cache/DataStore.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 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.cache;
+
+import java.time.Instant;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Internal store used to cache parameters
+ */
+public class DataStore {
+
+ private final ConcurrentHashMap store;
+
+ public DataStore() {
+ this.store = new ConcurrentHashMap<>();
+ }
+
+ static class ValueNode {
+ public final Object value;
+ public final Instant time;
+
+ public ValueNode(Object value, Instant time){
+ this.value = value;
+ this.time = time;
+ }
+ }
+
+ public void put(String key, Object value, Instant time){
+ store.put(key, new ValueNode(value, time));
+ }
+
+ public void remove(String Key){
+ store.remove(Key);
+ }
+
+ public Object get(String key) {
+ return store.containsKey(key)?store.get(key).value:null;
+ }
+
+ public boolean hasExpired(String key, Instant now) {
+ boolean hasExpired = !store.containsKey(key) || now.isAfter(store.get(key).time);
+ // Auto-clean if the parameter has expired
+ if (hasExpired) {
+ remove(key);
+ }
+ return hasExpired;
+ }
+}
diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/TransformationException.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/TransformationException.java
new file mode 100644
index 000000000..7d28d12d1
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/exception/TransformationException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2020 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.exception;
+
+public class TransformationException extends RuntimeException {
+
+ public TransformationException(Exception e) {
+ super(e);
+ }
+
+ public TransformationException(String message) { super(message);
+ }
+}
diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Base64Transformer.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Base64Transformer.java
new file mode 100644
index 000000000..b9af4fbd4
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Base64Transformer.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 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.transform;
+
+import software.amazon.lambda.powertools.parameters.exception.TransformationException;
+
+import java.util.Base64;
+
+/**
+ * Transformer that take a base64 encoded string and return a decoded string.
+ */
+public class Base64Transformer extends BasicTransformer {
+
+ @Override
+ public String applyTransformation(String value) throws TransformationException {
+ try {
+ return new String(Base64.getDecoder().decode(value));
+ } catch (Exception e) {
+ throw new TransformationException(e);
+ }
+ }
+}
diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/BasicTransformer.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/BasicTransformer.java
new file mode 100644
index 000000000..5251d9f16
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/BasicTransformer.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020 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.transform;
+
+import software.amazon.lambda.powertools.parameters.exception.TransformationException;
+
+/**
+ * Abstract transformer that take a String and transform it in another String.
+ */
+public abstract class BasicTransformer implements Transformer {
+
+ @Override
+ public String applyTransformation(String value, Class targetClass) throws TransformationException {
+ return applyTransformation(value);
+ }
+
+ public abstract String applyTransformation(String value);
+}
diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformer.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformer.java
new file mode 100644
index 000000000..d84a1ab3a
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformer.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2020 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.transform;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import software.amazon.lambda.powertools.parameters.exception.TransformationException;
+
+/**
+ * Transformer that transform a json string into an Object. Based on Jackson.
+ *
+ * @param type of the Object to create during transformation.
+ */
+public class JsonTransformer implements Transformer {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Override
+ public T applyTransformation(String value, Class targetClass) throws TransformationException {
+ try {
+ return mapper.readValue(value, targetClass);
+ } catch (JsonProcessingException e) {
+ throw new TransformationException(e);
+ }
+ }
+}
diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/TransformationManager.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/TransformationManager.java
new file mode 100644
index 000000000..00e6f84a9
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/TransformationManager.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2020 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.transform;
+
+import software.amazon.lambda.powertools.parameters.exception.TransformationException;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Manager in charge of transforming parameter values in another format.
+ * Leverages a {@link Transformer} in order to perform the transformation.
+ * The transformer must be passed with {@link #setTransformer(Class)} before performing any transform operation.
+ */
+public class TransformationManager {
+
+ private Class extends Transformer> transformer = null;
+
+ /**
+ * Set the {@link Transformer} to use for transformation. Must be called before any transformation.
+ *
+ * @param transformerClass class of the {@link Transformer}
+ */
+ public void setTransformer(Class extends Transformer> transformerClass) {
+ this.transformer = transformerClass;
+ }
+
+ /**
+ * @return true if a {@link Transformer} has been passed to the Manager
+ */
+ public boolean shouldTransform() {
+ return transformer != null;
+ }
+
+ /**
+ * Transform a String in another String. Must be used with a {@link BasicTransformer}.
+ *
+ * @param value the value to transform
+ * @return the value transformed
+ */
+ public String performBasicTransformation(String value) {
+ if (transformer == null) {
+ throw new IllegalStateException("You cannot perform a transformation without Transformer, use the provider.withTransformation() method to specify it.");
+ }
+ if (!BasicTransformer.class.isAssignableFrom(transformer)) {
+ throw new IllegalStateException("Wrong Transformer for a String, choose a BasicTransformer.");
+ }
+ try {
+ BasicTransformer basicTransformer = (BasicTransformer) transformer.getDeclaredConstructor().newInstance(null);
+ return basicTransformer.applyTransformation(value);
+ } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
+ throw new TransformationException(e);
+ }
+ }
+
+ /**
+ * Transform a String in a Java Object.
+ *
+ * @param value the value to transform
+ * @param targetClass the type of the target object.
+ * @return the value transformed in an object ot type T.
+ */
+ public T performComplexTransformation(String value, Class targetClass) {
+ if (transformer == null) {
+ throw new IllegalStateException("You cannot perform a transformation without Transformer, use the provider.withTransformation() method to specify it.");
+ }
+
+ try {
+ Transformer complexTransformer = transformer.getDeclaredConstructor().newInstance(null);
+ return complexTransformer.applyTransformation(value, targetClass);
+ } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
+ throw new TransformationException(e);
+ }
+ }
+}
diff --git a/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Transformer.java b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Transformer.java
new file mode 100644
index 000000000..3c57b2aa9
--- /dev/null
+++ b/powertools-parameters/src/main/java/software/amazon/lambda/powertools/parameters/transform/Transformer.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020 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.transform;
+
+import software.amazon.lambda.powertools.parameters.exception.TransformationException;
+
+/**
+ * Interface for parameter transformers. Implement it to create a new Transformer.
+ *
+ * @param type of the target object that will be created with the transformer.
+ */
+public interface Transformer {
+
+ /**
+ * Convenient access to {@link JsonTransformer}, to use in providers (
provider.withTransformation(json)
)
+ */
+ Class json = JsonTransformer.class;
+
+ /**
+ * Convenient access to {@link Base64Transformer}, to use in providers (
provider.withTransformation(base64)
)
+ */
+ Class base64 = Base64Transformer.class;
+
+ /**
+ * Apply a transformation on the input value (String)
+ * @param value the parameter value to transform
+ * @param targetClass class of the target object
+ * @return a transformed parameter
+ * @throws TransformationException when a transformation error occurs
+ */
+ T applyTransformation(String value, Class targetClass) throws TransformationException;
+}
diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java
new file mode 100644
index 000000000..8dd2d7658
--- /dev/null
+++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/BaseProviderTest.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright 2020 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.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.lambda.powertools.parameters.cache.CacheManager;
+import software.amazon.lambda.powertools.parameters.transform.ObjectToDeserialize;
+import software.amazon.lambda.powertools.parameters.transform.TransformationManager;
+import software.amazon.lambda.powertools.parameters.transform.Transformer;
+
+import java.time.Clock;
+import java.time.temporal.ChronoUnit;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+import static java.time.Clock.offset;
+import static java.time.Duration.of;
+import static java.time.temporal.ChronoUnit.MINUTES;
+import static java.time.temporal.ChronoUnit.SECONDS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.mockito.MockitoAnnotations.openMocks;
+import static software.amazon.lambda.powertools.parameters.transform.Transformer.base64;
+import static software.amazon.lambda.powertools.parameters.transform.Transformer.json;
+
+public class BaseProviderTest {
+
+ Clock clock;
+ CacheManager cacheManager;
+ TransformationManager transformationManager;
+ BasicProvider provider;
+
+ boolean getFromStore = false;
+
+ class BasicProvider extends BaseProvider {
+
+ public BasicProvider(CacheManager cacheManager) {
+ super(cacheManager);
+ }
+
+ private String value = "valueFromStore";
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ @Override
+ protected String getValue(String key) {
+ getFromStore = true;
+ return value;
+ }
+
+ @Override
+ protected Map getMultipleValues(String path) {
+ getFromStore = true;
+ Map map = new HashMap<>();
+ map.put(path, value);
+ return map;
+ }
+ }
+
+ @BeforeEach
+ public void setup() {
+ openMocks(this);
+
+ clock = Clock.systemDefaultZone();
+ cacheManager = new CacheManager();
+ provider = new BasicProvider(cacheManager);
+ transformationManager = new TransformationManager();
+ provider.setTransformationManager(transformationManager);
+ }
+
+ @Test
+ public void get_notCached_shouldGetValue() {
+ String foo = provider.get("toto");
+
+ assertThat(foo).isEqualTo("valueFromStore");
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void get_cached_shouldGetFromCache() {
+ provider.get("foo");
+ getFromStore = false;
+
+ String foo = provider.get("foo");
+ assertThat(foo).isEqualTo("valueFromStore");
+ assertThat(getFromStore).isFalse();
+ }
+
+ @Test
+ public void get_expired_shouldGetValue() {
+ provider.get("bar");
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(6, SECONDS)));
+
+ provider.get("bar");
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void getMultiple_notCached_shouldGetValue() {
+ Map foo = provider.getMultiple("toto");
+
+ assertThat(foo.get("toto")).isEqualTo("valueFromStore");
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void getMultiple_cached_shouldGetFromCache() {
+ provider.getMultiple("foo");
+ getFromStore = false;
+
+ Map foo = provider.getMultiple("foo");
+ assertThat(foo.get("foo")).isEqualTo("valueFromStore");
+ assertThat(getFromStore).isFalse();
+ }
+
+ @Test
+ public void getMultiple_expired_shouldGetValue() {
+ provider.getMultiple("bar");
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(6, SECONDS)));
+
+ provider.getMultiple("bar");
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void get_customTTL_cached_shouldGetFromCache() {
+ provider.withMaxAge(12, ChronoUnit.MINUTES).get("key");
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(10, MINUTES)));
+
+ provider.get("key");
+ assertThat(getFromStore).isFalse();
+ }
+
+ @Test
+ public void get_customTTL_expired_shouldGetValue() {
+ provider.withMaxAge(2, ChronoUnit.MINUTES).get("mykey");
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(3, MINUTES)));
+
+ provider.get("mykey");
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void get_customDefaultTTL_cached_shouldGetFromCache() {
+ provider.defaultMaxAge(12, ChronoUnit.MINUTES).get("foobar");
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(10, MINUTES)));
+
+ provider.get("foobar");
+ assertThat(getFromStore).isFalse();
+ }
+
+ @Test
+ public void get_customDefaultTTL_expired_shouldGetValue() {
+ provider.defaultMaxAge(2, ChronoUnit.MINUTES).get("barbaz");
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(3, MINUTES)));
+
+ provider.get("barbaz");
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void get_customDefaultTTLAndTTL_cached_shouldGetFromCache() {
+ provider.defaultMaxAge(12, ChronoUnit.MINUTES)
+ .withMaxAge(5, SECONDS)
+ .get("foobaz");
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(4, SECONDS)));
+
+ provider.get("foobaz");
+ assertThat(getFromStore).isFalse();
+ }
+
+ @Test
+ public void get_customDefaultTTLAndTTL_expired_shouldGetValue() {
+ provider.defaultMaxAge(2, ChronoUnit.MINUTES)
+ .withMaxAge(5, SECONDS)
+ .get("bariton");
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(6, SECONDS)));
+
+ provider.get("bariton");
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void get_basicTransformation_shouldTransformInString() {
+ provider.setValue(Base64.getEncoder().encodeToString("bar".getBytes()));
+
+ String value = provider.withTransformation(Transformer.base64).get("base64");
+
+ assertThat(value).isEqualTo("bar");
+ }
+
+ @Test
+ public void get_complexTransformation_shouldTransformInObject() {
+ provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}");
+
+ ObjectToDeserialize objectToDeserialize = provider.withTransformation(json).get("foo", ObjectToDeserialize.class);
+
+ assertThat(objectToDeserialize).matches(
+ o -> o.getFoo().equals("Foo")
+ && o.getBar() == 42
+ && o.getBaz() == 123456789);
+ }
+
+ @Test
+ public void getObject_notCached_shouldGetValue() {
+ provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}");
+
+ ObjectToDeserialize foo = provider.withTransformation(json).get("foo", ObjectToDeserialize.class);
+
+ assertThat(foo).isNotNull();
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void getObject_cached_shouldGetFromCache() {
+ provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}");
+
+ provider.withTransformation(json).get("foo", ObjectToDeserialize.class);
+ getFromStore = false;
+
+ ObjectToDeserialize foo = provider.withTransformation(json).get("foo", ObjectToDeserialize.class);
+ assertThat(foo).isNotNull();
+ assertThat(getFromStore).isFalse();
+ }
+
+ @Test
+ public void getObject_expired_shouldGetValue() {
+ provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}");
+
+ provider.withTransformation(json).get("foo", ObjectToDeserialize.class);
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(6, SECONDS)));
+
+ provider.withTransformation(json).get("foo", ObjectToDeserialize.class);
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void getObject_customTTL_cached_shouldGetFromCache() {
+ provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}");
+
+ provider.withMaxAge(12, ChronoUnit.MINUTES)
+ .withTransformation(json)
+ .get("foo", ObjectToDeserialize.class);
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(10, MINUTES)));
+
+ provider.withTransformation(json).get("foo", ObjectToDeserialize.class);
+ assertThat(getFromStore).isFalse();
+ }
+
+ @Test
+ public void getObject_customTTL_expired_shouldGetValue() {
+ provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}");
+
+ provider.withMaxAge(2, ChronoUnit.MINUTES)
+ .withTransformation(json)
+ .get("foo", ObjectToDeserialize.class);
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(3, MINUTES)));
+
+ provider.withTransformation(json).get("foo", ObjectToDeserialize.class);
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void getObject_customDefaultTTL_cached_shouldGetFromCache() {
+ provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}");
+
+ provider.defaultMaxAge(12, ChronoUnit.MINUTES)
+ .withTransformation(json)
+ .get("foo", ObjectToDeserialize.class);
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(10, MINUTES)));
+
+ provider.withTransformation(json).get("foo", ObjectToDeserialize.class);
+ assertThat(getFromStore).isFalse();
+ }
+
+ @Test
+ public void getObject_customDefaultTTL_expired_shouldGetValue() {
+ provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}");
+
+ provider.defaultMaxAge(2, ChronoUnit.MINUTES)
+ .withTransformation(json)
+ .get("foo", ObjectToDeserialize.class);
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(3, MINUTES)));
+
+ provider.withTransformation(json).get("foo", ObjectToDeserialize.class);
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void getObject_customDefaultTTLAndTTL_cached_shouldGetFromCache() {
+ provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}");
+
+ provider.defaultMaxAge(12, ChronoUnit.MINUTES)
+ .withMaxAge(5, SECONDS)
+ .withTransformation(json)
+ .get("foo", ObjectToDeserialize.class);
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(4, SECONDS)));
+
+ provider.withTransformation(json).get("foo", ObjectToDeserialize.class);
+ assertThat(getFromStore).isFalse();
+ }
+
+ @Test
+ public void getObject_customDefaultTTLAndTTL_expired_shouldGetValue() {
+ provider.setValue("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}");
+
+ provider.defaultMaxAge(2, ChronoUnit.MINUTES)
+ .withMaxAge(5, SECONDS)
+ .withTransformation(json)
+ .get("foo", ObjectToDeserialize.class);
+ getFromStore = false;
+
+ provider.setClock(offset(clock, of(6, SECONDS)));
+
+ provider.withTransformation(json).get("foo", ObjectToDeserialize.class);
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void get_noTransformationManager_shouldThrowException() {
+ provider.setTransformationManager(null);
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> provider.withTransformation(base64).get("foo"));
+ }
+
+ @Test
+ public void getObject_noTransformationManager_shouldThrowException() {
+ provider.setTransformationManager(null);
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> provider.get("foo", ObjectToDeserialize.class));
+ }
+
+ @Test
+ public void getTwoParams_shouldResetTTLOptionsInBetween() {
+ provider.withMaxAge(50, SECONDS).get("foo50");
+
+ provider.get("foo5");
+
+ provider.setClock(offset(clock, of(6, SECONDS)));
+
+ getFromStore = false;
+
+ provider.get("foo5");
+ assertThat(getFromStore).isTrue();
+ }
+
+ @Test
+ public void getTwoParams_shouldResetTransformationOptionsInBetween() {
+ provider.setValue(Base64.getEncoder().encodeToString("base64encoded".getBytes()));
+ String foob64 = provider.withTransformation(base64).get("foob64");
+
+ provider.setValue("string");
+ String foostr = provider.get("foostr");
+
+ assertThat(foob64).isEqualTo("base64encoded");
+ assertThat(foostr).isEqualTo("string");
+ }
+}
diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java
new file mode 100644
index 000000000..813ed7638
--- /dev/null
+++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/ParamManagerIntegrationTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2020 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.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.secretsmanager.SecretsManagerClient;
+import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
+import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
+import software.amazon.awssdk.services.ssm.SsmClient;
+import software.amazon.awssdk.services.ssm.model.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+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.*;
+import static org.mockito.MockitoAnnotations.openMocks;
+
+public class ParamManagerIntegrationTest {
+
+ @Mock
+ SsmClient ssmClient;
+
+ @Captor
+ ArgumentCaptor ssmParamCaptor;
+
+ @Captor
+ ArgumentCaptor ssmParamByPathCaptor;
+
+ @Mock
+ SecretsManagerClient secretsManagerClient;
+
+ @Captor
+ ArgumentCaptor secretsCaptor;
+
+
+ @BeforeEach
+ public void setup() throws IllegalAccessException {
+ openMocks(this);
+
+ writeStaticField(ParamManager.class, "ssmProvider", null, true);
+ writeStaticField(ParamManager.class, "secretsProvider", null, true);
+ }
+
+ @Test
+ public void ssmProvider_get() {
+ SSMProvider ssmProvider = ParamManager.getSsmProvider(ssmClient);
+
+ String expectedValue = "value";
+ Parameter parameter = Parameter.builder().value(expectedValue).build();
+ GetParameterResponse result = GetParameterResponse.builder().parameter(parameter).build();
+ when(ssmClient.getParameter(ssmParamCaptor.capture())).thenReturn(result);
+
+ assertThat(ssmProvider.get("key")).isEqualTo(expectedValue);
+ assertThat(ssmParamCaptor.getValue().name()).isEqualTo("key");
+
+ assertThat(ssmProvider.get("key")).isEqualTo(expectedValue); // second time is from cache
+ verify(ssmClient, times(1)).getParameter(any(GetParameterRequest.class));
+ }
+
+ @Test
+ public void ssmProvider_getMultiple() {
+ SSMProvider ssmProvider = ParamManager.getSsmProvider(ssmClient);
+
+ List parameters = new ArrayList<>();
+ parameters.add(Parameter.builder().name("/prod/app1/key1").value("foo1").build());
+ 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(ssmClient.getParametersByPath(ssmParamByPathCaptor.capture())).thenReturn(response);
+
+ Map params = ssmProvider.getMultiple("/prod/app1");
+ assertThat(ssmParamByPathCaptor.getValue().path()).isEqualTo("/prod/app1");
+
+ assertThat(params).contains(
+ MapEntry.entry("key1", "foo1"),
+ MapEntry.entry("key2", "foo2"),
+ MapEntry.entry("key3", "foo3"));
+
+ assertThat(ssmProvider.get("/prod/app1/key1")).isEqualTo("foo1");
+
+ ssmProvider.getMultiple("/prod/app1");// second time is from cache
+ verify(ssmClient, times(1)).getParametersByPath(any(GetParametersByPathRequest.class));
+ }
+
+ @Test
+ public void secretsProvider_get() {
+ SecretsProvider secretsProvider = ParamManager.getSecretsProvider(secretsManagerClient);
+
+ String expectedValue = "Value1";
+ GetSecretValueResponse response = GetSecretValueResponse.builder().secretString(expectedValue).build();
+ when(secretsManagerClient.getSecretValue(secretsCaptor.capture())).thenReturn(response);
+
+ assertThat(secretsProvider.get("keys")).isEqualTo(expectedValue);
+ assertThat(secretsCaptor.getValue().secretId()).isEqualTo("keys");
+
+ assertThat(secretsProvider.get("keys")).isEqualTo(expectedValue); // second time is from cache
+ verify(secretsManagerClient, times(1)).getSecretValue(any(GetSecretValueRequest.class));
+ }
+}
diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java
new file mode 100644
index 000000000..761979e00
--- /dev/null
+++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SSMProviderTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2020 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.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.ssm.SsmClient;
+import software.amazon.awssdk.services.ssm.model.*;
+import software.amazon.lambda.powertools.parameters.cache.CacheManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+import static org.mockito.MockitoAnnotations.openMocks;
+
+public class SSMProviderTest {
+
+ @Mock
+ SsmClient client;
+
+ @Captor
+ ArgumentCaptor paramCaptor;
+
+ @Captor
+ ArgumentCaptor paramByPathCaptor;
+
+ CacheManager cacheManager;
+
+ SSMProvider provider;
+
+ @BeforeEach
+ public void init() {
+ openMocks(this);
+ cacheManager = new CacheManager();
+ provider = new SSMProvider(cacheManager, client);
+ }
+
+ @Test
+ public void getValue() {
+ String key = "Key1";
+ String expectedValue = "Value1";
+ initMock(expectedValue);
+
+ String value = provider.getValue(key);
+
+ assertThat(value).isEqualTo(expectedValue);
+ assertThat(paramCaptor.getValue().name()).isEqualTo(key);
+ assertThat(paramCaptor.getValue().withDecryption()).isFalse();
+ }
+
+ @Test
+ public void getValueDecrypted() {
+ String key = "Key2";
+ String expectedValue = "Value2";
+ initMock(expectedValue);
+
+ String value = provider.withDecryption().getValue(key);
+
+ assertThat(value).isEqualTo(expectedValue);
+ assertThat(paramCaptor.getValue().name()).isEqualTo(key);
+ assertThat(paramCaptor.getValue().withDecryption()).isTrue();
+ }
+
+ @Test
+ public void getMultiple() {
+ List parameters = new ArrayList<>();
+ parameters.add(Parameter.builder().name("/prod/app1/key1").value("foo1").build());
+ 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);
+
+ Map params = provider.getMultiple("/prod/app1");
+ assertThat(params).contains(
+ MapEntry.entry("key1", "foo1"),
+ MapEntry.entry("key2", "foo2"),
+ MapEntry.entry("key3", "foo3"));
+ assertThat(provider.get("/prod/app1/key1")).isEqualTo("foo1");
+ assertThat(provider.get("/prod/app1/key2")).isEqualTo("foo2");
+ assertThat(provider.get("/prod/app1/key3")).isEqualTo("foo3");
+
+ assertThat(paramByPathCaptor.getValue().path()).isEqualTo("/prod/app1");
+ assertThat(paramByPathCaptor.getValue().withDecryption()).isFalse();
+ assertThat(paramByPathCaptor.getValue().recursive()).isFalse();
+ }
+
+ @Test
+ public void getMultiple_cached_shouldNotCallSSM() {
+ List parameters = new ArrayList<>();
+ parameters.add(Parameter.builder().name("/prod/app1/key1").value("foo1").build());
+ 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);
+
+ provider.getMultiple("/prod/app1");
+
+ // should get the following from cache
+ provider.getMultiple("/prod/app1");
+ provider.get("/prod/app1/key1");
+ provider.get("/prod/app1/key2");
+ provider.get("/prod/app1/key3");
+
+ verify(client, times(1)).getParametersByPath(any(GetParametersByPathRequest.class));
+
+ }
+
+ @Test
+ public void getMultipleWithNextToken() {
+ List parameters1 = new ArrayList<>();
+ parameters1.add(Parameter.builder().name("/prod/app1/key1").value("foo1").build());
+ parameters1.add(Parameter.builder().name("/prod/app1/key2").value("foo2").build());
+ GetParametersByPathResponse response1 = GetParametersByPathResponse.builder().parameters(parameters1).nextToken("123abc").build();
+
+ List parameters2 = new ArrayList<>();
+ 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);
+
+ Map params = provider.getMultiple("/prod/app1");
+
+ assertThat(params).contains(
+ MapEntry.entry("key1", "foo1"),
+ MapEntry.entry("key2", "foo2"),
+ MapEntry.entry("key3", "foo3"));
+
+ List requestParams = paramByPathCaptor.getAllValues();
+ GetParametersByPathRequest request1 = requestParams.get(0);
+ GetParametersByPathRequest request2 = requestParams.get(1);
+
+ assertThat(asList(request1, request2)).allSatisfy(req -> {
+ assertThat(req.path()).isEqualTo("/prod/app1");
+ assertThat(req.withDecryption()).isFalse();
+ assertThat(req.recursive()).isFalse();
+ });
+
+ assertThat(request1.nextToken()).isNull();
+ assertThat(request2.nextToken()).isEqualTo("123abc");
+ }
+
+ 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);
+ }
+
+}
diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SecretsProviderTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SecretsProviderTest.java
new file mode 100644
index 000000000..611f05fa9
--- /dev/null
+++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/SecretsProviderTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2020 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.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 software.amazon.awssdk.core.SdkBytes;
+import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
+import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
+import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
+import software.amazon.lambda.powertools.parameters.cache.CacheManager;
+
+import java.util.Base64;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.MockitoAnnotations.openMocks;
+
+public class SecretsProviderTest {
+
+ @Mock
+ SecretsManagerClient client;
+
+ @Captor
+ ArgumentCaptor paramCaptor;
+
+ CacheManager cacheManager;
+
+ SecretsProvider provider;
+
+ @BeforeEach
+ public void init() {
+ openMocks(this);
+ cacheManager = new CacheManager();
+ provider = new SecretsProvider(cacheManager, client);
+ }
+
+ @Test
+ public void getValue() {
+ String key = "Key1";
+ String expectedValue = "Value1";
+ GetSecretValueResponse response = GetSecretValueResponse.builder().secretString(expectedValue).build();
+ Mockito.when(client.getSecretValue(paramCaptor.capture())).thenReturn(response);
+
+ String value = provider.getValue(key);
+
+ assertThat(value).isEqualTo(expectedValue);
+ assertThat(paramCaptor.getValue().secretId()).isEqualTo(key);
+ }
+
+ @Test
+ public void getValueBase64() {
+ String key = "Key2";
+ String expectedValue = "Value2";
+ byte[] valueb64 = Base64.getEncoder().encode(expectedValue.getBytes());
+ GetSecretValueResponse response = GetSecretValueResponse.builder().secretBinary(SdkBytes.fromByteArray(valueb64)).build();
+ Mockito.when(client.getSecretValue(paramCaptor.capture())).thenReturn(response);
+
+ String value = provider.getValue(key);
+
+ assertThat(value).isEqualTo(expectedValue);
+ assertThat(paramCaptor.getValue().secretId()).isEqualTo(key);
+ }
+}
diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/CacheManagerTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/CacheManagerTest.java
new file mode 100644
index 000000000..2464b4278
--- /dev/null
+++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/CacheManagerTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2020 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.cache;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.Clock;
+import java.util.Optional;
+
+import static java.time.Clock.offset;
+import static java.time.Duration.of;
+import static java.time.temporal.ChronoUnit.SECONDS;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CacheManagerTest {
+
+ CacheManager manager;
+
+ Clock clock;
+
+ @BeforeEach
+ public void setup() {
+ clock = Clock.systemDefaultZone();
+ manager = new CacheManager();
+ }
+
+ @Test
+ public void getIfNotExpired_notExpired_shouldReturnValue() {
+ manager.putInCache("key", "value");
+
+ Optional value = manager.getIfNotExpired("key", clock.instant());
+
+ assertThat(value).isPresent().contains("value");
+ }
+
+ @Test
+ public void getIfNotExpired_expired_shouldReturnNothing() {
+ manager.putInCache("key", "value");
+
+ Optional value = manager.getIfNotExpired("key", offset(clock, of(6, SECONDS)).instant());
+
+ assertThat(value).isNotPresent();
+ }
+
+ @Test
+ public void getIfNotExpired_withCustomExpirationTime_notExpired_shouldReturnValue() {
+ manager.setExpirationTime(of(42, SECONDS));
+ manager.putInCache("key", "value");
+
+ Optional value = manager.getIfNotExpired("key", offset(clock, of(40, SECONDS)).instant());
+
+ assertThat(value).isPresent().contains("value");
+ }
+
+ @Test
+ public void getIfNotExpired_withCustomDefaultExpirationTime_notExpired_shouldReturnValue() {
+ manager.setDefaultExpirationTime(of(42, SECONDS));
+ manager.putInCache("key", "value");
+
+
+ Optional value = manager.getIfNotExpired("key", offset(clock, of(40, SECONDS)).instant());
+
+ assertThat(value).isPresent().contains("value");
+ }
+
+ @Test
+ public void getIfNotExpired_customDefaultExpirationTime_customExpirationTime_shouldUseExpirationTime() {
+ manager.setDefaultExpirationTime(of(42, SECONDS));
+ manager.setExpirationTime(of(2, SECONDS));
+ manager.putInCache("key", "value");
+
+ Optional value = manager.getIfNotExpired("key", offset(clock, of(40, SECONDS)).instant());
+
+ assertThat(value).isNotPresent();
+ }
+
+ @Test
+ public void getIfNotExpired_resetExpirationTime_shouldUseDefaultExpirationTime() {
+ manager.setDefaultExpirationTime(of(42, SECONDS));
+ manager.setExpirationTime(of(2, SECONDS));
+ manager.putInCache("key", "value");
+ manager.resetExpirationTime();
+ manager.putInCache("key2", "value2");
+
+ Optional value = manager.getIfNotExpired("key", offset(clock, of(40, SECONDS)).instant());
+ Optional value2 = manager.getIfNotExpired("key2", offset(clock, of(40, SECONDS)).instant());
+
+ assertThat(value).isNotPresent();
+ assertThat(value2).isPresent().contains("value2");
+ }
+
+}
diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/DataStoreTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/DataStoreTest.java
new file mode 100644
index 000000000..c68992bf1
--- /dev/null
+++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/cache/DataStoreTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2020 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.cache;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.Clock;
+import java.time.Instant;
+
+import static java.time.Clock.offset;
+import static java.time.Duration.of;
+import static java.time.temporal.ChronoUnit.SECONDS;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DataStoreTest {
+
+ Clock clock;
+ DataStore store;
+
+ @BeforeEach
+ public void setup() {
+ clock = Clock.systemDefaultZone();
+ store = new DataStore();
+ }
+
+ @Test
+ public void put_shouldInsertInStore() {
+ store.put("key", "value", Instant.now());
+ assertThat(store.get("key")).isEqualTo("value");
+ }
+
+ @Test
+ public void get_invalidKey_shouldReturnNull() {
+ assertThat(store.get("key")).isNull();
+ }
+
+ @Test
+ public void hasExpired_invalidKey_shouldReturnTrue() {
+ assertThat(store.hasExpired("key", clock.instant())).isTrue();
+ }
+
+ @Test
+ public void hasExpired_notExpired_shouldReturnFalse() {
+ Instant now = Instant.now();
+
+ store.put("key", "value", now.plus(10, SECONDS));
+
+ assertThat(store.hasExpired("key", offset(clock, of(5, SECONDS)).instant())).isFalse();
+ }
+
+ @Test
+ public void hasExpired_expired_shouldReturnTrueAndRemoveElement() {
+ Instant now = Instant.now();
+
+ store.put("key", "value", now.plus(10, SECONDS));
+
+ assertThat(store.hasExpired("key", offset(clock, of(11, SECONDS)).instant())).isTrue();
+ assertThat(store.get("key")).isNull();
+ }
+}
diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/Base64TransformerTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/Base64TransformerTest.java
new file mode 100644
index 000000000..428b7e0ab
--- /dev/null
+++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/Base64TransformerTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 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.transform;
+
+import org.junit.jupiter.api.Test;
+import software.amazon.lambda.powertools.parameters.exception.TransformationException;
+
+import java.util.Base64;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+public class Base64TransformerTest {
+
+ @Test
+ public void transform_base64_shouldTransformInString() {
+ Base64Transformer transformer = new Base64Transformer();
+
+ String s = transformer.applyTransformation(Base64.getEncoder().encodeToString("foobar".getBytes()));
+
+ assertThat(s).isEqualTo("foobar");
+ }
+
+ @Test
+ public void transform_base64WrongFormat_shouldThrowException() {
+ Base64Transformer transformer = new Base64Transformer();
+
+ assertThatExceptionOfType(TransformationException.class)
+ .isThrownBy(() -> transformer.applyTransformation("foobarbaz"));
+ }
+}
diff --git a/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformerTest.java b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformerTest.java
new file mode 100644
index 000000000..fe4fae0bb
--- /dev/null
+++ b/powertools-parameters/src/test/java/software/amazon/lambda/powertools/parameters/transform/JsonTransformerTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 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.transform;
+
+import org.junit.jupiter.api.Test;
+import software.amazon.lambda.powertools.parameters.exception.TransformationException;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.data.MapEntry.entry;
+
+public class JsonTransformerTest {
+
+ @Test
+ public void transform_json_shouldTransformInObject() throws TransformationException {
+ JsonTransformer transformation = new JsonTransformer<>();
+
+ ObjectToDeserialize objectToDeserialize = transformation.applyTransformation("{\"foo\":\"Foo\", \"bar\":42, \"baz\":123456789}", ObjectToDeserialize.class);
+ assertThat(objectToDeserialize).matches(
+ o -> o.getFoo().equals("Foo")
+ && o.getBar() == 42
+ && o.getBaz() == 123456789);
+ }
+
+ @Test
+ public void transform_json_shouldTransformInHashMap() throws TransformationException {
+ JsonTransformer
+
+ software.amazon.awssdk
+ aws-core
+ com.amazonawsaws-lambda-java-core
@@ -62,6 +66,10 @@
com.amazonawsaws-xray-recorder-sdk-core
+
+ com.amazonaws
+ aws-xray-recorder-sdk-aws-sdk-core
+ com.amazonawsaws-xray-recorder-sdk-aws-sdk-v2
From 74fae0edc1c743e17cb1f05da72a5560ab1edbcb Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 2 Oct 2020 08:14:35 +0200
Subject: [PATCH 0021/1547] build(deps): bump jackson-databind from 2.11.2 to
2.11.3 (#117)
Bumps [jackson-databind](https://github.com/FasterXML/jackson) from 2.11.2 to 2.11.3.
- [Release notes](https://github.com/FasterXML/jackson/releases)
- [Commits](https://github.com/FasterXML/jackson/commits)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 9dd4347df..baf97c25d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,7 +51,7 @@
1.81.82.13.3
- 2.11.2
+ 2.11.31.9.62.14.102.7.1
From aaba5c7d680b919c7b6643498a45e1d8c1d5fc95 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 2 Oct 2020 08:14:41 +0200
Subject: [PATCH 0022/1547] build(deps): bump software.amazon.awssdk:bom from
2.14.10 to 2.15.1 (#118)
Bumps [software.amazon.awssdk:bom](https://github.com/aws/aws-sdk-java-v2) from 2.14.10 to 2.15.1.
- [Release notes](https://github.com/aws/aws-sdk-java-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-java-v2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-java-v2/compare/2.14.10...2.15.1)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index baf97c25d..5ff98ac6c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -53,7 +53,7 @@
2.13.32.11.31.9.6
- 2.14.10
+ 2.15.12.7.11.1.0UTF-8
From febb4263e65bac2767df7592f8a305677ce09c7e Mon Sep 17 00:00:00 2001
From: Pankaj Agrawal
Date: Fri, 2 Oct 2020 09:44:57 +0200
Subject: [PATCH 0023/1547] ci: Prepare for 0.4.0-beta (#116)
---
README.md | 6 +++---
docs/content/index.mdx | 13 +++++++++++--
docs/content/utilities/parameters.mdx | 12 ++++++++++++
.../utilities/sqs_large_message_handling.mdx | 2 +-
example/HelloWorldFunction/pom.xml | 8 ++++----
pom.xml | 2 +-
powertools-core/pom.xml | 2 +-
powertools-logging/pom.xml | 2 +-
powertools-metrics/pom.xml | 2 +-
powertools-parameters/pom.xml | 2 +-
powertools-sqs/pom.xml | 2 +-
powertools-tracing/pom.xml | 2 +-
12 files changed, 38 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index f37e6ca7f..5b1b878cf 100644
--- a/README.md
+++ b/README.md
@@ -17,17 +17,17 @@ Powertools is available in Maven Central. You can use your favourite dependency
software.amazon.lambdapowertools-tracing
- 0.3.1-beta
+ 0.4.0-betasoftware.amazon.lambdapowertools-logging
- 0.3.1-beta
+ 0.4.0-betasoftware.amazon.lambdapowertools-metrics
- 0.3.1-beta
+ 0.4.0-beta
...
diff --git a/docs/content/index.mdx b/docs/content/index.mdx
index 6a9282683..41ff2d576 100644
--- a/docs/content/index.mdx
+++ b/docs/content/index.mdx
@@ -16,12 +16,17 @@ Powertools dependencies are available in Maven Central. You can use your favouri
software.amazon.lambdapowertools-tracing
- 0.3.1-beta
+ 0.4.0-betasoftware.amazon.lambdapowertools-logging
- 0.3.1-beta
+ 0.4.0-beta
+
+
+ software.amazon.lambda
+ powertools-metrics
+ 0.4.0-beta
...
@@ -50,6 +55,10 @@ And configure the aspectj-maven-plugin to compile-time weave (CTW) the aws-lambd
software.amazon.lambdapowertools-logging
+
+ software.amazon.lambda
+ powertools-metrics
+
diff --git a/docs/content/utilities/parameters.mdx b/docs/content/utilities/parameters.mdx
index 2a9d44d65..964d834ab 100644
--- a/docs/content/utilities/parameters.mdx
+++ b/docs/content/utilities/parameters.mdx
@@ -15,6 +15,18 @@ The parameters utility provides a way to retrieve parameter values from
* Cache parameter values for a given amount of time (defaults to 5 seconds)
* Transform parameter values from JSON or base 64 encoded strings
+## Install
+
+To install this utility, add the following dependency to your project.
+
+```xml
+
+ software.amazon.lambda
+ powertools-parameters
+ 0.4.0-beta
+
+```
+
**IAM Permissions**
This utility requires additional permissions to work as expected. See the table below:
diff --git a/docs/content/utilities/sqs_large_message_handling.mdx b/docs/content/utilities/sqs_large_message_handling.mdx
index eb41f1c39..cb273f38c 100644
--- a/docs/content/utilities/sqs_large_message_handling.mdx
+++ b/docs/content/utilities/sqs_large_message_handling.mdx
@@ -29,7 +29,7 @@ To install this utility, add the following dependency to your project.
software.amazon.lambdapowertools-sqs
- 0.3.1-beta
+ 0.4.0-beta
```
diff --git a/example/HelloWorldFunction/pom.xml b/example/HelloWorldFunction/pom.xml
index 778254672..9ad3559f9 100644
--- a/example/HelloWorldFunction/pom.xml
+++ b/example/HelloWorldFunction/pom.xml
@@ -16,22 +16,22 @@
software.amazon.lambdapowertools-tracing
- 0.3.1-beta
+ 0.4.0-betasoftware.amazon.lambdapowertools-logging
- 0.3.1-beta
+ 0.4.0-betasoftware.amazon.lambdapowertools-metrics
- 0.3.1-beta
+ 0.4.0-betasoftware.amazon.lambdapowertools-parameters
- 0.3.1-beta
+ 0.4.0-betacom.amazonaws
diff --git a/pom.xml b/pom.xml
index 5ff98ac6c..cf5f1a3e2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
software.amazon.lambdapowertools-parent
- 0.3.1-beta
+ 0.4.0-betapomAWS Lambda Powertools Java library Parent
diff --git a/powertools-core/pom.xml b/powertools-core/pom.xml
index b82b38ff8..8206cb583 100644
--- a/powertools-core/pom.xml
+++ b/powertools-core/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.3.1-beta
+ 0.4.0-betaAWS Lambda Powertools Java library Core
diff --git a/powertools-logging/pom.xml b/powertools-logging/pom.xml
index f2b210636..70c51a48e 100644
--- a/powertools-logging/pom.xml
+++ b/powertools-logging/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.3.1-beta
+ 0.4.0-betaAWS Lambda Powertools Java library Logging
diff --git a/powertools-metrics/pom.xml b/powertools-metrics/pom.xml
index b9b344887..56009e46b 100644
--- a/powertools-metrics/pom.xml
+++ b/powertools-metrics/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.3.1-beta
+ 0.4.0-betaAWS Lambda Powertools Java library Metrics
diff --git a/powertools-parameters/pom.xml b/powertools-parameters/pom.xml
index ec53cb412..3dc815f93 100644
--- a/powertools-parameters/pom.xml
+++ b/powertools-parameters/pom.xml
@@ -7,7 +7,7 @@
powertools-parentsoftware.amazon.lambda
- 0.3.1-beta
+ 0.4.0-betapowertools-parameters
diff --git a/powertools-sqs/pom.xml b/powertools-sqs/pom.xml
index 992b281af..5ea80f5cc 100644
--- a/powertools-sqs/pom.xml
+++ b/powertools-sqs/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.3.1-beta
+ 0.4.0-betaAWS Lambda Powertools Java library SQS
diff --git a/powertools-tracing/pom.xml b/powertools-tracing/pom.xml
index eb83116e0..c056be202 100644
--- a/powertools-tracing/pom.xml
+++ b/powertools-tracing/pom.xml
@@ -10,7 +10,7 @@
powertools-parentsoftware.amazon.lambda
- 0.3.1-beta
+ 0.4.0-betaAWS Lambda Powertools Java library Tracing
From 748b11a81edb79ab53a65d53295136376506b736 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 Oct 2020 08:54:20 +0200
Subject: [PATCH 0024/1547] build(deps): bump software.amazon.awssdk:bom from
2.15.1 to 2.15.2 (#122)
Bumps [software.amazon.awssdk:bom](https://github.com/aws/aws-sdk-java-v2) from 2.15.1 to 2.15.2.
- [Release notes](https://github.com/aws/aws-sdk-java-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-java-v2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-java-v2/compare/2.15.1...2.15.2)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index cf5f1a3e2..3023f0d96 100644
--- a/pom.xml
+++ b/pom.xml
@@ -53,7 +53,7 @@
2.13.32.11.31.9.6
- 2.15.1
+ 2.15.22.7.11.1.0UTF-8
From 6ebbd3f2d8235206cbcbdd9390c513adc40b7c28 Mon Sep 17 00:00:00 2001
From: Pankaj Agrawal
Date: Mon, 5 Oct 2020 16:45:11 +0200
Subject: [PATCH 0025/1547] feat: SQS Partial batch Utility (#120)
* Initial API skeleton for Partial SQS batch util
* Better error handling
* Initial Test cases setup and some refactorings
* Full Test cases coverage
* Rename API method for batch processing
* java docs
* public docs update
* Fix correct place holder for queuename and account
* Example usage with relevant permissions
* Minor doc updates
* Ranme method to set custom sqs client
* Make test less confusing
Co-authored-by: Pankaj Agrawal
---
docs/content/utilities/batch.mdx | 251 ++++++++++++++++++
.../utilities/sqs_large_message_handling.mdx | 6 +-
docs/gatsby-config.js | 3 +-
example/HelloWorldFunction/pom.xml | 9 +
.../src/main/java/helloworld/AppSqsEvent.java | 35 +++
.../main/java/helloworld/AppSqsEventUtil.java | 39 +++
example/events/eventSqs.json | 36 +++
example/template.yaml | 51 ++++
powertools-sqs/pom.xml | 4 +
.../lambda/powertools/sqs/PowertoolsSqs.java | 194 +++++++++++++-
.../sqs/SQSBatchProcessingException.java | 76 ++++++
.../powertools/sqs/SqsBatchProcessor.java | 62 +++++
.../powertools/sqs/SqsMessageHandler.java | 29 ++
.../powertools/sqs/internal/BatchContext.java | 85 ++++++
...Aspect.java => SqsLargeMessageAspect.java} | 4 +-
.../SqsMessageBatchProcessorAspect.java | 37 +++
.../sqs/PowertoolsSqsBatchProcessorTest.java | 230 ++++++++++++++++
...ava => PowertoolsSqsLargeMessageTest.java} | 14 +-
.../powertools/sqs/SampleSqsHandler.java | 12 +
.../sqs/handlers/LambdaHandlerApiGateway.java | 3 +
.../PartialBatchFailureSuppressedHandler.java | 32 +++
.../PartialBatchPartialFailureHandler.java | 32 +++
.../handlers/PartialBatchSuccessHandler.java | 28 ++
...st.java => SqsLargeMessageAspectTest.java} | 18 +-
.../SqsMessageBatchProcessorAspectTest.java | 118 ++++++++
.../test/resources/sampleSqsBatchEvent.json | 36 +++
26 files changed, 1416 insertions(+), 28 deletions(-)
create mode 100644 docs/content/utilities/batch.mdx
create mode 100644 example/HelloWorldFunction/src/main/java/helloworld/AppSqsEvent.java
create mode 100644 example/HelloWorldFunction/src/main/java/helloworld/AppSqsEventUtil.java
create mode 100644 example/events/eventSqs.json
create mode 100644 powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SQSBatchProcessingException.java
create mode 100644 powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsBatchProcessor.java
create mode 100644 powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsMessageHandler.java
create mode 100644 powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/BatchContext.java
rename powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/{SqsMessageAspect.java => SqsLargeMessageAspect.java} (97%)
create mode 100644 powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspect.java
create mode 100644 powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/PowertoolsSqsBatchProcessorTest.java
rename powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/{PowertoolsSqsTest.java => PowertoolsSqsLargeMessageTest.java} (93%)
create mode 100644 powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/SampleSqsHandler.java
create mode 100644 powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchFailureSuppressedHandler.java
create mode 100644 powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchPartialFailureHandler.java
create mode 100644 powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/handlers/PartialBatchSuccessHandler.java
rename powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/{SqsMessageAspectTest.java => SqsLargeMessageAspectTest.java} (96%)
create mode 100644 powertools-sqs/src/test/java/software/amazon/lambda/powertools/sqs/internal/SqsMessageBatchProcessorAspectTest.java
create mode 100644 powertools-sqs/src/test/resources/sampleSqsBatchEvent.json
diff --git a/docs/content/utilities/batch.mdx b/docs/content/utilities/batch.mdx
new file mode 100644
index 000000000..74401a726
--- /dev/null
+++ b/docs/content/utilities/batch.mdx
@@ -0,0 +1,251 @@
+---
+title: SQS Batch Processing
+description: Utility
+---
+
+import Note from "../../src/components/Note"
+
+The SQS batch processing utility provides a way to handle partial failures when processing batches of messages from SQS.
+
+**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.
+
+
+ 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
+
+
+## Install
+
+To install this utility, add the following dependency to your project.
+
+```xml
+
+ software.amazon.lambda
+ powertools-sqs
+ 0.4.0-beta
+
+```
+
+And configure the aspectj-maven-plugin to compile-time weave (CTW) the
+aws-lambda-powertools-java aspects into your project. You may already have this
+plugin in your pom. In that case add the dependency to the `aspectLibraries`
+section.
+
+```xml
+
+
+ ...
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.11
+
+ 1.8
+ 1.8
+ 1.8
+
+
+
+ software.amazon.lambda
+ powertools-sqs
+
+
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+```
+
+**IAM Permissions**
+
+This utility requires additional permissions to work as expected. Lambda functions using this utility require the `sqs:GetQueueUrl` and `sqs:DeleteMessageBatch` permission.
+
+## Processing messages from SQS
+
+You can use either **[SqsBatchProcessor annotation](#SqsBatchProcessor annotation)**, or **[PowertoolsSqs Utility API](#PowertoolsSqs 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)** Raise `SQSBatchProcessingException` to ensure failed messages return to your SQS queue
+
+The only difference is that **PowertoolsSqs 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](#SqsBatchProcessor annotation) and [PowertoolsSqs Utility API](#PowertoolsSqs 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.
+
+### SqsBatchProcessor 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`
+* **Any unprocessed messages detected**, we will raise `SQSBatchProcessingException` to ensure failed messages return to your SQS queue
+
+
+ You will not have accessed to the processed messages within the Lambda Handler - all processing logic will and should be performed by the implemented SqsMessageHandler#process() function.
+
+
+
+```java:title=App.java
+public class AppSqsEvent implements RequestHandler {
+ @Override
+ @SqsBatchProcessor(SampleMessageHandler.class) // highlight-line
+ 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;
+ }
+ }
+}
+```
+
+### PowertoolsSqs Utility API
+
+If you require access to the result of processed messages, you can use this utility.
+
+The result from calling PowertoolsSqs#batchProcessor() on the context manager will be a list of all the return values from your SqsMessageHandler#process() function.
+
+```java:title=App.java
+public class AppSqsEvent implements RequestHandler> {
+ @Override
+ public List handleRequest(SQSEvent input, Context context) {
+ List returnValues = PowertoolsSqs.batchProcessor(input, SampleMessageHandler.class); // highlight-line
+
+ 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;
+ }
+ }
+}
+```
+
+You can also use the utility in a more functional way` by providing inline implementation of functional interface SqsMessageHandler#process()
+
+```java:title=App.java
+public class AppSqsEvent implements RequestHandler> {
+
+ @Override
+ public List handleRequest(SQSEvent input, Context context) {
+ // highlight-start
+ List returnValues = PowertoolsSqs.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;
+ });
+ // highlight-end
+
+ 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
+**[SqsBatchProcessor annotation](#SqsBatchProcessor annotation)**, or **[PowertoolsSqs Utility API](#PowertoolsSqs Utility API)**.
+
+```java:title=App.java
+
+public class AppSqsEvent implements RequestHandler> {
+ // highlight-start
+ static {
+ PowertoolsSqs.overrideSqsClient(SqsClient.builder()
+ .build());
+ }
+ // highlight-end
+
+ @Override
+ public List handleRequest(SQSEvent input, Context context) {
+ List returnValues = PowertoolsSqs.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 SqsBatchProcessor annotation**
+
+```java:title=App.java
+...
+ @Override
+ @SqsBatchProcessor(value = SampleMessageHandler.class, suppressException = true) // highlight-line
+ public String handleRequest(SQSEvent input, Context context) {
+ return "{\"statusCode\": 200}";
+ }
+```
+
+**Within PowertoolsSqs Utility API**
+
+```java:title=App.java
+ @Override
+ public List handleRequest(SQSEvent input, Context context) {
+ List returnValues = PowertoolsSqs.batchProcessor(input, true, SampleMessageHandler.class); // highlight-line
+
+ return returnValues;
+ }
+```
diff --git a/docs/content/utilities/sqs_large_message_handling.mdx b/docs/content/utilities/sqs_large_message_handling.mdx
index cb273f38c..e2e4a77ad 100644
--- a/docs/content/utilities/sqs_large_message_handling.mdx
+++ b/docs/content/utilities/sqs_large_message_handling.mdx
@@ -35,7 +35,7 @@ To install this utility, add the following dependency to your project.
And configure the aspectj-maven-plugin to compile-time weave (CTW) the
aws-lambda-powertools-java aspects into your project. You may already have this
-plugin in your pom. In that case add the depenedency to the `aspectLibraries`
+plugin in your pom. In that case add the dependency to the `aspectLibraries`
section.
```xml
@@ -51,12 +51,12 @@ section.
1.81.8
- ...
+
software.amazon.lambdapowertools-sqs
- ...
+
diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js
index d3cd330a1..b79126b44 100644
--- a/docs/gatsby-config.js
+++ b/docs/gatsby-config.js
@@ -29,7 +29,8 @@ module.exports = {
],
'Utilities': [
'utilities/sqs_large_message_handling',
- 'utilities/parameters'
+ 'utilities/batch',
+ 'utilities/parameters',
],
},
navConfig: {
diff --git a/example/HelloWorldFunction/pom.xml b/example/HelloWorldFunction/pom.xml
index 9ad3559f9..c23351de5 100644
--- a/example/HelloWorldFunction/pom.xml
+++ b/example/HelloWorldFunction/pom.xml
@@ -33,6 +33,11 @@
powertools-parameters0.4.0-beta
+
+ software.amazon.lambda
+ powertools-sqs
+ 0.4.0-beta
+ com.amazonawsaws-lambda-java-core
@@ -90,6 +95,10 @@
software.amazon.lambdapowertools-metrics
+
+ software.amazon.lambda
+ powertools-sqs
+
diff --git a/example/HelloWorldFunction/src/main/java/helloworld/AppSqsEvent.java b/example/HelloWorldFunction/src/main/java/helloworld/AppSqsEvent.java
new file mode 100644
index 000000000..ff9d050af
--- /dev/null
+++ b/example/HelloWorldFunction/src/main/java/helloworld/AppSqsEvent.java
@@ -0,0 +1,35 @@
+package helloworld;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import software.amazon.lambda.powertools.logging.PowertoolsLogging;
+import software.amazon.lambda.powertools.sqs.SqsBatchProcessor;
+import software.amazon.lambda.powertools.sqs.SqsMessageHandler;
+
+import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
+
+public class AppSqsEvent implements RequestHandler {
+ private static final Logger LOG = LogManager.getLogger(AppSqsEvent.class);
+
+ @Override
+ @SqsBatchProcessor(SampleMessageHandler.class)
+ @PowertoolsLogging(logEvent = true)
+ public String handleRequest(SQSEvent input, Context context) {
+ return "{\"statusCode\": 200}";
+ }
+
+ public class SampleMessageHandler implements SqsMessageHandler {
+
+ @Override
+ public String process(SQSMessage message) {
+ if("19dd0b57-b21e-4ac1-bd88-01bbb068cb99".equals(message.getMessageId())) {
+ throw new RuntimeException(message.getMessageId());
+ }
+ LOG.info("Processing message with details {}", message);
+ return message.getMessageId();
+ }
+ }
+}
diff --git a/example/HelloWorldFunction/src/main/java/helloworld/AppSqsEventUtil.java b/example/HelloWorldFunction/src/main/java/helloworld/AppSqsEventUtil.java
new file mode 100644
index 000000000..a1300defc
--- /dev/null
+++ b/example/HelloWorldFunction/src/main/java/helloworld/AppSqsEventUtil.java
@@ -0,0 +1,39 @@
+package helloworld;
+
+import java.util.List;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import software.amazon.lambda.powertools.sqs.PowertoolsSqs;
+import software.amazon.lambda.powertools.sqs.SQSBatchProcessingException;
+
+import static java.util.Collections.emptyList;
+
+public class AppSqsEventUtil implements RequestHandler> {
+ private static final Logger LOG = LogManager.getLogger(AppSqsEventUtil.class);
+
+ @Override
+ public List handleRequest(SQSEvent input, Context context) {
+ try {
+
+ return PowertoolsSqs.batchProcessor(input, (message) -> {
+ if ("19dd0b57-b21e-4ac1-bd88-01bbb068cb99".equals(message.getMessageId())) {
+ throw new RuntimeException(message.getMessageId());
+ }
+
+ LOG.info("Processing message with details {}", message);
+ return message.getMessageId();
+ });
+
+ } catch (SQSBatchProcessingException e) {
+ LOG.info("Exception details {}", e.getMessage(), e);
+ LOG.info("Success message Returns{}", e.successMessageReturnValues());
+ LOG.info("Failed messages {}", e.getFailures());
+ LOG.info("Failed messages Reasons {}", e.getExceptions());
+ return emptyList();
+ }
+ }
+}
diff --git a/example/events/eventSqs.json b/example/events/eventSqs.json
new file mode 100644
index 000000000..37a29c4dd
--- /dev/null
+++ b/example/events/eventSqs.json
@@ -0,0 +1,36 @@
+{
+ "Records": [
+ {
+ "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb99",
+ "receiptHandle": "MessageReceiptHandle",
+ "body": "Hello from SQS!",
+ "attributes": {
+ "ApproximateReceiveCount": "1",
+ "SentTimestamp": "1523232000000",
+ "SenderId": "123456789012",
+ "ApproximateFirstReceiveTimestamp": "1523232000001"
+ },
+ "messageAttributes": {},
+ "md5OfBody": "7b270e59b47ff90a553787216d55d999",
+ "eventSource": "aws:sqs",
+ "eventSourceARN": "arn:aws:sqs:eu-west-1:123456789:powertools-example-TestSqsQueue-1JW5W8N9",
+ "awsRegion": "eu-west-1"
+ },
+ {
+ "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
+ "receiptHandle": "MessageReceiptHandle",
+ "body": "Hello from SQS!",
+ "attributes": {
+ "ApproximateReceiveCount": "1",
+ "SentTimestamp": "1523232000000",
+ "SenderId": "123456789012",
+ "ApproximateFirstReceiveTimestamp": "1523232000001"
+ },
+ "messageAttributes": {},
+ "md5OfBody": "7b270e59b47ff90a553787216d55d91d",
+ "eventSource": "aws:sqs",
+ "eventSourceARN": "arn:aws:sqs:eu-west-1:123456789:powertools-example-TestSqsQueue-1JW5W8N9",
+ "awsRegion": "eu-west-1"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/example/template.yaml b/example/template.yaml
index a7dd1b8be..9f279c2bf 100644
--- a/example/template.yaml
+++ b/example/template.yaml
@@ -125,6 +125,57 @@ Resources:
Value: aGVsbG8gd29ybGQ=
Description: Base64 SSM Parameter for lambda-powertools-java powertools-parameters module
+ TestSqsQueue:
+ Type: AWS::SQS::Queue
+
+ HelloWorldSqsEventFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: HelloWorldFunction
+ Handler: helloworld.AppSqsEvent::handleRequest
+ Runtime: java8
+ MemorySize: 512
+ Tracing: Active
+ Policies:
+ - Statement:
+ - Sid: AdditionalPermisssionForPowertoolsSQSUtils
+ Effect: Allow
+ Action:
+ - sqs:GetQueueUrl
+ - sqs:DeleteMessageBatch
+ Resource: !GetAtt TestSqsQueue.Arn
+ Events:
+ TestSQSEvent:
+ Type: SQS
+ Properties:
+ Queue: !GetAtt TestSqsQueue.Arn
+ BatchSize: 10
+
+ TestAnotherSqsQueue:
+ Type: AWS::SQS::Queue
+
+ HelloWorldSqsEventUtilFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: HelloWorldFunction
+ Handler: helloworld.AppSqsEventUtil::handleRequest
+ Runtime: java8
+ MemorySize: 512
+ Tracing: Active
+ Policies:
+ - Statement:
+ - Sid: AdditionalPermisssionForPowertoolsSQSUtils
+ Effect: Allow
+ Action:
+ - sqs:GetQueueUrl
+ - sqs:DeleteMessageBatch
+ Resource: !GetAtt TestAnotherSqsQueue.Arn
+ Events:
+ TestSQSEvent:
+ Type: SQS
+ Properties:
+ Queue: !GetAtt TestAnotherSqsQueue.Arn
+ BatchSize: 10
Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
diff --git a/powertools-sqs/pom.xml b/powertools-sqs/pom.xml
index 5ea80f5cc..52867d191 100644
--- a/powertools-sqs/pom.xml
+++ b/powertools-sqs/pom.xml
@@ -57,6 +57,10 @@
software.amazon.payloadoffloadingpayloadoffloading-common
+
+ software.amazon.awssdk
+ sqs
+ org.aspectj
diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/PowertoolsSqs.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/PowertoolsSqs.java
index 1a60bdc71..01ded6410 100644
--- a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/PowertoolsSqs.java
+++ b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/PowertoolsSqs.java
@@ -13,6 +13,8 @@
*/
package software.amazon.lambda.powertools.sqs;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -20,20 +22,24 @@
import com.amazonaws.services.lambda.runtime.events.SQSEvent;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
-import software.amazon.lambda.powertools.sqs.internal.SqsMessageAspect;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import software.amazon.awssdk.services.sqs.SqsClient;
+import software.amazon.lambda.powertools.sqs.internal.BatchContext;
+import software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect;
import software.amazon.payloadoffloading.PayloadS3Pointer;
import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
-import static software.amazon.lambda.powertools.sqs.internal.SqsMessageAspect.processMessages;
+import static software.amazon.lambda.powertools.sqs.internal.SqsLargeMessageAspect.processMessages;
/**
- * A class of helper functions to add additional functionality to LargeMessageHandler.
- *
- * {@see PowertoolsLogging}
+ * A class of helper functions to add additional functionality to {@link SQSEvent} processing.
*/
public final class PowertoolsSqs {
+ private static final Log LOG = LogFactory.getLog(PowertoolsSqs.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
+ private static SqsClient client = SqsClient.create();
private PowertoolsSqs() {
}
@@ -74,12 +80,188 @@ public static R enrichedMessageFromS3(final SQSEvent sqsEvent,
R returnValue = messageFunction.apply(sqsMessages);
if (deleteS3Payload) {
- s3Pointers.forEach(SqsMessageAspect::deleteMessage);
+ 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) {
+ PowertoolsSqs.client = client;
+ }
+
+ /**
+ * This utility method is used to processes each {@link SQSMessage} inside received {@link SQSEvent}
+ *
+ *
+ * Utility 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)}
+ * {@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 to utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress
+ * it, Refer {@link PowertoolsSqs#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.
+ */
+ public static List batchProcessor(final SQSEvent event,
+ final Class extends SqsMessageHandler> handler) {
+ return batchProcessor(event, false, handler);
+ }
+
+ /**
+ * This utility method is used to processes each {@link SQSMessage} inside received {@link SQSEvent}
+ *
+ *
+ * Utility 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)}
+ * {@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.
+ */
+ public static List batchProcessor(final SQSEvent event,
+ final boolean suppressException,
+ final Class extends SqsMessageHandler> handler) {
+
+ SqsMessageHandler handlerInstance = instantiatedHandler(handler);
+ return batchProcessor(event, suppressException, handlerInstance);
+ }
+
+ /**
+ * This utility method is used to processes each {@link SQSMessage} inside received {@link SQSEvent}
+ *
+ *
+ * Utility 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)}
+ * {@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 to utility to throw {@link SQSBatchProcessingException} in case of failures but rather suppress
+ * it, Refer {@link PowertoolsSqs#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.
+ */
+ public static List batchProcessor(final SQSEvent event,
+ final SqsMessageHandler handler) {
+ return batchProcessor(event, false, handler);
+ }
+
+ /**
+ * This utility method is used to processes each {@link SQSMessage} inside received {@link SQSEvent}
+ *
+ *
+ * Utility 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)}
+ * {@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.
+ */
+ public static List batchProcessor(final SQSEvent event,
+ final boolean suppressException,
+ final SqsMessageHandler handler) {
+ final List handlerReturn = new ArrayList<>();
+
+ BatchContext batchContext = new BatchContext(client);
+
+ for (SQSMessage message : event.getRecords()) {
+ try {
+ handlerReturn.add(handler.process(message));
+ batchContext.addSuccess(message);
+ } catch (Exception e) {
+ batchContext.addFailure(message, e);
+ }
+ }
+
+ batchContext.processSuccessAndHandleFailed(handlerReturn, suppressException);
+
+ return handlerReturn;
+ }
+
+ private static SqsMessageHandler instantiatedHandler(final Class extends SqsMessageHandler> handler) {
+
+ try {
+ if (null == handler.getDeclaringClass()) {
+ return handler.newInstance();
+ }
+
+ final Constructor extends SqsMessageHandler> constructor = handler.getDeclaredConstructor(handler.getDeclaringClass());
+ constructor.setAccessible(true);
+ return constructor.newInstance(handler.getDeclaringClass().newInstance());
+ } catch (Exception e) {
+ LOG.error("Failed creating handler instance", e);
+ throw new RuntimeException("Unexpected error occurred. Please raise issue at " +
+ "https://github.com/awslabs/aws-lambda-powertools-java/issues", e);
+ }
+ }
+
private static SQSMessage clonedMessage(final SQSMessage sqsMessage) {
try {
return objectMapper
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
new file mode 100644
index 000000000..38a9c943d
--- /dev/null
+++ b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SQSBatchProcessingException.java
@@ -0,0 +1,76 @@
+package software.amazon.lambda.powertools.sqs;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+
+import static com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
+import static java.util.stream.Collectors.joining;
+
+/**
+ *
+ * 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.
+ *
+ *
+ */
+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 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 returnValues;
+ }
+
+ /**
+ * Details of {@link SQSMessage} that failed in {@link SqsMessageHandler#process(SQSMessage)}
+ * @return List of failed messages
+ */
+ public List getFailures() {
+ return failures;
+ }
+
+ @Override
+ public void printStackTrace() {
+ for (Exception exception : exceptions) {
+ exception.printStackTrace();
+ }
+ }
+}
diff --git a/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsBatchProcessor.java b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsBatchProcessor.java
new file mode 100644
index 000000000..342765052
--- /dev/null
+++ b/powertools-sqs/src/main/java/software/amazon/lambda/powertools/sqs/SqsBatchProcessor.java
@@ -0,0 +1,62 @@
+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;
+
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+
+import static com.amazonaws.services.lambda.runtime.events.SQSEvent.*;
+
+/**
+ * {@link SqsBatchProcessor} 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 SqsBatchProcessor#suppressException()} to true. By default its value is false
+ *