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 { + + MetricsLogger metricsLogger = PowertoolsMetricsLogger.metricsLogger(); + + @Override + @PowertoolsMetrics(namespace = "ExampleApplication", service = "booking") + public Object handleRequest(Object input, Context context) { + ... + } +} +``` + +You can initialize Metrics anywhere in your code as many times as you need - It'll keep track of your aggregate metrics in memory. + +## Creating metrics + +You can create metrics using `putMetric`, and manually create dimensions for all your aggregate metrics using `add_dimension`. + +```java:title=app.py +public class PowertoolsMetricsEnabledHandler implements RequestHandler { + + MetricsLogger metricsLogger = PowertoolsMetricsLogger.metricsLogger(); + + @Override + @PowertoolsMetrics(namespace = "ExampleApplication", service = "booking") + public Object handleRequest(Object input, Context context) { + # highlight-start + metricsLogger.putDimensions(DimensionSet.of("environment", "prod")); + metricsLogger.putMetric("SuccessfulBooking", 1, Unit.COUNT); + # highlight-end + ... + } +} +``` + +The `Unit` enum facilitate finding a supported metric unit by CloudWatch. + +CloudWatch EMF supports a max of 100 metrics. Metrics utility will flush all metrics when adding the 100th metric while subsequent metrics will be aggregated into a new EMF object, for your convenience. + +## Creating a metric with a different dimension + +CloudWatch EMF uses the same dimensions across all your metrics. Use `single_metric` if you have a metric that should have different dimensions. + + + Generally, this would be an edge case since you pay for unique metric. Keep the following formula in mind: +

+ unique metric = (metric_name + dimension_name + dimension_value) +

+ +```java:title=Handler.java +withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> { + metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); +}); +``` + +## Adding metadata + +You can use `putMetadata` for advanced use cases, where you want to metadata as part of the serialized metrics object. + + + This will not be available during metrics visualization - Use dimensions for this purpose +
+ +```javv: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 + + ... +``` + +This will be available in CloudWatch Logs to ease operations on high cardinal data. + +The `@PowertoolsMetrics` annotation **validates**, **serializes**, and **flushes** all your metrics. During metrics validation, if no metrics are provided no exception will be raised. + +If metrics are provided, and any of the following criteria are not met, `ValidationException` exception will be raised: + +* Minimum of 1 dimension +* Maximum of 9 dimensions + +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) { + ... +``` + +## Capturing cold start metric + +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) { + ... +``` + +If it's a cold start invocation, this feature will: + +* Create a separate EMF blob solely containing a metric named `ColdStart` +* Add `FunctionName` and `Service` dimensions + +This has the advantage of keeping cold start metric separate from your application metrics. \ No newline at end of file diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js index e588bb5eb..503e8327a 100644 --- a/docs/gatsby-config.js +++ b/docs/gatsby-config.js @@ -24,7 +24,8 @@ module.exports = { ], 'Core utilities': [ 'core/logging', - 'core/tracing' + 'core/tracing', + 'core/metrics' ], 'Utilities': [ 'utilities/sqs_large_message_handling' diff --git a/example/HelloWorldFunction/pom.xml b/example/HelloWorldFunction/pom.xml index 361603e52..d739279d7 100644 --- a/example/HelloWorldFunction/pom.xml +++ b/example/HelloWorldFunction/pom.xml @@ -23,6 +23,11 @@ powertools-logging 0.2.0-beta + + software.amazon.lambda + powertools-metrics + 0.2.0-beta + com.amazonaws aws-lambda-java-core @@ -76,6 +81,10 @@ software.amazon.lambda powertools-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 47a86b3da..2769d457c 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,7 @@ powertools-logging powertools-tracing powertools-sqs + powertools-metrics @@ -67,6 +68,7 @@ 3.2.1 1.6 5.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.10 test + + org.mockito + mockito-inline + 3.5.10 + test + org.aspectj aspectjweaver 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 {