diff --git a/docs/FAQs.md b/docs/FAQs.md index f3fd2514d..75f699c91 100644 --- a/docs/FAQs.md +++ b/docs/FAQs.md @@ -3,16 +3,15 @@ title: FAQs description: Frequently Asked Questions --- - ## How can I use Powertools for AWS Lambda (Java) with Lombok? -Powertools uses `aspectj-maven-plugin` to compile-time weave (CTW) aspects into the project. In case you want to use `Lombok` or other compile-time preprocessor for your project, it is required to change `aspectj-maven-plugin` configuration to enable in-place weaving feature. Otherwise the plugin will ignore changes introduced by `Lombok` and will use `.java` files as a source. +Many utilities in this library use `aspectj-maven-plugin` to compile-time weave (CTW) aspects into the project. In case you want to use `Lombok` or other compile-time preprocessor for your project, it is required to change `aspectj-maven-plugin` configuration to enable in-place weaving feature. Otherwise the plugin will ignore changes introduced by `Lombok` and will use `.java` files as a source. To enable in-place weaving feature you need to use following `aspectj-maven-plugin` configuration: ```xml hl_lines="2-6" - true + true ${project.build.directory}/classes @@ -29,14 +28,14 @@ To enable in-place weaving feature you need to use following `aspectj-maven-plug ## How can I use Powertools for AWS Lambda (Java) with Kotlin projects? -Powertools uses `aspectj-maven-plugin` to compile-time weave (CTW) aspects into the project. When using it with Kotlin projects, it is required to `forceAjcCompile`. -No explicit configuration should be required for gradle projects. +Many utilities use `aspectj-maven-plugin` to compile-time weave (CTW) aspects into the project. When using it with Kotlin projects, it is required to `forceAjcCompile`. +No explicit configuration should be required for gradle projects. To enable `forceAjcCompile` you need to use following `aspectj-maven-plugin` configuration: ```xml hl_lines="2" - true + true ... @@ -49,17 +48,17 @@ To enable `forceAjcCompile` you need to use following `aspectj-maven-plugin` con ## How can I use Powertools for AWS Lambda (Java) with the AWS CRT HTTP Client? -Powertools uses the `url-connection-client` as the default HTTP client. The `url-connection-client` is a lightweight HTTP client, which keeps the impact on Lambda cold starts to a minimum. -With the [announcement](https://aws.amazon.com/blogs/developer/announcing-availability-of-the-aws-crt-http-client-in-the-aws-sdk-for-java-2-x/) of the `aws-crt-client` a new HTTP client has been released, which offers faster SDK startup time and smaller memory footprint. +Utilities relying on AWS SDK clients use the `url-connection-client` as the default HTTP client. The `url-connection-client` is a lightweight HTTP client, which keeps the impact on Lambda cold starts to a minimum. +With the [announcement](https://aws.amazon.com/blogs/developer/announcing-availability-of-the-aws-crt-http-client-in-the-aws-sdk-for-java-2-x/) of the `aws-crt-client` a new HTTP client has been released, which offers faster SDK startup time and smaller memory footprint. -Unfortunately, replacing the `url-connection-client` dependency with the `aws-crt-client` will not immediately improve the lambda cold start performance and memory footprint, -as the default version of the dependency contains native system libraries for all supported runtimes and architectures (Linux, MacOS, Windows, AMD64, ARM64, etc). This makes the CRT client portable, without the user having to consider _where_ their code will run, but comes at the cost of JAR size. +Unfortunately, replacing the `url-connection-client` dependency with the `aws-crt-client` will not immediately improve the lambda cold start performance and memory footprint, +as the default version of the dependency contains native system libraries for all supported runtimes and architectures (Linux, MacOS, Windows, AMD64, ARM64, etc). This makes the CRT client portable, without the user having to consider _where_ their code will run, but comes at the cost of JAR size. ### Configuring dependencies -Using the `aws-crt-client` in your project requires the exclusion of the `url-connection-client` transitive dependency from the powertools dependency. +Using the `aws-crt-client` in your project requires the exclusion of the `url-connection-client` transitive dependency from the `powertools-*` dependency. -```xml +```xml software.amazon.lambda powertools-parameters @@ -72,8 +71,9 @@ Using the `aws-crt-client` in your project requires the exclusion of the `url-co ``` -Next, add the `aws-crt-client` and exclude the "generic" `aws-crt` dependency (contains all runtime libraries). -Instead, set a specific classifier of the `aws-crt` to use the one for your target runtime: either `linux-x86_64` for a Lambda configured for x86 or `linux-aarch_64` for Lambda using arm64. + +Next, add the `aws-crt-client` and exclude the "generic" `aws-crt` dependency (contains all runtime libraries). +Instead, set a specific classifier of the `aws-crt` to use the one for your target runtime: either `linux-x86_64` for a Lambda configured for x86 or `linux-aarch_64` for Lambda using arm64. !!! note "You will need to add a separate maven profile to build and debug locally when your development environment does not share the target architecture you are using in Lambda." By specifying the specific target runtime, we prevent other target runtimes from being included in the jar file, resulting in a smaller Lambda package and improved cold start times. @@ -102,10 +102,11 @@ By specifying the specific target runtime, we prevent other target runtimes from ``` ### Explicitly set the AWS CRT HTTP Client -After configuring the dependencies, it's required to explicitly specify the AWS SDK HTTP client. -Depending on the Powertools module, there is a different way to configure the SDK client. -The following example shows how to use the Lambda Powertools Parameters module while leveraging the AWS CRT Client. +After configuring the dependencies, it's required to explicitly specify the AWS SDK HTTP client. +Depending on the utility you are using, there is a different way to configure the SDK client. + +The following example shows how to use the Parameters module while leveraging the AWS CRT Client. ```java hl_lines="16 23-24" import static software.amazon.lambda.powertools.parameters.transform.Transformer.base64; @@ -141,12 +142,12 @@ public class RequestHandlerWithParams implements RequestHandler } ``` -The `aws-crt-client` was considered for adoption as the default HTTP client in Lambda Powertools for Java as mentioned in [Move SDK http client to CRT](https://github.com/aws-powertools/powertools-lambda-java/issues/1092), +The `aws-crt-client` was considered for adoption as the default HTTP client in Powertools for AWS Lambda (Java) as mentioned in [Move SDK http client to CRT](https://github.com/aws-powertools/powertools-lambda-java/issues/1092), but due to the impact on the developer experience it was decided to stick with the `url-connection-client`. ## How can I use Powertools for AWS Lambda (Java) with GraalVM? -Powertools core utilities, i.e. [logging](./core/logging.md), [metrics](./core/metrics.md) and [tracing](./core/tracing.md), include the [GraalVM Reachability Metadata (GRM)](https://www.graalvm.org/latest/reference-manual/native-image/metadata/) in the `META-INF` directories of the respective JARs. You can find a working example of Serverless Application Model (SAM) based application in the [examples](../examples/powertools-examples-core-utilities/sam-graalvm/README.md) directory. +Core utilities, i.e. [logging](./core/logging.md), [metrics](./core/metrics.md) and [tracing](./core/tracing.md), include the [GraalVM Reachability Metadata (GRM)](https://www.graalvm.org/latest/reference-manual/native-image/metadata/) in the `META-INF` directories of the respective JARs. You can find a working example of Serverless Application Model (SAM) based application in the [examples](../examples/powertools-examples-core-utilities/sam-graalvm/README.md) directory. Below, you find typical steps you need to follow in a Maven based Java project: @@ -157,6 +158,7 @@ export JAVA_HOME= ``` ### Use log4j `>2.24.0` + Log4j version `2.24.0` adds [support for GraalVM](https://github.com/apache/logging-log4j2/issues/1539#issuecomment-2106766878). Depending on your project's dependency hierarchy, older version of log4j might be included in the final dependency graph. Make sure version `>2.24.0` of these dependencies are used by your Maven project: ```xml @@ -245,10 +247,11 @@ Create a Docker image using a `Dockerfile` like [this](../examples/powertools-ex docker build --platform linux/amd64 . -t your-org/your-app-graalvm-builder ``` -Create the native image of you Lambda function using the Docker command below. +Create the native image of you Lambda function using the Docker command below. ```shell docker run --platform linux/amd64 -it -v `pwd`:`pwd` -w `pwd` -v ~/.m2:/root/.m2 your-org/your-app-graalvm-builder mvn clean -Pnative-image package ``` + The native image is created in the `target/` directory. diff --git a/docs/core/logging.md b/docs/core/logging.md index 1160f62ff..2d9e57dda 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -13,6 +13,7 @@ Logging provides an opinionated logger with output structured as JSON. * Optionally logs Lambda response * Optionally supports log sampling by including a configurable percentage of DEBUG logs in logging output * Allows additional keys to be appended to the structured log at any point in time +* GraalVM support ## Getting started @@ -219,7 +220,7 @@ You can leverage the standard configuration files (_log4j2.xml_ or _logback.xml_ === "log4j2.xml" With log4j2, we leverage the [`JsonTemplateLayout`](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html){target="_blank"} - to provide structured logging. A default template is provided in powertools ([_LambdaJsonLayout.json_](https://github.com/aws-powertools/powertools-lambda-java/tree/v2/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json){target="_blank"}): + to provide structured logging. A default template is provided in powertools ([_LambdaJsonLayout.json_](https://github.com/aws-powertools/powertools-lambda-java/blob/4444b4bce8eb1cc19880d1c1ef07188d97de9126/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json){target="_blank"}): ```xml hl_lines="5" @@ -277,7 +278,7 @@ If the level is set to any other value, we set it to the default value (`INFO`). With [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced){target="_blank"}, you can enforce a minimum log level that Lambda will accept from your application code. -When enabled, you should keep Powertools and ALC log level in sync to avoid data loss. +When enabled, you should keep your own log level and ALC log level in sync to avoid data loss. Here's a sequence diagram to demonstrate how ALC will drop both `INFO` and `DEBUG` logs emitted from `Logger`, when ALC log level is stricter than `Logger`. @@ -308,7 +309,7 @@ We prioritise log level settings in this order: 2. `POWERTOOLS_LOG_LEVEL` environment variable 3. level defined in the `log4j2.xml` or `logback.xml` files -If you set Powertools level lower than ALC, we will emit a warning informing you that your messages will be discarded by Lambda. +If you set `POWERTOOLS_LOG_LEVEL` lower than ALC, we will emit a warning informing you that your messages will be discarded by Lambda. > **NOTE** > @@ -738,7 +739,7 @@ When debugging in non-production environments, you can instruct the `@Logging` a ``` ???+ note - If you use this on a RequestStreamHandler, Powertools must duplicate input streams in order to log them. + If you use this on a RequestStreamHandler, the SDK must duplicate input streams in order to log them. ## Logging handler response @@ -973,7 +974,7 @@ You can also customize how [exceptions are logged](https://logging.apache.org/lo See the [JSON Layout template documentation](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html){target="_blank"} for more details. ### Logback configuration -Logback configuration is done in _logback.xml_ and the Powertools [`LambdaJsonEncoder`](): +Logback configuration is done in _logback.xml_ and the `LambdaJsonEncoder`: ```xml diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 6083d935a..35d08b140 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -3,23 +3,29 @@ title: Metrics description: Core utility --- -Metrics creates custom metrics asynchronously by logging metrics to standard output following Amazon CloudWatch Embedded Metric Format (EMF). +Metrics creates custom metrics asynchronously by logging metrics to standard output following [Amazon CloudWatch Embedded Metric Format (EMF)](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html). -These metrics can be visualized through [Amazon CloudWatch Console](https://console.aws.amazon.com/cloudwatch/). +These metrics can be visualized through [Amazon CloudWatch Console](https://aws.amazon.com/cloudwatch/). -**Key features** +## Key features -* Aggregate up to 100 metrics using a single CloudWatch EMF object (large JSON blob). -* Validate against common metric definitions mistakes (metric unit, values, max dimensions, max metrics, etc). -* Metrics are created asynchronously by the CloudWatch service, no custom stacks needed. -* Context manager to create a one off metric with a different dimension. +- Aggregate up to 100 metrics using a single [CloudWatch EMF](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html){target="\_blank"} object (large JSON blob) +- Validating your metrics against common metric definitions mistakes (for example, metric unit, values, max dimensions, max metrics) +- Metrics are created asynchronously by the CloudWatch service. You do not need any custom stacks, and there is no impact to Lambda function latency +- Support for creating one off metrics with different dimensions +- GraalVM support ## Terminologies -If you're new to Amazon CloudWatch, there are two terminologies you must be aware of before using this utility: +If you're new to Amazon CloudWatch, there are some terminologies you must be aware of before using this utility: -* **Namespace**. It's the highest level container that will group multiple metrics from multiple services for a given application, for example `ServerlessEcommerce`. -* **Dimensions**. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example `ColdStart` metric by Payment `service`. +- **Namespace**. It's the highest level container that will group multiple metrics from multiple services for a given application, for example `ServerlessAirline`. +- **Dimensions**. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example `ColdStart` metric by `service`. +- **Metric**. It's the name of the metric, for example: `SuccessfulBooking` or `UpdatedBooking`. +- **Unit**. It's a value representing the unit of measure for the corresponding metric, for example: `Count` or `Seconds`. +- **Resolution**. It's a value representing the storage resolution for the corresponding metric. Metrics can be either `Standard` or `High` resolution. Read more about CloudWatch Periods [here](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition). + +Visit the AWS documentation for a complete explanation for [Amazon CloudWatch concepts](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html).
@@ -88,30 +94,44 @@ If you're new to Amazon CloudWatch, there are two terminologies you must be awar id 'java' id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0' } - + repositories { mavenCentral() } - + dependencies { aspect 'software.amazon.lambda:powertools-metrics:{{ powertools.version }}' } - + sourceCompatibility = 11 targetCompatibility = 11 ``` ## Getting started -Metric has two global settings that will be used across all metrics emitted: +Metrics has three global settings that will be used across all metrics emitted. Use your application or main service as the metric namespace to easily group all metrics: -| Setting | Description | Environment variable | Constructor parameter | -|----------------------|---------------------------------------------------------------------------------|--------------------------------|-----------------------| -| **Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` | -| **Service** | Optionally, sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `service` | +| Setting | Description | Environment variable | Decorator parameter | +| -------------------- | ------------------------------------------------------------------------------- | ---------------------------------- | ------------------- | +| **Metric namespace** | Logical container where all metrics will be placed e.g. `ServerlessAirline` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` | +| **Service** | Optionally, sets **service** metric dimension across all metrics e.g. `payment` | `POWERTOOLS_SERVICE_NAME` | `service` | +| **Function name** | Function name used as dimension for the cold start metric | `POWERTOOLS_METRICS_FUNCTION_NAME` | `functionName` | +| **Disable Metrics** | Optionally, disables all metrics flushing | `POWERTOOLS_METRICS_DISABLED` | N/A | !!! tip "Use your application or main service as the metric namespace to easily group all metrics" +!!! info "`POWERTOOLS_METRICS_DISABLED` will not disable default metrics created by AWS services." + +### Order of Precedence of `Metrics` configuration + +The `Metrics` Singleton can be configured by three different interfaces. The following order of precedence applies: + +1. `@FlushMetrics` annotation +2. `MetricsBuilder` using Builder pattern (see [Advanced section](#usage-without-metrics-annotation)) +3. Environment variables (recommended) + +For most use-cases, we recommend using Environment variables and only overwrite settings in code where needed using either the `@FlushMetrics` annotation or `MetricsBuilder` if the annotation cannot be used. + === "template.yaml" ```yaml hl_lines="9 10" @@ -129,101 +149,159 @@ Metric has two global settings that will be used across all metrics emitted: === "MetricsEnabledHandler.java" - ```java hl_lines="8" - import software.amazon.lambda.powertools.metrics.Metrics; - + ```java hl_lines="9" + import software.amazon.lambda.powertools.metrics.FlushMetrics; + import software.amazon.lambda.powertools.metrics.MetricsFactory; + public class MetricsEnabledHandler implements RequestHandler { - - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); - + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + @Override - @Metrics(namespace = "ExampleApplication", service = "booking") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { // ... } } ``` -You can initialize Metrics anywhere in your code as many times as you need - It'll keep track of your aggregate metrics in memory. +`Metrics` is implemented as a Singleton to keep track of your aggregate metrics in memory and make them accessible anywhere in your code. To guarantee that metrics are flushed properly the `@FlushMetrics` annotation must be added on the lambda handler. + +!!!info "You can use the Metrics utility without the `@FlushMetrics` annotation and flush manually. Read more in the [advanced section below](#usage-without-metrics-annotation)." ## Creating metrics -You can create metrics using `putMetric`, and manually create dimensions for all your aggregate metrics using `putDimensions`. +You can create metrics using `addMetric`, and manually create dimensions for all your aggregate metrics using `addDimension`. Anywhere in your code, you can access the current `Metrics` Singleton using the `MetricsFactory`. === "MetricsEnabledHandler.java" - ```java hl_lines="11 12" + ```java hl_lines="13" + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; - import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; + import software.amazon.lambda.powertools.metrics.MetricsFactory; + import software.amazon.lambda.powertools.metrics.model.MetricUnit; public class MetricsEnabledHandler implements RequestHandler { - - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); - + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + @Override - @Metrics(namespace = "ExampleApplication", service = "booking") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - metricsLogger.putDimensions(DimensionSet.of("environment", "prod")); - metricsLogger.putMetric("SuccessfulBooking", 1, Unit.COUNT); + metrics.addDimension("environment", "prod"); + metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); // ... } } ``` -!!! tip "The `Unit` enum facilitate finding a supported metric unit by CloudWatch." - -!!! note "Metrics overflow" - CloudWatch EMF supports a max of 100 metrics. Metrics utility will flush all metrics when adding the 100th metric while subsequent metrics will be aggregated into a new EMF object, for your convenience. +!!! tip "The `MetricUnit` enum facilitates finding a supported metric unit by CloudWatch." + +!!! note "Metrics dimensions" + CloudWatch EMF supports a max of 9 dimensions per metric. The Metrics utility will validate this limit when adding dimensions. + ### Adding high-resolution metrics You can create [high-resolution metrics](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/publishingMetrics.html#high-resolution-metrics) -passing a `storageResolution` to the `putMetric` method: +passing a `#!java MetricResolution.HIGH` to the `addMetric` method. If nothing is passed `#!java MetricResolution.STANDARD` will be used. === "HigResMetricsHandler.java" ```java hl_lines="3 13" + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; - import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; - import software.amazon.cloudwatchlogs.emf.model.StorageResolution; + import software.amazon.lambda.powertools.metrics.model.MetricResolution; public class MetricsEnabledHandler implements RequestHandler { - - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); - + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + @Override - @Metrics(namespace = "ExampleApplication", service = "booking") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { // ... - metricsLogger.putMetric("SuccessfulBooking", 1, Unit.COUNT, StorageResolution.HIGH); + metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT, MetricResolution.HIGH); } } ``` + !!! info "When is it useful?" High-resolution metrics are data with a granularity of one second and are very useful in several situations such as telemetry, time series, real-time incident management, and others. + + +### Adding dimensions + +You can add dimensions to your metrics using the `addDimension` method. You can either pass key-value pairs or you can create higher cardinality dimensions using `DimensionSet`. + +=== "KeyValueDimensionHandler.java" + + ```java hl_lines="3 13" + import software.amazon.lambda.powertools.metrics.FlushMetrics; + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.model.MetricResolution; + + public class MetricsEnabledHandler implements RequestHandler { + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + + @Override + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") + public Object handleRequest(Object input, Context context) { + metrics.addDimension("Dimension", "Value"); + metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); + } + } + ``` + +=== "HighCardinalityDimensionHandler.java" + + ```java hl_lines="4 13-14" + import software.amazon.lambda.powertools.metrics.FlushMetrics; + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.model.MetricResolution; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + + public class MetricsEnabledHandler implements RequestHandler { + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + + @Override + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") + public Object handleRequest(Object input, Context context) { + // You can add up to 30 dimensions in a single DimensionSet + metrics.addDimension(DimensionSet.of("Dimension1", "Value1", "Dimension2", "Value2")); + metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); + } + } + ``` ### Flushing metrics -The `@Metrics` annotation **validates**, **serializes**, and **flushes** all your metrics. During metrics validation, -if no metrics are provided no exception will be raised. If metrics are provided, and any of the following criteria are -not met, `ValidationException` exception will be raised. +The `@FlushMetrics` annotation **validates**, **serializes**, and **flushes** all your metrics. During metrics validation, +if no metrics are provided no exception will be raised. If metrics are provided, and any of the following criteria are +not met, `IllegalStateException` or `IllegalArgumentException` exceptions will be raised. + !!! tip "Metric validation" - * Maximum of 9 dimensions + - Maximum of 30 dimensions (`Service` default dimension counts as a regular dimension) + - Dimension keys and values cannot be null or empty + - Metric values must be valid numbers + -If you want to ensure that at least one metric is emitted, you can pass `raiseOnEmptyMetrics = true` to the **@Metrics** annotation: +If you want to ensure that at least one metric is emitted, you can pass `raiseOnEmptyMetrics = true` to the `@FlushMetrics` annotation: === "MetricsRaiseOnEmpty.java" ```java hl_lines="6" - import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; public class MetricsRaiseOnEmpty implements RequestHandler { @Override - @Metrics(raiseOnEmptyMetrics = true) + @FlushMetrics(raiseOnEmptyMetrics = true) public Object handleRequest(Object input, Context context) { ... } @@ -232,17 +310,17 @@ If you want to ensure that at least one metric is emitted, you can pass `raiseOn ## Capturing cold start metric -You can capture cold start metrics automatically with `@Metrics` via the `captureColdStart` variable. +You can capture cold start metrics automatically with `@FlushMetrics` via the `captureColdStart` variable. === "MetricsColdStart.java" ```java hl_lines="6" - import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; public class MetricsColdStart implements RequestHandler { @Override - @Metrics(captureColdStart = true) + @FlushMetrics(captureColdStart = true) public Object handleRequest(Object input, Context context) { ... } @@ -251,33 +329,77 @@ You can capture cold start metrics automatically with `@Metrics` via the `captur If it's a cold start invocation, this feature will: -* Create a separate EMF blob solely containing a metric named `ColdStart` -* Add `FunctionName` and `Service` dimensions +- Create a separate EMF blob solely containing a metric named `ColdStart` +- Add `FunctionName` and `Service` dimensions This has the advantage of keeping cold start metric separate from your application metrics. +You can also specify a custom function name to be used in the cold start metric: + +=== "MetricsColdStartCustomFunction.java" + + ```java hl_lines="6" + import software.amazon.lambda.powertools.metrics.FlushMetrics; + + public class MetricsColdStartCustomFunction implements RequestHandler { + + @Override + @FlushMetrics(captureColdStart = true, functionName = "CustomFunction") + public Object handleRequest(Object input, Context context) { + ... + } + } + ``` + + +!!!tip "You can overwrite the default `Service` and `FunctionName` dimensions of the cold start metric" + Set `#!java @FlushMetrics(captureColdStart = false)` and use the `captureColdStartMetric` method manually: + + ```java hl_lines="6 8" + public class MetricsColdStartCustomFunction implements RequestHandler { + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + + @Override + @FlushMetrics(captureColdStart = false) + public Object handleRequest(Object input, Context context) { + metrics.captureColdStartMetric(context, DimensionSet.of("CustomDimension", "CustomValue")); + ... + } + } + ``` + + ## Advanced -## Adding metadata +### Adding metadata -You can use `putMetadata` for advanced use cases, where you want to metadata as part of the serialized metrics object. +You can use `addMetadata` for advanced use cases, where you want to add metadata as part of the serialized metrics object. + !!! info - **This will not be available during metrics visualization, use `dimensions` for this purpose.** + This will not be available during metrics visualization, use Dimensions for this purpose. + +!!! info + Adding metadata with a key that is the same as an existing metric will be ignored + === "App.java" - ```java hl_lines="8 9" + ```java hl_lines="13" + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; - import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; + import software.amazon.lambda.powertools.metrics.MetricsFactory; public class App implements RequestHandler { + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + @Override - @Metrics(namespace = "ServerlessAirline", service = "payment") + @FlushMetrics(namespace = "ServerlessAirline", service = "booking-service") public Object handleRequest(Object input, Context context) { - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); - metricsLogger().putMetadata("booking_id", "1234567890"); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + metrics.addMetadata("booking_id", "1234567890"); // Needs to be added BEFORE flushing ... } } @@ -285,79 +407,225 @@ You can use `putMetadata` for advanced use cases, where you want to metadata as This will be available in CloudWatch Logs to ease operations on high cardinal data. -## Overriding default dimension set +### Setting default dimensions + +By default, all metrics emitted via module captures `Service` as one of the default dimensions. This is either specified via `POWERTOOLS_SERVICE_NAME` environment variable or via `service` attribute on `Metrics` annotation. -By default, all metrics emitted via module captures `Service` as one of the default dimension. This is either specified via -`POWERTOOLS_SERVICE_NAME` environment variable or via `service` attribute on `Metrics` annotation. If you wish to override the default -Dimension, it can be done via `#!java MetricsUtils.defaultDimensions()`. +If you wish to set custom default dimensions, it can be done via `#!java metrics.setDefaultDimensions()`. You can also use the `MetricsBuilder` instead of the `MetricsFactory` to configure **and** retrieve the `Metrics` Singleton at the same time (see `MetricsBuilder.java` tab). === "App.java" - ```java hl_lines="8 9 10" + ```java hl_lines="13" + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; - import static software.amazon.lambda.powertools.metrics.MetricsUtils; - + import software.amazon.lambda.powertools.metrics.MetricsFactory; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + public class App implements RequestHandler { - - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); - - static { - MetricsUtils.defaultDimensions(DimensionSet.of("CustomDimension", "booking")); + + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + + @Override + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") + public Object handleRequest(Object input, Context context) { + metrics.setDefaultDimensions(DimensionSet.of("CustomDimension", "booking", "Environment", "prod")); + ... } - + } + ``` + +=== "MetricsBuilder.java" + + ```java hl_lines="8-10" + import software.amazon.lambda.powertools.metrics.FlushMetrics; + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.MetricsFactory; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + + public class App implements RequestHandler { + + private static final Metrics metrics = MetricsBuilder.builder() + .withDefaultDimensions(DimensionSet.of("CustomDimension", "booking", "Environment", "prod")) + .build(); + @Override - @Metrics(namespace = "ExampleApplication", service = "booking") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); ... - MetricsUtils.withSingleMetric("Metric2", 1, Unit.COUNT, log -> {}); } } ``` -## Creating a metric with a different dimension + +!!!note + Overwriting the default dimensions will also overwrite the default `Service` dimension. If you wish to keep `Service` in your default dimensions, you need to add it manually. + -CloudWatch EMF uses the same dimensions across all your metrics. Use `withSingleMetric` if you have a metric that should have different dimensions. +### Creating a single metric with different configuration -!!! info - Generally, this would be an edge case since you [pay for unique metric](https://aws.amazon.com/cloudwatch/pricing/). Keep the following formula in mind: - **unique metric = (metric_name + dimension_name + dimension_value)** +You can create a single metric with its own namespace and dimensions using `flushSingleMetric`: === "App.java" - ```java hl_lines="7 8 9" - import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; + ```java hl_lines="12-18" + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.MetricsFactory; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + import software.amazon.lambda.powertools.metrics.model.MetricUnit; public class App implements RequestHandler { + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Override + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - }); + metrics.flushSingleMetric( + "CustomMetric", + 1, + MetricUnit.COUNT, + "CustomNamespace", + DimensionSet.of("CustomDimension", "value") // Dimensions are optional + ); } } ``` -## Creating metrics with different configurations + +!!! info + Generally, this would be an edge case since you [pay for unique metric](https://aws.amazon.com/cloudwatch/pricing). Keep the following formula in mind: -Use `withMetricsLogger` if you have one or more metrics that should have different configurations e.g. dimensions or namespace. + **unique metric = (metric_name + dimension_name + dimension_value)** + + +### Usage without `@FlushMetrics` annotation + +The `Metrics` Singleton provides all configuration options via `MetricsBuilder` in addition to the `@FlushMetrics` annotation. This can be useful if work in an environment or framework that does not leverage the vanilla Lambda `handleRequest` method. + +!!!info "The environment variables for Service and Namespace configuration still apply but can be overwritten with `MetricsBuilder` if needed." + +The following example shows how to configure a custom `Metrics` Singleton using the Builder pattern. Note that it is necessary to manually flush metrics now. === "App.java" - ```java hl_lines="7 8 9 10 11 12 13" - import static software.amazon.lambda.powertools.metrics.MetricsUtils.withMetricsLogger; + ```java hl_lines="7-12 19 23" + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.MetricsBuilder; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + import software.amazon.lambda.powertools.metrics.model.MetricUnit; public class App implements RequestHandler { + // Create and configure a Metrics singleton without annotation + private static final Metrics customMetrics = MetricsBuilder.builder() + .withNamespace("ServerlessAirline") + .withRaiseOnEmptyMetrics(true) + .withService("payment") + .build(); @Override public Object handleRequest(Object input, Context context) { - withMetricsLogger(logger -> { - // override default dimensions - logger.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - // add metrics - logger.putMetric("CustomMetrics1", 1, Unit.COUNT); - logger.putMetric("CustomMetrics2", 5, Unit.COUNT); - }); + // You can manually capture the cold start metric + // Lambda context is an optional argument if not available in your environment + // Dimensions are also optional. + customMetrics.captureColdStartMetric(context, DimensionSet.of("FunctionName", "MyFunction", "Service", "payment")); + + // Add metrics to the custom metrics singleton + customMetrics.addMetric("CustomMetric", 1, MetricUnit.COUNT); + customMetrics.flush(); } } ``` + +## Testing your code + +### Suppressing metrics output + +If you would like to suppress metrics output during your unit tests, you can use the `POWERTOOLS_DISABLE_METRICS` environment variable. For example, using Maven you can set in your build plugins: + +```xml + + org.apache.maven.plugins + maven-surefire-plugin + + + true + + + +``` + +### Asserting EMF output + +When unit testing your code, you can run assertions against the output generated by the `Metrics` Singleton. For the `EmfMetricsLogger`, you can assert the generated JSON blob following the [CloudWatch EMF specification](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format.html) against your expected output. + +Consider the following example where we redirect the standard output to a custom `PrintStream`. We use the Jackson library to parse the EMF output into a `JsonNode` and run assertions against that. + +```java hl_lines="23 28 33 50-55" +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.metrics.testutils.TestContext; + +class MetricsTestExample { + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @AfterEach + void tearDown() { + System.setOut(standardOut); + } + + @Test + void shouldCaptureMetricsFromAnnotatedHandler() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("test-metric")).isTrue(); + assertThat(rootNode.get("test-metric").asDouble()).isEqualTo(100.0); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("CustomNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("CustomService"); + } + + static class HandlerWithMetricsAnnotation implements RequestHandler, String> { + @Override + @FlushMetrics(namespace = "CustomNamespace", service = "CustomService") + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } +} +``` diff --git a/docs/core/tracing.md b/docs/core/tracing.md index ea3174fba..883f8db86 100644 --- a/docs/core/tracing.md +++ b/docs/core/tracing.md @@ -3,7 +3,7 @@ title: Tracing description: Core utility --- -Powertools tracing is an opinionated thin wrapper for [AWS X-Ray Java SDK](https://github.com/aws/aws-xray-sdk-java/) +The Tracing utility is an opinionated thin wrapper for [AWS X-Ray Java SDK](https://github.com/aws/aws-xray-sdk-java/) a provides functionality to reduce the overhead of performing common tracing tasks. ![Tracing showcase](../media/tracing_utility_showcase.png) @@ -14,6 +14,7 @@ a provides functionality to reduce the overhead of performing common tracing tas * Helper methods to improve the developer experience of creating new X-Ray subsegments. * Better developer experience when developing with multiple threads. * Auto patch supported modules by AWS X-Ray + * GraalVM support ## Install diff --git a/docs/index.md b/docs/index.md index ef30b4197..43d1ea03b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -236,12 +236,13 @@ Use the following [dependency matrix](https://github.com/eclipse-aspectj/aspectj **Explicit parameters take precedence over environment variables.** | Environment variable | Description | Utility | -|----------------------------------------|----------------------------------------------------------------------------------------|---------------------------| +| -------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------- | | **POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics dimension and structured logging | All | | **POWERTOOLS_METRICS_NAMESPACE** | Sets namespace used for metrics | [Metrics](./core/metrics) | +| **POWERTOOLS_METRICS_FUNCTION_NAME** | Function name used as dimension for the cold start metric | [Metrics](./core/metrics) | +| **POWERTOOLS_METRICS_DISABLED** | Disables all flushing of metrics | [Metrics](./core/metrics) | | **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logging](./core/logging) | | **POWERTOOLS_LOG_LEVEL** | Sets logging level | [Logging](./core/logging) | | **POWERTOOLS_LOGGER_LOG_EVENT** | Enables/Disables whether to log the incoming event when using the aspect | [Logging](./core/logging) | | **POWERTOOLS_TRACER_CAPTURE_RESPONSE** | Enables/Disables tracing mode to capture method response | [Tracing](./core/tracing) | | **POWERTOOLS_TRACER_CAPTURE_ERROR** | Enables/Disables tracing mode to capture method error | [Tracing](./core/tracing) | - diff --git a/docs/upgrade.md b/docs/upgrade.md index 11be3dc5f..d2b1af096 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -112,7 +112,7 @@ This step is only required if you are using log4j2 as your logging implementatio **3. Migrate all logging specific calls to SLF4J native primitives (recommended)** -The new logging utility is designed to integrate seamlessly with Java SLF4J to allow customers adopt Powertools Logging without large code refactorings. This improvement requires the migration of non-native SLF4J primitives from the v1 Logging utility. +The new logging utility is designed to integrate seamlessly with Java SLF4J to allow customers adopt the Logging utility without large code refactorings. This improvement requires the migration of non-native SLF4J primitives from the v1 Logging utility. !!! info "While we recommend using SLF4J as a logging implementation independent facade, you can still use the log4j2 and logback interfaces directly." diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index e5fb11800..beb460aa6 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -14,6 +14,7 @@ or [AWS AppConfig](https://aws.amazon.com/systems-manager/features/appconfig/). * Retrieve one or multiple parameters from an underlying provider in a standard way * Cache parameter values for a given amount of time (defaults to 5 seconds) * Transform parameter values from JSON or base 64 encoded strings +* GraalVM support ## Install In order to provide lightweight dependencies, each parameters module is available as its own diff --git a/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/App.java index 5aa268ffe..bb21f84d3 100644 --- a/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/App.java @@ -14,8 +14,6 @@ package helloworld; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; import com.amazonaws.services.lambda.runtime.Context; @@ -32,10 +30,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.tracing.TracingUtils; @@ -45,22 +45,23 @@ */ public class App implements RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); + @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { Map headers = new HashMap<>(); headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> - { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); - }); + DimensionSet dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); @@ -72,8 +73,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv TracingUtils.putAnnotation("Test", "New"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - TracingUtils.withSubsegment("loggingResponse", subsegment -> - { + TracingUtils.withSubsegment("loggingResponse", subsegment -> { String sampled = "log something out"; log.info(sampled); log.info(output); diff --git a/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/AppStream.java index 94806cc38..8bc57b201 100644 --- a/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/AppStream.java +++ b/examples/powertools-examples-core-utilities/cdk/app/src/main/java/helloworld/AppStream.java @@ -26,7 +26,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import java.io.InputStreamReader; import java.io.BufferedReader; @@ -40,7 +40,7 @@ public class AppStream implements RequestStreamHandler { @Override @Logging(logEvent = true) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) // RequestStreamHandler can be used instead of RequestHandler for cases when you'd like to deserialize request body or serialize response body yourself, instead of allowing that to happen automatically // Note that you still need to return a proper JSON for API Gateway to handle // See Lambda Response format for examples: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html diff --git a/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java index b1a701b8f..39ed6f8d8 100644 --- a/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java @@ -15,8 +15,6 @@ package helloworld; import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; import com.amazonaws.services.lambda.runtime.Context; @@ -33,10 +31,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.tracing.TracingUtils; @@ -46,23 +46,24 @@ */ public class App implements RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { Map headers = new HashMap<>(); headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> - { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); - }); + DimensionSet dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1" + ); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); diff --git a/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/AppStream.java index 94806cc38..8bc57b201 100644 --- a/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/AppStream.java +++ b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/AppStream.java @@ -26,7 +26,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import java.io.InputStreamReader; import java.io.BufferedReader; @@ -40,7 +40,7 @@ public class AppStream implements RequestStreamHandler { @Override @Logging(logEvent = true) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) // RequestStreamHandler can be used instead of RequestHandler for cases when you'd like to deserialize request body or serialize response body yourself, instead of allowing that to happen automatically // Note that you still need to return a proper JSON for API Gateway to handle // See Lambda Response format for examples: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html diff --git a/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt b/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt index 8e8857079..b05e0d055 100644 --- a/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt +++ b/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt @@ -20,13 +20,13 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent import com.amazonaws.xray.entities.Subsegment import org.slf4j.LoggerFactory import org.slf4j.MDC -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.logging.Logging import software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry import software.amazon.lambda.powertools.metrics.Metrics -import software.amazon.lambda.powertools.metrics.MetricsUtils +import software.amazon.lambda.powertools.metrics.Metrics +import software.amazon.lambda.powertools.metrics.MetricsFactory +import software.amazon.lambda.powertools.metrics.model.DimensionSet +import software.amazon.lambda.powertools.metrics.model.MetricUnit import software.amazon.lambda.powertools.tracing.CaptureMode import software.amazon.lambda.powertools.tracing.Tracing import software.amazon.lambda.powertools.tracing.TracingUtils @@ -39,16 +39,23 @@ import java.net.URL */ class App : RequestHandler { + private val log = LoggerFactory.getLogger(this::class.java) + private val metrics: Metrics = MetricsFactory.getMetricsInstance() + @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) override fun handleRequest(input: APIGatewayProxyRequestEvent?, context: Context?): APIGatewayProxyResponseEvent { val headers = mapOf("Content-Type" to "application/json", "X-Custom-Header" to "application/json") - MetricsUtils.metricsLogger().putMetric("CustomMetric1", 1.0, Unit.COUNT) - MetricsUtils.withSingleMetric("CustomMetrics2", 1.0, Unit.COUNT, "Another") { metric: MetricsLogger -> - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")) - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")) - } + + metrics.addMetric("CustomMetric1", 1.0, MetricUnit.COUNT) + + val dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1" + ) + metrics.flushSingleMetric("CustomMetric2", 1.0, MetricUnit.COUNT, "Another", dimensionSet) + MDC.put("test", "willBeLogged") val response = APIGatewayProxyResponseEvent().withHeaders(headers) return try { @@ -89,6 +96,4 @@ class App : RequestHandler = mapper.readValue(input, MutableMap::class.java) diff --git a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/java/helloworld/App.java index e7c410042..68664feec 100644 --- a/examples/powertools-examples-core-utilities/sam-graalvm/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/sam-graalvm/src/main/java/helloworld/App.java @@ -15,8 +15,6 @@ package helloworld; import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; import com.amazonaws.services.lambda.runtime.Context; @@ -33,11 +31,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.StorageResolution; -import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.tracing.TracingUtils; @@ -47,25 +47,25 @@ */ public class App implements RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { Map headers = new HashMap<>(); headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> - { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); - }); + DimensionSet dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); - metricsLogger().putMetric("CustomMetric3", 1, Unit.COUNT, StorageResolution.HIGH); + metrics.addMetric("CustomMetric3", 1, MetricUnit.COUNT, MetricResolution.HIGH); MDC.put("test", "willBeLogged"); @@ -77,8 +77,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv TracingUtils.putAnnotation("Test", "New"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - TracingUtils.withSubsegment("loggingResponse", subsegment -> - { + TracingUtils.withSubsegment("loggingResponse", subsegment -> { String sampled = "log something out"; log.info(sampled); log.info(output); diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java index e7c410042..2675c96eb 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java @@ -15,14 +15,8 @@ package helloworld; import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -30,14 +24,23 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.StorageResolution; -import software.amazon.cloudwatchlogs.emf.model.Unit; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.tracing.TracingUtils; @@ -47,25 +50,25 @@ */ public class App implements RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { Map headers = new HashMap<>(); headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> - { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); - }); + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension("AnotherService", "CustomService"); + dimensionSet.addDimension("AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); - metricsLogger().putMetric("CustomMetric3", 1, Unit.COUNT, StorageResolution.HIGH); + metrics.addMetric("CustomMetric3", 1, MetricUnit.COUNT, MetricResolution.HIGH); MDC.put("test", "willBeLogged"); @@ -77,8 +80,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv TracingUtils.putAnnotation("Test", "New"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - TracingUtils.withSubsegment("loggingResponse", subsegment -> - { + TracingUtils.withSubsegment("loggingResponse", subsegment -> { String sampled = "log something out"; log.info(sampled); log.info(output); diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java index 94806cc38..8bc57b201 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java +++ b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/AppStream.java @@ -26,7 +26,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import java.io.InputStreamReader; import java.io.BufferedReader; @@ -40,7 +40,7 @@ public class AppStream implements RequestStreamHandler { @Override @Logging(logEvent = true) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) // RequestStreamHandler can be used instead of RequestHandler for cases when you'd like to deserialize request body or serialize response body yourself, instead of allowing that to happen automatically // Note that you still need to return a proper JSON for API Gateway to handle // See Lambda Response format for examples: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html diff --git a/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/App.java index e0b1a2979..771f5c1f1 100644 --- a/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/App.java @@ -15,8 +15,6 @@ package helloworld; import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; import com.amazonaws.services.lambda.runtime.Context; @@ -33,10 +31,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.slf4j.MDC; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.tracing.TracingUtils; @@ -46,23 +46,23 @@ */ public class App implements RequestHandler { private static final Logger log = LogManager.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { Map headers = new HashMap<>(); headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> - { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); - }); + DimensionSet dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); @@ -74,8 +74,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv TracingUtils.putAnnotation("Test", "New"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - TracingUtils.withSubsegment("loggingResponse", subsegment -> - { + TracingUtils.withSubsegment("loggingResponse", subsegment -> { String sampled = "log something out"; log.info(sampled); log.info(output); diff --git a/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/AppStream.java index 401ef8c48..c13ab9f2e 100644 --- a/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/AppStream.java +++ b/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/AppStream.java @@ -22,14 +22,14 @@ import java.io.OutputStream; import java.util.Map; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; public class AppStream implements RequestStreamHandler { private static final ObjectMapper mapper = new ObjectMapper(); @Override @Logging(logEvent = true) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { Map map = mapper.readValue(input, Map.class); diff --git a/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java index e0b1a2979..771f5c1f1 100644 --- a/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java @@ -15,8 +15,6 @@ package helloworld; import static software.amazon.lambda.powertools.logging.argument.StructuredArguments.entry; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; import com.amazonaws.services.lambda.runtime.Context; @@ -33,10 +31,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.slf4j.MDC; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.tracing.CaptureMode; import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.tracing.TracingUtils; @@ -46,23 +46,23 @@ */ public class App implements RequestHandler { private static final Logger log = LogManager.getLogger(App.class); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { Map headers = new HashMap<>(); headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> - { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); - }); + DimensionSet dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); @@ -74,8 +74,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv TracingUtils.putAnnotation("Test", "New"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); - TracingUtils.withSubsegment("loggingResponse", subsegment -> - { + TracingUtils.withSubsegment("loggingResponse", subsegment -> { String sampled = "log something out"; log.info(sampled); log.info(output); diff --git a/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/AppStream.java index 401ef8c48..c13ab9f2e 100644 --- a/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/AppStream.java +++ b/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/AppStream.java @@ -22,14 +22,14 @@ import java.io.OutputStream; import java.util.Map; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; public class AppStream implements RequestStreamHandler { private static final ObjectMapper mapper = new ObjectMapper(); @Override @Logging(logEvent = true) - @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + @FlushMetrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { Map map = mapper.readValue(input, Map.class); diff --git a/pom.xml b/pom.xml index cb761183e..81e513b8c 100644 --- a/pom.xml +++ b/pom.xml @@ -14,8 +14,8 @@ --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 software.amazon.lambda @@ -101,7 +101,7 @@ 1.12.781 2.18.0 1.6.0 - 5.12.0 + 5.17.0 @@ -279,6 +279,11 @@ aws-embedded-metrics ${aws-embedded-metrics.version} + + org.apache.commons + commons-lang3 + 3.15.0 + @@ -294,12 +299,6 @@ 1.9.1 test - - org.apache.commons - commons-lang3 - 3.15.0 - test - org.aspectj aspectjweaver @@ -341,6 +340,18 @@ ${mockito.version} test + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + org.mockito + mockito-subclass + ${mockito.version} + test + com.amazonaws aws-lambda-java-tests diff --git a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index a86e515f7..7244b3212 100644 --- a/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/metrics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -16,37 +16,36 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.StorageResolution; -import software.amazon.cloudwatchlogs.emf.model.Unit; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; -import software.amazon.lambda.powertools.metrics.MetricsUtils; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.time.Instant; - public class Function implements RequestHandler { - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); + Metrics metrics = MetricsFactory.getMetricsInstance(); - @Metrics(captureColdStart = true) + @FlushMetrics(captureColdStart = true) public String handleRequest(Input input, Context context) { - Instant currentTimeTruncatedPlusThirty = - LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES).toInstant(ZoneOffset.UTC).plusSeconds(30); - metricsLogger.setTimestamp(currentTimeTruncatedPlusThirty); + Instant currentTimeTruncatedPlusThirty = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES) + .toInstant(ZoneOffset.UTC).plusSeconds(30); + metrics.setTimestamp(currentTimeTruncatedPlusThirty); DimensionSet dimensionSet = new DimensionSet(); input.getDimensions().forEach((key, value) -> dimensionSet.addDimension(key, value)); - metricsLogger.putDimensions(dimensionSet); + metrics.addDimension(dimensionSet); - input.getMetrics().forEach((key, value) -> metricsLogger.putMetric(key, value, Unit.COUNT, - input.getHighResolution().equalsIgnoreCase("true") ? StorageResolution.HIGH : - StorageResolution.STANDARD)); + input.getMetrics().forEach((key, value) -> metrics.addMetric(key, value, MetricUnit.COUNT, + input.getHighResolution().equalsIgnoreCase("true") ? MetricResolution.HIGH + : MetricResolution.STANDARD)); return "OK"; } -} \ No newline at end of file +} diff --git a/powertools-metrics/pom.xml b/powertools-metrics/pom.xml index d51ea5b33..46f7bcd99 100644 --- a/powertools-metrics/pom.xml +++ b/powertools-metrics/pom.xml @@ -14,8 +14,8 @@ --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 powertools-metrics @@ -65,6 +65,10 @@ com.fasterxml.jackson.core jackson-databind + + org.apache.commons + commons-lang3 + @@ -82,6 +86,11 @@ mockito-core test + + org.mockito + mockito-junit-jupiter + test + org.slf4j slf4j-simple @@ -92,11 +101,6 @@ junit-pioneer test - - org.apache.commons - commons-lang3 - test - org.aspectj aspectjweaver @@ -116,7 +120,6 @@ org.mockito mockito-subclass - 5.17.0 test @@ -125,9 +128,9 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.3 - -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics,experimental-class-define-support + -Dmockito.mock.maker=subclass -Dorg.graalvm.nativeimage.imagecode=agent + -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics,experimental-class-define-support --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED @@ -142,7 +145,6 @@ org.mockito mockito-subclass - 5.17.0 test @@ -151,7 +153,7 @@ org.graalvm.buildtools native-maven-plugin - 0.10.2 + 0.10.6 true @@ -178,16 +180,26 @@ --initialize-at-build-time=org.junit.Ignore --initialize-at-build-time=java.lang.annotation.Annotation --initialize-at-build-time=org.junit.runners.model.FrameworkField - --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase - --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic - --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 - --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 - --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader - --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable - --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase - --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType - --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod - --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 + + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic + + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$1 + + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Argument$BindingMechanic$2 + + --initialize-at-build-time=net.bytebuddy.utility.dispatcher.JavaDispatcher$DynamicClassLoader + + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable + + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$AbstractBase + + --initialize-at-build-time=net.bytebuddy.description.type.TypeDescription$ForLoadedType + + --initialize-at-build-time=net.bytebuddy.description.method.MethodDescription$ForLoadedMethod + + --initialize-at-build-time=net.bytebuddy.implementation.bind.annotation.Super$Instantiation$2 --trace-class-initialization=net.bytebuddy.description.type.TypeDescription$ForLoadedType,net.bytebuddy.description.method.MethodDescription$ForLoadedMethod,net.bytebuddy.description.method.MethodDescription$InDefinedShape$AbstractBase$ForLoadedExecutable @@ -207,5 +219,16 @@ src/main/resources + + + org.apache.maven.plugins + maven-surefire-plugin + + + Lambda + + + + diff --git a/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java b/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java deleted file mode 100644 index e2d886fe5..000000000 --- a/powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package software.amazon.cloudwatchlogs.emf.model; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import java.lang.reflect.Field; - -public final class MetricsLoggerHelper { - private MetricsLoggerHelper() { - } - - public static boolean hasNoMetrics() { - return metricsContext().getRootNode().getAws().isEmpty(); - } - - public static long dimensionsCount() { - return metricsContext().getDimensions().size(); - } - - public static MetricsContext metricsContext() { - try { - Field f = metricsLogger().getClass().getDeclaredField("context"); - f.setAccessible(true); - return (MetricsContext) f.get(metricsLogger()); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } -} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/FlushMetrics.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/FlushMetrics.java new file mode 100644 index 000000000..952625f5b --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/FlushMetrics.java @@ -0,0 +1,71 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code FlushMetrics} is used to signal that the annotated Lambda handler method should be + * extended with Metrics flushing functionality. Will have no effect when used on a method that is not a Lambda handler. + * + *

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

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

+ * + *

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

+ * + *

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

+ * + *

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

+ * + *

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

+ * + *

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

+ * + *

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

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

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

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

- * - *

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

- * - *

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

- * - *

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

- * - *

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

- * - *

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

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

- * {@see Metrics} - */ -public final class MetricsUtils { - private static final MetricsLogger metricsLogger = new MetricsLogger(); - private static DimensionSet[] defaultDimensions; - - private MetricsUtils() { - } - - /** - * The instance of the {@code MetricsLogger} used by {@code Metrics}. - * - * @return The instance of the MetricsLogger used by Metrics. - */ - public static MetricsLogger metricsLogger() { - return metricsLogger; - } - - /** - * Configure default dimension to be used by logger. - * By default, @{@link Metrics} annotation captures configured service as a dimension Service - * - * @param dimensionSets Default value of dimensions set for logger - */ - public static void defaultDimensions(final DimensionSet... dimensionSets) { - MetricsUtils.defaultDimensions = dimensionSets; - } - - /** - * Add and immediately flush a single metric. It will use the default namespace - * specified either on {@link Metrics} annotation or via POWERTOOLS_METRICS_NAMESPACE env var. - * It by default captures function_request_id as property if used together with {@link Metrics} annotation. It will also - * capture xray_trace_id as property if tracing is enabled. - * - * @param name the name of the metric - * @param value the value of the metric - * @param unit the unit type of the metric - * @param logger the MetricsLogger - */ - public static void withSingleMetric(final String name, - final double value, - final Unit unit, - final Consumer logger) { - withMetricsLogger(metricsLogger -> - { - metricsLogger.putMetric(name, value, unit); - logger.accept(metricsLogger); - }); - } - - /** - * Add and immediately flush a single metric. - * It by default captures function_request_id as property if used together with {@link Metrics} annotation. It will also - * capture xray_trace_id as property if tracing is enabled. - * - * @param name the name of the metric - * @param value the value of the metric - * @param unit the unit type of the metric - * @param namespace the namespace associated with the metric - * @param logger the MetricsLogger - */ - public static void withSingleMetric(final String name, - final double value, - final Unit unit, - final String namespace, - final Consumer logger) { - withMetricsLogger(metricsLogger -> - { - metricsLogger.setNamespace(namespace); - metricsLogger.putMetric(name, value, unit); - logger.accept(metricsLogger); - }); - } - - /** - * Provide and immediately flush a {@link MetricsLogger}. It uses the default namespace - * specified either on {@link Metrics} annotation or via POWERTOOLS_METRICS_NAMESPACE env var. - * It by default captures function_request_id as property if used together with {@link Metrics} annotation. It will also - * capture xray_trace_id as property if tracing is enabled. - * - * @param logger the MetricsLogger - */ - public static void withMetricsLogger(final Consumer logger) { - MetricsLogger metricsLogger = logger(); - - try { - metricsLogger.setNamespace(defaultNameSpace()); - captureRequestAndTraceId(metricsLogger); - logger.accept(metricsLogger); - } finally { - metricsLogger.flush(); - } - } - - public static DimensionSet[] getDefaultDimensions() { - return Arrays.copyOf(defaultDimensions, defaultDimensions.length); - } - - public static boolean hasDefaultDimension() { - return null != defaultDimensions; - } - - private static void captureRequestAndTraceId(MetricsLogger metricsLogger) { - awsRequestId(). - ifPresent(requestId -> metricsLogger.putProperty(REQUEST_ID_PROPERTY, requestId)); - - getXrayTraceId() - .ifPresent(traceId -> metricsLogger.putProperty(TRACE_ID_PROPERTY, traceId)); - } - - private static String defaultNameSpace() { - MetricsContext context = MetricsLoggerHelper.metricsContext(); - if ("aws-embedded-metrics".equals(context.getNamespace())) { - String namespace = SystemWrapper.getenv("POWERTOOLS_METRICS_NAMESPACE"); - return namespace != null ? namespace : "aws-embedded-metrics"; - } else { - return context.getNamespace(); - } - } - - private static Optional awsRequestId() { - MetricsContext context = MetricsLoggerHelper.metricsContext(); - return ofNullable(context.getProperty(REQUEST_ID_PROPERTY)) - .map(Object::toString); - } - - private static MetricsLogger logger() { - MetricsContext metricsContext = new MetricsContext(); - - if (hasDefaultDimension()) { - metricsContext.setDimensions(defaultDimensions); - } - - return new MetricsLogger(new EnvironmentProvider(), metricsContext); - } -} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java new file mode 100644 index 000000000..a55e1da5a --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java @@ -0,0 +1,326 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics.internal; + +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.getXrayTraceId; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isColdStart; + +import java.time.Instant; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.amazonaws.services.lambda.runtime.Context; + +import software.amazon.cloudwatchlogs.emf.environment.EnvironmentProvider; +import software.amazon.cloudwatchlogs.emf.model.DimensionSet; +import software.amazon.cloudwatchlogs.emf.model.MetricsContext; +import software.amazon.cloudwatchlogs.emf.model.StorageResolution; +import software.amazon.cloudwatchlogs.emf.model.Unit; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; + +/** + * Implementation of Metrics that uses the EMF library. Proxies Metrics interface calls to underlying + * library {@link software.amazon.cloudwatchlogs.emf.logger.MetricsLogger}. + */ +public class EmfMetricsLogger implements Metrics { + private static final Logger LOGGER = LoggerFactory.getLogger(EmfMetricsLogger.class); + private static final String TRACE_ID_PROPERTY = "xray_trace_id"; + private static final String REQUEST_ID_PROPERTY = "function_request_id"; + private static final String COLD_START_METRIC = "ColdStart"; + private static final String METRICS_DISABLED_ENV_VAR = "POWERTOOLS_METRICS_DISABLED"; + + private final software.amazon.cloudwatchlogs.emf.logger.MetricsLogger emfLogger; + private final EnvironmentProvider environmentProvider; + private boolean raiseOnEmptyMetrics = false; + private String namespace; + private Map defaultDimensions = new HashMap<>(); + private final AtomicBoolean hasMetrics = new AtomicBoolean(false); + + public EmfMetricsLogger(EnvironmentProvider environmentProvider, MetricsContext metricsContext) { + this.emfLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger(environmentProvider, + metricsContext); + this.environmentProvider = environmentProvider; + } + + @Override + public void addMetric(String key, double value, MetricUnit unit, MetricResolution resolution) { + StorageResolution storageResolution = resolution == MetricResolution.HIGH ? StorageResolution.HIGH + : StorageResolution.STANDARD; + emfLogger.putMetric(key, value, convertUnit(unit), storageResolution); + hasMetrics.set(true); + } + + @Override + public void addDimension(software.amazon.lambda.powertools.metrics.model.DimensionSet dimensionSet) { + if (dimensionSet == null) { + throw new IllegalArgumentException("DimensionSet cannot be null"); + } + + DimensionSet emfDimensionSet = new DimensionSet(); + dimensionSet.getDimensions().forEach((key, val) -> { + try { + emfDimensionSet.addDimension(key, val); + // Update our local copy of default dimensions + defaultDimensions.put(key, val); + } catch (Exception e) { + // Ignore dimension errors + } + }); + + emfLogger.putDimensions(emfDimensionSet); + } + + @Override + public void addMetadata(String key, Object value) { + emfLogger.putMetadata(key, value); + } + + @Override + public void setDefaultDimensions(software.amazon.lambda.powertools.metrics.model.DimensionSet dimensionSet) { + if (dimensionSet == null) { + throw new IllegalArgumentException("DimensionSet cannot be null"); + } + + DimensionSet emfDimensionSet = new DimensionSet(); + Map dimensions = dimensionSet.getDimensions(); + dimensions.forEach((key, value) -> { + try { + emfDimensionSet.addDimension(key, value); + } catch (Exception e) { + // Ignore dimension errors + } + }); + emfLogger.setDimensions(emfDimensionSet); + // Store a copy of the default dimensions + this.defaultDimensions = new LinkedHashMap<>(dimensions); + } + + @Override + public software.amazon.lambda.powertools.metrics.model.DimensionSet getDefaultDimensions() { + return software.amazon.lambda.powertools.metrics.model.DimensionSet.of(defaultDimensions); + } + + @Override + public void setNamespace(String namespace) { + Validator.validateNamespace(namespace); + + this.namespace = namespace; + try { + emfLogger.setNamespace(namespace); + } catch (Exception e) { + LOGGER.error("Namespace cannot be set due to an error in EMF", e); + } + } + + @Override + public void setRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics) { + this.raiseOnEmptyMetrics = raiseOnEmptyMetrics; + } + + @Override + public void setTimestamp(Instant timestamp) { + Validator.validateTimestamp(timestamp); + + emfLogger.setTimestamp(timestamp); + } + + @Override + public void clearDefaultDimensions() { + emfLogger.resetDimensions(false); + defaultDimensions.clear(); + } + + @Override + public void flush() { + if (isMetricsDisabled()) { + LOGGER.debug("Metrics are disabled, skipping flush"); + return; + } + + Validator.validateNamespace(namespace); + + if (!hasMetrics.get()) { + if (raiseOnEmptyMetrics) { + throw new IllegalStateException("No metrics were emitted"); + } else { + LOGGER.warn("No metrics were emitted"); + } + } + emfLogger.flush(); + } + + @Override + public void captureColdStartMetric(Context context, + software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) { + if (isColdStart()) { + if (isMetricsDisabled()) { + LOGGER.debug("Metrics are disabled, skipping cold start metric capture"); + return; + } + + Validator.validateNamespace(namespace); + + software.amazon.cloudwatchlogs.emf.logger.MetricsLogger coldStartLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger(); + + try { + coldStartLogger.setNamespace(namespace); + } catch (Exception e) { + LOGGER.error("Namespace cannot be set for cold start metrics due to an error in EMF", e); + } + + coldStartLogger.putMetric(COLD_START_METRIC, 1, Unit.COUNT); + + // Set dimensions if provided + if (dimensions != null) { + DimensionSet emfDimensionSet = new DimensionSet(); + dimensions.getDimensions().forEach((key, val) -> { + try { + emfDimensionSet.addDimension(key, val); + } catch (Exception e) { + // Ignore dimension errors + } + }); + coldStartLogger.setDimensions(emfDimensionSet); + } + + // Add request ID from context if available + if (context != null) { + coldStartLogger.putProperty(REQUEST_ID_PROPERTY, context.getAwsRequestId()); + } + + // Add trace ID using the standard logic + getXrayTraceId().ifPresent(traceId -> coldStartLogger.putProperty(TRACE_ID_PROPERTY, traceId)); + + coldStartLogger.flush(); + } + } + + @Override + public void captureColdStartMetric(software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) { + captureColdStartMetric(null, dimensions); + } + + @Override + public void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, + software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) { + if (isMetricsDisabled()) { + LOGGER.debug("Metrics are disabled, skipping single metric flush"); + return; + } + + Validator.validateNamespace(namespace); + + // Create a new logger for this single metric + software.amazon.cloudwatchlogs.emf.logger.MetricsLogger singleMetricLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger( + environmentProvider); + + try { + singleMetricLogger.setNamespace(namespace); + } catch (Exception e) { + LOGGER.error("Namespace cannot be set for single metric due to an error in EMF", e); + } + + // Add the metric + singleMetricLogger.putMetric(name, value, convertUnit(unit)); + + // Set dimensions if provided + if (dimensions != null) { + DimensionSet emfDimensionSet = new DimensionSet(); + dimensions.getDimensions().forEach((key, val) -> { + try { + emfDimensionSet.addDimension(key, val); + } catch (Exception e) { + // Ignore dimension errors + } + }); + singleMetricLogger.setDimensions(emfDimensionSet); + } + + // Flush the metric + singleMetricLogger.flush(); + } + + private boolean isMetricsDisabled() { + String disabledValue = System.getenv(METRICS_DISABLED_ENV_VAR); + return "true".equalsIgnoreCase(disabledValue); + } + + private Unit convertUnit(MetricUnit unit) { + switch (unit) { + case SECONDS: + return Unit.SECONDS; + case MICROSECONDS: + return Unit.MICROSECONDS; + case MILLISECONDS: + return Unit.MILLISECONDS; + case BYTES: + return Unit.BYTES; + case KILOBYTES: + return Unit.KILOBYTES; + case MEGABYTES: + return Unit.MEGABYTES; + case GIGABYTES: + return Unit.GIGABYTES; + case TERABYTES: + return Unit.TERABYTES; + case BITS: + return Unit.BITS; + case KILOBITS: + return Unit.KILOBITS; + case MEGABITS: + return Unit.MEGABITS; + case GIGABITS: + return Unit.GIGABITS; + case TERABITS: + return Unit.TERABITS; + case PERCENT: + return Unit.PERCENT; + case COUNT: + return Unit.COUNT; + case BYTES_SECOND: + return Unit.BYTES_SECOND; + case KILOBYTES_SECOND: + return Unit.KILOBYTES_SECOND; + case MEGABYTES_SECOND: + return Unit.MEGABYTES_SECOND; + case GIGABYTES_SECOND: + return Unit.GIGABYTES_SECOND; + case TERABYTES_SECOND: + return Unit.TERABYTES_SECOND; + case BITS_SECOND: + return Unit.BITS_SECOND; + case KILOBITS_SECOND: + return Unit.KILOBITS_SECOND; + case MEGABITS_SECOND: + return Unit.MEGABITS_SECOND; + case GIGABITS_SECOND: + return Unit.GIGABITS_SECOND; + case TERABITS_SECOND: + return Unit.TERABITS_SECOND; + case COUNT_SECOND: + return Unit.COUNT_SECOND; + case NONE: + default: + return Unit.NONE; + } + } +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java index 56a35f67f..b214d7c52 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspect.java @@ -14,138 +14,129 @@ package software.amazon.lambda.powertools.metrics.internal; -import static software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper.dimensionsCount; -import static software.amazon.cloudwatchlogs.emf.model.MetricsLoggerHelper.hasNoMetrics; import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.coldStartDone; import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.extractContext; -import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isColdStart; import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isHandlerMethod; -import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.serviceName; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.hasDefaultDimension; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import com.amazonaws.services.lambda.runtime.Context; -import java.lang.reflect.Field; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.MetricsContext; -import software.amazon.cloudwatchlogs.emf.model.Unit; + +import com.amazonaws.services.lambda.runtime.Context; + +import software.amazon.lambda.powertools.common.internal.LambdaConstants; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.Metrics; -import software.amazon.lambda.powertools.metrics.MetricsUtils; -import software.amazon.lambda.powertools.metrics.ValidationException; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; @Aspect public class LambdaMetricsAspect { public static final String TRACE_ID_PROPERTY = "xray_trace_id"; public static final String REQUEST_ID_PROPERTY = "function_request_id"; - private static final String NAMESPACE = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); - - private static String service(Metrics metrics) { - return !"".equals(metrics.service()) ? metrics.service() : serviceName(); - } + private static final String SERVICE_DIMENSION = "Service"; + private static final String FUNCTION_NAME_ENV_VAR = "POWERTOOLS_METRICS_FUNCTION_NAME"; - // This can be simplified after this issues https://github.com/awslabs/aws-embedded-metrics-java/issues/35 is fixed - public static void refreshMetricsContext(Metrics metrics) { - try { - Field f = metricsLogger().getClass().getDeclaredField("context"); - f.setAccessible(true); - MetricsContext context = new MetricsContext(); + private String functionName(FlushMetrics metrics, Context context) { + if (!"".equals(metrics.functionName())) { + return metrics.functionName(); + } - DimensionSet[] defaultDimensions = hasDefaultDimension() ? MetricsUtils.getDefaultDimensions() - : new DimensionSet[] {DimensionSet.of("Service", service(metrics))}; + String envFunctionName = System.getenv(FUNCTION_NAME_ENV_VAR); + if (envFunctionName != null && !envFunctionName.isEmpty()) { + return envFunctionName; + } - context.setDimensions(defaultDimensions); + return context != null ? context.getFunctionName() : null; + } - f.set(metricsLogger(), context); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); + private String serviceNameWithFallback(FlushMetrics metrics) { + if (!"".equals(metrics.service())) { + return metrics.service(); } + return LambdaHandlerProcessor.serviceName(); } - @SuppressWarnings({"EmptyMethod"}) + @SuppressWarnings({ "EmptyMethod" }) @Pointcut("@annotation(metrics)") - public void callAt(Metrics metrics) { + public void callAt(FlushMetrics metrics) { + // AspectJ point cut referenced in around() method } - @Around(value = "callAt(metrics) && execution(@Metrics * *.*(..))", argNames = "pjp,metrics") + @Around(value = "callAt(metrics) && execution(@FlushMetrics * *.*(..))", argNames = "pjp,metrics") public Object around(ProceedingJoinPoint pjp, - Metrics metrics) throws Throwable { + FlushMetrics metrics) throws Throwable { Object[] proceedArgs = pjp.getArgs(); if (isHandlerMethod(pjp)) { + Metrics metricsInstance = MetricsFactory.getMetricsInstance(); - MetricsLogger logger = metricsLogger(); - - refreshMetricsContext(metrics); - - logger.setNamespace(namespace(metrics)); - - Context extractedContext = extractContext(pjp); + // The MetricsFactory applies default settings from the environment or can be configured by the + // MetricsBuilder. We only overwrite settings if they are explicitly set in the @FlushMetrics + // annotation. + if (!"".equals(metrics.namespace())) { + metricsInstance.setNamespace(metrics.namespace()); + } - if (null != extractedContext) { - coldStartSingleMetricIfApplicable(extractedContext.getAwsRequestId(), - extractedContext.getFunctionName(), metrics); - logger.putProperty(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); + // We only overwrite the default dimensions if the user didn't overwrite them previously. This means that + // they are either empty or only contain the default "Service" dimension. + if (!"".equals(metrics.service().trim()) + && (metricsInstance.getDefaultDimensions().getDimensionKeys().size() <= 1 + || metricsInstance.getDefaultDimensions().getDimensionKeys().contains(SERVICE_DIMENSION))) { + metricsInstance.setDefaultDimensions(DimensionSet.of(SERVICE_DIMENSION, metrics.service())); } + metricsInstance.setRaiseOnEmptyMetrics(metrics.raiseOnEmptyMetrics()); + + // Add trace ID metadata if available LambdaHandlerProcessor.getXrayTraceId() - .ifPresent(traceId -> logger.putProperty(TRACE_ID_PROPERTY, traceId)); + .ifPresent(traceId -> metricsInstance.addMetadata(TRACE_ID_PROPERTY, traceId)); + + captureColdStartMetricIfEnabled(extractContext(pjp), metrics); try { return pjp.proceed(proceedArgs); - } finally { coldStartDone(); - validateMetricsAndRefreshOnFailure(metrics); - logger.flush(); - refreshMetricsContext(metrics); + metricsInstance.flush(); } } return pjp.proceed(proceedArgs); } - private void coldStartSingleMetricIfApplicable(final String awsRequestId, - final String functionName, - final Metrics metrics) { - if (metrics.captureColdStart() - && isColdStart()) { - MetricsLogger metricsLogger = new MetricsLogger(); - metricsLogger.setNamespace(namespace(metrics)); - metricsLogger.putMetric("ColdStart", 1, Unit.COUNT); - metricsLogger.setDimensions(DimensionSet.of("Service", service(metrics), "FunctionName", functionName)); - metricsLogger.putProperty(REQUEST_ID_PROPERTY, awsRequestId); - metricsLogger.flush(); + private void captureColdStartMetricIfEnabled(Context extractedContext, FlushMetrics metrics) { + if (extractedContext == null) { + return; } - } + Metrics metricsInstance = MetricsFactory.getMetricsInstance(); + metricsInstance.addMetadata(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); - private void validateBeforeFlushingMetrics(Metrics metrics) { - if (metrics.raiseOnEmptyMetrics() && hasNoMetrics()) { - throw new ValidationException("No metrics captured, at least one metrics must be emitted"); - } + // Only capture cold start metrics if enabled on annotation + if (metrics.captureColdStart()) { + // Get function name from annotation or context + String funcName = functionName(metrics, extractedContext); - if (dimensionsCount() > 9) { - throw new ValidationException(String.format("Number of Dimensions must be in range of 0-9." + - " Actual size: %d.", dimensionsCount())); - } - } + DimensionSet coldStartDimensions = new DimensionSet(); - private String namespace(Metrics metrics) { - return !"".equals(metrics.namespace()) ? metrics.namespace() : NAMESPACE; - } + // Get service name from metrics instance default dimensions or fallback + String serviceName = metricsInstance.getDefaultDimensions().getDimensions().getOrDefault( + SERVICE_DIMENSION, + serviceNameWithFallback(metrics)); + + // Only add service if it is not undefined + if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) { + coldStartDimensions.addDimension(SERVICE_DIMENSION, serviceName); + } + + // Add function name + coldStartDimensions.addDimension("FunctionName", + funcName != null ? funcName : extractedContext.getFunctionName()); - private void validateMetricsAndRefreshOnFailure(Metrics metrics) { - try { - validateBeforeFlushingMetrics(metrics); - } catch (ValidationException e) { - refreshMetricsContext(metrics); - throw e; + metricsInstance.captureColdStartMetric(extractedContext, coldStartDimensions); } } } diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/Validator.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/Validator.java new file mode 100644 index 000000000..eebb54739 --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/Validator.java @@ -0,0 +1,135 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics.internal; + +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang3.StringUtils; + +/** + * Utility class for validating metrics-related parameters. + */ +public class Validator { + private static final int MAX_DIMENSION_NAME_LENGTH = 250; + private static final int MAX_DIMENSION_VALUE_LENGTH = 1024; + private static final int MAX_NAMESPACE_LENGTH = 255; + private static final String NAMESPACE_REGEX = "^[a-zA-Z0-9._#:/-]+$"; + public static final long MAX_TIMESTAMP_PAST_AGE_SECONDS = TimeUnit.DAYS.toSeconds(14); + public static final long MAX_TIMESTAMP_FUTURE_AGE_SECONDS = TimeUnit.HOURS.toSeconds(2); + + private Validator() { + // Private constructor to prevent instantiation + } + + /** + * Validates that a namespace is properly specified. + * + * @param namespace The namespace to validate + * @throws IllegalArgumentException if the namespace is invalid + */ + public static void validateNamespace(String namespace) { + if (namespace == null || namespace.trim().isEmpty()) { + throw new IllegalArgumentException("Namespace must be specified before flushing metrics"); + } + + if (namespace.length() > MAX_NAMESPACE_LENGTH) { + throw new IllegalArgumentException( + "Namespace exceeds maximum length of " + MAX_NAMESPACE_LENGTH + ": " + namespace); + } + + if (!namespace.matches(NAMESPACE_REGEX)) { + throw new IllegalArgumentException("Namespace contains invalid characters: " + namespace); + } + } + + /** + * Validates Timestamp. + * + * @see CloudWatch + * Timestamp + * @param timestamp Timestamp + * @throws IllegalArgumentException if timestamp is invalid + */ + public static void validateTimestamp(Instant timestamp) { + if (timestamp == null) { + throw new IllegalArgumentException("Timestamp cannot be null"); + } + + if (timestamp.isAfter( + Instant.now().plusSeconds(MAX_TIMESTAMP_FUTURE_AGE_SECONDS))) { + throw new IllegalArgumentException( + "Timestamp cannot be more than " + + MAX_TIMESTAMP_FUTURE_AGE_SECONDS + + " seconds in the future"); + } + + if (timestamp.isBefore( + Instant.now().minusSeconds(MAX_TIMESTAMP_PAST_AGE_SECONDS))) { + throw new IllegalArgumentException( + "Timestamp cannot be more than " + + MAX_TIMESTAMP_PAST_AGE_SECONDS + + " seconds in the past"); + } + } + + /** + * Validates a dimension key-value pair. + * + * @param key The dimension key to validate + * @param value The dimension value to validate + * @throws IllegalArgumentException if the key or value is invalid + */ + public static void validateDimension(String key, String value) { + if (key == null || key.trim().isEmpty()) { + throw new IllegalArgumentException("Dimension key cannot be null or empty"); + } + + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException("Dimension value cannot be null or empty"); + } + + if (StringUtils.containsWhitespace(key)) { + throw new IllegalArgumentException("Dimension key cannot contain whitespaces: " + key); + } + + if (StringUtils.containsWhitespace(value)) { + throw new IllegalArgumentException("Dimension value cannot contain whitespaces: " + value); + } + + if (key.startsWith(":")) { + throw new IllegalArgumentException("Dimension key cannot start with colon: " + key); + } + + if (key.length() > MAX_DIMENSION_NAME_LENGTH) { + throw new IllegalArgumentException( + "Dimension name exceeds maximum length of " + MAX_DIMENSION_NAME_LENGTH + ": " + key); + } + + if (value.length() > MAX_DIMENSION_VALUE_LENGTH) { + throw new IllegalArgumentException( + "Dimension value exceeds maximum length of " + MAX_DIMENSION_VALUE_LENGTH + ": " + value); + } + + if (!StringUtils.isAsciiPrintable(key)) { + throw new IllegalArgumentException("Dimension name has invalid characters: " + key); + } + + if (!StringUtils.isAsciiPrintable(value)) { + throw new IllegalArgumentException("Dimension value has invalid characters: " + value); + } + } +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/DimensionSet.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/DimensionSet.java new file mode 100644 index 000000000..e93f34237 --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/DimensionSet.java @@ -0,0 +1,193 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics.model; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import software.amazon.lambda.powertools.metrics.internal.Validator; + +/** + * Represents a set of dimensions for CloudWatch metrics + */ +public class DimensionSet { + private static final int MAX_DIMENSION_SET_SIZE = 30; + + private final Map dimensions = new LinkedHashMap<>(); + + /** + * Create a dimension set with a single key-value pair + * + * @param key dimension key + * @param value dimension value + * @return a new DimensionSet + */ + public static DimensionSet of(String key, String value) { + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension(key, value); + return dimensionSet; + } + + /** + * Create a dimension set with two key-value pairs + * + * @param key1 first dimension key + * @param value1 first dimension value + * @param key2 second dimension key + * @param value2 second dimension value + * @return a new DimensionSet + */ + public static DimensionSet of(String key1, String value1, String key2, String value2) { + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension(key1, value1); + dimensionSet.addDimension(key2, value2); + return dimensionSet; + } + + /** + * Create a dimension set with three key-value pairs + * + * @param key1 first dimension key + * @param value1 first dimension value + * @param key2 second dimension key + * @param value2 second dimension value + * @param key3 third dimension key + * @param value3 third dimension value + * @return a new DimensionSet + */ + public static DimensionSet of(String key1, String value1, String key2, String value2, String key3, String value3) { + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension(key1, value1); + dimensionSet.addDimension(key2, value2); + dimensionSet.addDimension(key3, value3); + return dimensionSet; + } + + /** + * Create a dimension set with four key-value pairs + * + * @param key1 first dimension key + * @param value1 first dimension value + * @param key2 second dimension key + * @param value2 second dimension value + * @param key3 third dimension key + * @param value3 third dimension value + * @param key4 fourth dimension key + * @param value4 fourth dimension value + * @return a new DimensionSet + */ + public static DimensionSet of(String key1, String value1, String key2, String value2, + String key3, String value3, String key4, String value4) { + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension(key1, value1); + dimensionSet.addDimension(key2, value2); + dimensionSet.addDimension(key3, value3); + dimensionSet.addDimension(key4, value4); + return dimensionSet; + } + + /** + * Create a dimension set with five key-value pairs + * + * @param key1 first dimension key + * @param value1 first dimension value + * @param key2 second dimension key + * @param value2 second dimension value + * @param key3 third dimension key + * @param value3 third dimension value + * @param key4 fourth dimension key + * @param value4 fourth dimension value + * @param key5 fifth dimension key + * @param value5 fifth dimension value + * @return a new DimensionSet + */ + public static DimensionSet of(String key1, String value1, String key2, String value2, + String key3, String value3, String key4, String value4, + String key5, String value5) { + DimensionSet dimensionSet = new DimensionSet(); + dimensionSet.addDimension(key1, value1); + dimensionSet.addDimension(key2, value2); + dimensionSet.addDimension(key3, value3); + dimensionSet.addDimension(key4, value4); + dimensionSet.addDimension(key5, value5); + return dimensionSet; + } + + /** + * Create a dimension set from a map of key-value pairs + * + * @param dimensions map of dimension key-value pairs + * @return a new DimensionSet + */ + public static DimensionSet of(Map dimensions) { + DimensionSet dimensionSet = new DimensionSet(); + dimensions.forEach(dimensionSet::addDimension); + return dimensionSet; + } + + /** + * Add a dimension to this dimension set + * + * @param key dimension key + * @param value dimension value + * @return this dimension set for chaining + * @throws IllegalArgumentException if key or value is invalid + * @throws IllegalStateException if adding would exceed the maximum number of dimensions + */ + public DimensionSet addDimension(String key, String value) { + validateDimension(key, value); + + if (dimensions.size() >= MAX_DIMENSION_SET_SIZE) { + throw new IllegalStateException( + "Cannot exceed " + MAX_DIMENSION_SET_SIZE + " dimensions per dimension set"); + } + + dimensions.put(key, value); + return this; + } + + /** + * Get the dimension keys in this dimension set + * + * @return set of dimension keys + */ + public Set getDimensionKeys() { + return dimensions.keySet(); + } + + /** + * Get the value for a dimension key + * + * @param key dimension key + * @return dimension value or null if not found + */ + public String getDimensionValue(String key) { + return dimensions.get(key); + } + + /** + * Get the dimensions as a map. Creates a shallow copy + * + * @return map of dimensions + */ + public Map getDimensions() { + return new LinkedHashMap<>(dimensions); + } + + private void validateDimension(String key, String value) { + Validator.validateDimension(key, value); + } +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/ValidationException.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricResolution.java similarity index 66% rename from powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/ValidationException.java rename to powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricResolution.java index a553abbbd..db514c8b7 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/ValidationException.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricResolution.java @@ -12,11 +12,21 @@ * */ -package software.amazon.lambda.powertools.metrics; +package software.amazon.lambda.powertools.metrics.model; -public class ValidationException extends RuntimeException { +/** + * Resolution for metrics + */ +public enum MetricResolution { + STANDARD(60), HIGH(1); + + private final int seconds; + + MetricResolution(int seconds) { + this.seconds = seconds; + } - public ValidationException(String message) { - super(message); + public int getSeconds() { + return seconds; } } diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricUnit.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricUnit.java new file mode 100644 index 000000000..445d950b2 --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricUnit.java @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics.model; + +/** + * Metric units supported by CloudWatch + */ +public enum MetricUnit { + SECONDS("Seconds"), + MICROSECONDS("Microseconds"), + MILLISECONDS("Milliseconds"), + BYTES("Bytes"), + KILOBYTES("Kilobytes"), + MEGABYTES("Megabytes"), + GIGABYTES("Gigabytes"), + TERABYTES("Terabytes"), + BITS("Bits"), + KILOBITS("Kilobits"), + MEGABITS("Megabits"), + GIGABITS("Gigabits"), + TERABITS("Terabits"), + PERCENT("Percent"), + COUNT("Count"), + BYTES_SECOND("Bytes/Second"), + KILOBYTES_SECOND("Kilobytes/Second"), + MEGABYTES_SECOND("Megabytes/Second"), + GIGABYTES_SECOND("Gigabytes/Second"), + TERABYTES_SECOND("Terabytes/Second"), + BITS_SECOND("Bits/Second"), + KILOBITS_SECOND("Kilobits/Second"), + MEGABITS_SECOND("Megabits/Second"), + GIGABITS_SECOND("Gigabits/Second"), + TERABITS_SECOND("Terabits/Second"), + COUNT_SECOND("Count/Second"), + NONE("None"); + + private final String name; + + MetricUnit(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProvider.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProvider.java new file mode 100644 index 000000000..12c99b18f --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics.provider; + +import software.amazon.cloudwatchlogs.emf.environment.EnvironmentProvider; +import software.amazon.cloudwatchlogs.emf.model.MetricsContext; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.internal.EmfMetricsLogger; + +/** + * Provider implementation for EMF metrics + */ +public class EmfMetricsProvider implements MetricsProvider { + + @Override + public Metrics getMetricsInstance() { + return new EmfMetricsLogger(new EnvironmentProvider(), new MetricsContext()); + } +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/MetricsProvider.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/MetricsProvider.java new file mode 100644 index 000000000..e6c79e000 --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/MetricsProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics.provider; + +import software.amazon.lambda.powertools.metrics.Metrics; + +/** + * Interface for metrics provider implementations + */ +public interface MetricsProvider { + + /** + * Get a new instance of a metrics implementation + * + * @return a new metrics instance + */ + Metrics getMetricsInstance(); +} diff --git a/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/jni-config.json b/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/jni-config.json index 8ea90d67f..75ee9e5f5 100644 --- a/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/jni-config.json +++ b/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/jni-config.json @@ -1,22 +1,33 @@ [ -{ - "name":"java.lang.String", - "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] -}, -{ - "name":"java.lang.System", - "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] -}, -{ - "name":"org.apache.maven.surefire.booter.ForkedBooter", - "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] -}, -{ - "name":"sun.instrument.InstrumentationImpl", - "methods":[{"name":"","parameterTypes":["long","boolean","boolean","boolean"] }, {"name":"loadClassAndCallAgentmain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"loadClassAndCallPremain","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"transform","parameterTypes":["java.lang.Module","java.lang.ClassLoader","java.lang.String","java.lang.Class","java.security.ProtectionDomain","byte[]","boolean"] }] -}, -{ - "name":"sun.management.VMManagementImpl", - "fields":[{"name":"compTimeMonitoringSupport"}, {"name":"currentThreadCpuTimeSupport"}, {"name":"objectMonitorUsageSupport"}, {"name":"otherThreadCpuTimeSupport"}, {"name":"remoteDiagnosticCommandsSupport"}, {"name":"synchronizerUsageSupport"}, {"name":"threadAllocatedMemorySupport"}, {"name":"threadContentionMonitoringSupport"}] -} + { + "name": "java.lang.Boolean", + "methods": [{ "name": "getBoolean", "parameterTypes": ["java.lang.String"] }] + }, + { + "name": "java.lang.String", + "methods": [ + { "name": "lastIndexOf", "parameterTypes": ["int"] }, + { "name": "substring", "parameterTypes": ["int"] } + ] + }, + { + "name": "java.lang.System", + "methods": [ + { "name": "getProperty", "parameterTypes": ["java.lang.String"] }, + { "name": "setProperty", "parameterTypes": ["java.lang.String", "java.lang.String"] } + ] + }, + { + "name": "sun.management.VMManagementImpl", + "fields": [ + { "name": "compTimeMonitoringSupport" }, + { "name": "currentThreadCpuTimeSupport" }, + { "name": "objectMonitorUsageSupport" }, + { "name": "otherThreadCpuTimeSupport" }, + { "name": "remoteDiagnosticCommandsSupport" }, + { "name": "synchronizerUsageSupport" }, + { "name": "threadAllocatedMemorySupport" }, + { "name": "threadContentionMonitoringSupport" } + ] + } ] diff --git a/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/reflect-config.json b/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/reflect-config.json index bf67fc97b..43b2822d6 100644 --- a/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/reflect-config.json +++ b/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/reflect-config.json @@ -1,285 +1,152 @@ [ -{ - "name":"com.amazonaws.services.lambda.runtime.Context", - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"getAwsRequestId","parameterTypes":[] }, {"name":"getClientContext","parameterTypes":[] }, {"name":"getFunctionName","parameterTypes":[] }, {"name":"getFunctionVersion","parameterTypes":[] }, {"name":"getIdentity","parameterTypes":[] }, {"name":"getInvokedFunctionArn","parameterTypes":[] }, {"name":"getLogGroupName","parameterTypes":[] }, {"name":"getLogStreamName","parameterTypes":[] }, {"name":"getLogger","parameterTypes":[] }, {"name":"getMemoryLimitInMB","parameterTypes":[] }, {"name":"getRemainingTimeInMillis","parameterTypes":[] }] -}, -{ - "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", - "methods":[{"name":"","parameterTypes":[] }] -}, -{ - "name":"com.sun.tools.attach.VirtualMachine" -}, -{ - "name":"java.io.InputStream" -}, -{ - "name":"java.io.OutputStream" -}, -{ - "name":"java.io.Serializable", - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true -}, -{ - "name":"java.lang.Class", - "methods":[{"name":"forName","parameterTypes":["java.lang.String"] }, {"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] -}, -{ - "name":"java.lang.ClassLoader", - "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] -}, -{ - "name":"java.lang.Comparable", - "queryAllDeclaredMethods":true -}, -{ - "name":"java.lang.Double", - "allDeclaredFields":true, - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true -}, -{ - "name":"java.lang.Module", - "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] -}, -{ - "name":"java.lang.Number", - "allDeclaredFields":true, - "queryAllDeclaredMethods":true -}, -{ - "name":"java.lang.Object", - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] -}, -{ - "name":"java.lang.ProcessEnvironment", - "fields":[{"name":"theCaseInsensitiveEnvironment"}, {"name":"theEnvironment"}] -}, -{ - "name":"java.lang.ProcessHandle", - "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] -}, -{ - "name":"java.lang.Runtime", - "methods":[{"name":"version","parameterTypes":[] }] -}, -{ - "name":"java.lang.Runtime$Version", - "methods":[{"name":"feature","parameterTypes":[] }] -}, -{ - "name":"java.lang.StackWalker" -}, -{ - "name":"java.lang.System", - "methods":[{"name":"getSecurityManager","parameterTypes":[] }] -}, -{ - "name":"java.lang.annotation.Retention", - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true -}, -{ - "name":"java.lang.annotation.Target", - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true -}, -{ - "name":"java.lang.constant.Constable", - "queryAllDeclaredMethods":true -}, -{ - "name":"java.lang.constant.ConstantDesc", - "queryAllDeclaredMethods":true -}, -{ - "name":"java.lang.invoke.MethodHandle", - "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] -}, -{ - "name":"java.lang.invoke.MethodHandles", - "methods":[{"name":"lookup","parameterTypes":[] }] -}, -{ - "name":"java.lang.invoke.MethodHandles$Lookup", - "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] -}, -{ - "name":"java.lang.invoke.MethodType", - "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] -}, -{ - "name":"java.lang.reflect.AccessibleObject", - "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] -}, -{ - "name":"java.lang.reflect.AnnotatedArrayType", - "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] -}, -{ - "name":"java.lang.reflect.AnnotatedType", - "methods":[{"name":"getType","parameterTypes":[] }] -}, -{ - "name":"java.lang.reflect.Executable", - "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] -}, -{ - "name":"java.lang.reflect.Method", - "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] -}, -{ - "name":"java.lang.reflect.Parameter", - "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] -}, -{ - "name":"java.security.AccessController", - "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] -}, -{ - "name":"java.util.Collections$UnmodifiableMap", - "fields":[{"name":"m"}] -}, -{ - "name":"java.util.concurrent.ForkJoinTask", - "fields":[{"name":"aux"}, {"name":"status"}] -}, -{ - "name":"java.util.concurrent.atomic.AtomicBoolean", - "fields":[{"name":"value"}] -}, -{ - "name":"java.util.concurrent.atomic.AtomicReference", - "fields":[{"name":"value"}] -}, -{ - "name":"jdk.internal.misc.Unsafe" -}, -{ - "name":"kotlin.jvm.JvmInline" -}, -{ - "name":"org.apiguardian.api.API", - "queryAllPublicMethods":true -}, -{ - "name":"software.amazon.cloudwatchlogs.emf.logger.MetricsLogger", - "fields":[{"name":"context"}] -}, -{ - "name":"software.amazon.cloudwatchlogs.emf.model.Metadata", - "allDeclaredFields":true, - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"getCloudWatchMetrics","parameterTypes":[] }, {"name":"getCustomMetadata","parameterTypes":[] }, {"name":"getTimestamp","parameterTypes":[] }] -}, -{ - "name":"software.amazon.cloudwatchlogs.emf.model.MetricDefinition", - "allDeclaredFields":true, - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"getName","parameterTypes":[] }, {"name":"getStorageResolution","parameterTypes":[] }, {"name":"getUnit","parameterTypes":[] }] -}, -{ - "name":"software.amazon.cloudwatchlogs.emf.model.MetricDirective", - "allDeclaredFields":true, - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"getAllDimensionKeys","parameterTypes":[] }, {"name":"getAllMetrics","parameterTypes":[] }, {"name":"getNamespace","parameterTypes":[] }] -}, -{ - "name":"software.amazon.cloudwatchlogs.emf.model.RootNode", - "allDeclaredFields":true, - "queryAllDeclaredMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"getAws","parameterTypes":[] }, {"name":"getTargetMembers","parameterTypes":[] }] -}, -{ - "name":"software.amazon.cloudwatchlogs.emf.serializers.InstantSerializer", - "methods":[{"name":"","parameterTypes":[] }] -}, -{ - "name":"software.amazon.cloudwatchlogs.emf.serializers.StorageResolutionFilter", - "methods":[{"name":"","parameterTypes":[] }] -}, -{ - "name":"software.amazon.cloudwatchlogs.emf.serializers.StorageResolutionSerializer", - "methods":[{"name":"","parameterTypes":[] }] -}, -{ - "name":"software.amazon.cloudwatchlogs.emf.serializers.UnitSerializer", - "methods":[{"name":"","parameterTypes":[] }] -}, -{ - "name":"software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor", - "fields":[{"name":"IS_COLD_START"}] -}, -{ - "name":"software.amazon.lambda.powertools.metrics.MetricsLoggerTest", - "allDeclaredFields":true, - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }, {"name":"metricsLoggerCaptureUtilityWithDefaultNameSpace","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }, {"name":"shouldUseTraceIdFromSystemPropertyIfEnvVarNotPresent","parameterTypes":[] }, {"name":"singleMetricsCaptureUtility","parameterTypes":[] }, {"name":"singleMetricsCaptureUtilityWithDefaultDimension","parameterTypes":[] }, {"name":"singleMetricsCaptureUtilityWithDefaultNameSpace","parameterTypes":[] }, {"name":"singleMetricsCaptureUtilityWithNullNamespace","parameterTypes":[] }, {"name":"tearDown","parameterTypes":[] }] -}, -{ - "name":"software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsColdStartEnabledHandler", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledDefaultDimensionHandler", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledDefaultNoDimensionHandler", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledHandler", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledStreamHandler", - "methods":[{"name":"handleRequest","parameterTypes":["java.io.InputStream","java.io.OutputStream","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsExceptionWhenNoMetricsHandler", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsNoDimensionsHandler", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsNoExceptionWhenNoMetricsHandler", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsTooManyDimensionsHandler", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsWithExceptionInHandler", - "methods":[{"name":"handleRequest","parameterTypes":["java.lang.Object","com.amazonaws.services.lambda.runtime.Context"] }] -}, -{ - "name":"software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest", - "allDeclaredFields":true, - "allDeclaredClasses":true, - "queryAllDeclaredMethods":true, - "queryAllPublicMethods":true, - "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":[] }, {"name":"allowWhenNoDimensionsSet","parameterTypes":[] }, {"name":"exceptionWhenNoMetricsEmitted","parameterTypes":[] }, {"name":"exceptionWhenTooManyDimensionsSet","parameterTypes":[] }, {"name":"metricsPublishedEvenHandlerThrowsException","parameterTypes":[] }, {"name":"metricsWithColdStart","parameterTypes":[] }, {"name":"metricsWithDefaultDimensionSpecified","parameterTypes":[] }, {"name":"metricsWithDefaultNoDimensionSpecified","parameterTypes":[] }, {"name":"metricsWithStreamHandler","parameterTypes":[] }, {"name":"metricsWithoutColdStart","parameterTypes":[] }, {"name":"noColdStartMetricsWhenColdStartDone","parameterTypes":[] }, {"name":"noExceptionWhenNoMetricsEmitted","parameterTypes":[] }, {"name":"setUp","parameterTypes":[] }, {"name":"tearDown","parameterTypes":[] }] -}, -{ - "name":"sun.reflect.ReflectionFactory", - "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] -} + { + "name": "com.amazonaws.services.lambda.runtime.Context", + "allDeclaredClasses": true, + "queryAllPublicMethods": true + }, + { + "name": "com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "java.io.Serializable", + "queryAllDeclaredMethods": true + }, + { + "name": "java.lang.Comparable", + "queryAllDeclaredMethods": true + }, + { + "name": "java.lang.Double", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "java.lang.Number", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true + }, + { + "name": "java.lang.ProcessEnvironment", + "fields": [{ "name": "theCaseInsensitiveEnvironment" }, { "name": "theEnvironment" }] + }, + { + "name": "java.lang.String" + }, + { + "name": "java.lang.constant.Constable", + "queryAllDeclaredMethods": true + }, + { + "name": "java.lang.constant.ConstantDesc", + "queryAllDeclaredMethods": true + }, + { + "name": "java.util.Collections$UnmodifiableMap", + "fields": [{ "name": "m" }] + }, + { + "name": "java.util.Map" + }, + { + "name": "java.util.concurrent.atomic.AtomicBoolean", + "fields": [{ "name": "value" }] + }, + { + "name": "java.util.concurrent.atomic.AtomicReference", + "fields": [{ "name": "value" }] + }, + { + "name": "java.util.function.Consumer", + "queryAllPublicMethods": true + }, + { + "name": "org.apiguardian.api.API", + "queryAllPublicMethods": true + }, + { + "name": "software.amazon.cloudwatchlogs.emf.model.Metadata", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "getCloudWatchMetrics", "parameterTypes": [] }, + { "name": "getCustomMetadata", "parameterTypes": [] }, + { "name": "getTimestamp", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.model.MetricDefinition", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "getName", "parameterTypes": [] }, + { "name": "getStorageResolution", "parameterTypes": [] }, + { "name": "getUnit", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.model.MetricDirective", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "getAllDimensionKeys", "parameterTypes": [] }, + { "name": "getAllMetrics", "parameterTypes": [] }, + { "name": "getNamespace", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.model.RootNode", + "allDeclaredFields": true, + "queryAllDeclaredMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "getAws", "parameterTypes": [] }, + { "name": "getTargetMembers", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.serializers.InstantSerializer", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.serializers.StorageResolutionFilter", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.serializers.StorageResolutionSerializer", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "software.amazon.cloudwatchlogs.emf.serializers.UnitSerializer", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor", + "fields": [{ "name": "IS_COLD_START" }], + "methods": [{ "name": "resetServiceName", "parameterTypes": [] }] + }, + { + "name": "software.amazon.lambda.powertools.metrics.Metrics", + "allDeclaredClasses": true, + "queryAllPublicMethods": true + }, + { + "name": "software.amazon.lambda.powertools.metrics.MetricsFactory", + "fields": [{ "name": "metrics" }, { "name": "provider" }] + }, + { + "name": "software.amazon.lambda.powertools.metrics.internal.EmfMetricsLogger", + "methods": [ + { "name": "convertUnit", "parameterTypes": ["software.amazon.lambda.powertools.metrics.model.MetricUnit"] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.provider.MetricsProvider", + "allDeclaredClasses": true, + "queryAllPublicMethods": true + } ] diff --git a/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/resource-config.json b/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/resource-config.json index dd8fabec3..d47298855 100644 --- a/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/resource-config.json +++ b/powertools-metrics/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-metrics/resource-config.json @@ -1,19 +1,13 @@ { - "resources":{ - "includes":[{ - "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" - }, { - "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" - }, { - "pattern":"\\QMETA-INF/services/org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory\\E" - }, { - "pattern":"\\QMETA-INF/services/org.assertj.core.configuration.Configuration\\E" - }, { - "pattern":"\\QMETA-INF/services/org.assertj.core.presentation.Representation\\E" - }, { - "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" - }, { - "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" - }]}, - "bundles":[] + "resources": { + "includes": [ + { + "pattern": "\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, + { + "pattern": "\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + } + ] + }, + "bundles": [] } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/ConfigurationPrecedenceTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/ConfigurationPrecedenceTest.java new file mode 100644 index 000000000..1bf3b6a69 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/ConfigurationPrecedenceTest.java @@ -0,0 +1,206 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.SetEnvironmentVariable; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.metrics.testutils.TestContext; + +/** + * Tests to verify the hierarchy of precedence for configuration: + * 1. @FlushMetrics annotation + * 2. MetricsBuilder + * 3. Environment variables + */ +class ConfigurationPrecedenceTest { + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() throws Exception { + System.setOut(new PrintStream(outputStreamCaptor)); + + // Reset LambdaHandlerProcessor's SERVICE_NAME + Method resetServiceName = LambdaHandlerProcessor.class.getDeclaredMethod("resetServiceName"); + resetServiceName.setAccessible(true); + resetServiceName.invoke(null); + + // Reset IS_COLD_START + java.lang.reflect.Field coldStartField = LambdaHandlerProcessor.class.getDeclaredField("IS_COLD_START"); + coldStartField.setAccessible(true); + coldStartField.set(null, null); + } + + @AfterEach + void tearDown() throws Exception { + System.setOut(standardOut); + + // Reset the singleton state between tests + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); + field.setAccessible(true); + field.set(null, null); + + field = MetricsFactory.class.getDeclaredField("provider"); + field.setAccessible(true); + field.set(null, new software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider()); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "EnvNamespace") + @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = "EnvService") + void annotationShouldOverrideBuilderAndEnvironment() throws Exception { + // Given + // Configure with builder first + MetricsBuilder.builder() + .withNamespace("BuilderNamespace") + .withService("BuilderService") + .build(); + + RequestHandler, String> handler = new HandlerWithMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // Annotation values should take precedence + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("AnnotationNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("AnnotationService"); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "EnvNamespace") + @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = "EnvService") + void builderShouldOverrideEnvironment() throws Exception { + // Given + // Configure with builder + MetricsBuilder.builder() + .withNamespace("BuilderNamespace") + .withService("BuilderService") + .build(); + + RequestHandler, String> handler = new HandlerWithDefaultMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // Builder values should take precedence over environment variables + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("BuilderNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("BuilderService"); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "EnvNamespace") + @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = "EnvService") + void environmentVariablesShouldBeUsedWhenNoOverrides() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithDefaultMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // Environment variable values should be used + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("EnvNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("EnvService"); + } + + @Test + void shouldUseDefaultsWhenNoConfiguration() throws Exception { + // Given + MetricsBuilder.builder() + .withNamespace("TestNamespace") + .build(); + + RequestHandler, String> handler = new HandlerWithDefaultMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // Default values should be used + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("TestNamespace"); + // Service dimension should not be present when service is undefined + assertThat(rootNode.has("Service")).isFalse(); + } + + private static class HandlerWithMetricsAnnotation implements RequestHandler, String> { + @Override + @FlushMetrics(namespace = "AnnotationNamespace", service = "AnnotationService") + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } + + private static class HandlerWithDefaultMetricsAnnotation implements RequestHandler, String> { + @Override + @FlushMetrics + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } + +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsBuilderTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsBuilderTest.java new file mode 100644 index 000000000..bd300fb6b --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsBuilderTest.java @@ -0,0 +1,186 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; +import software.amazon.lambda.powertools.metrics.testutils.TestMetrics; +import software.amazon.lambda.powertools.metrics.testutils.TestMetricsProvider; + +class MetricsBuilderTest { + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @AfterEach + void tearDown() throws Exception { + System.setOut(standardOut); + + // Reset the singleton state between tests + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); + field.setAccessible(true); + field.set(null, null); + + field = MetricsFactory.class.getDeclaredField("provider"); + field.setAccessible(true); + field.set(null, new software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider()); + } + + @Test + void shouldBuildWithCustomNamespace() throws Exception { + // When + Metrics metrics = MetricsBuilder.builder() + .withNamespace("CustomNamespace") + .build(); + + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("CustomNamespace"); + } + + @Test + void shouldBuildWithCustomService() throws Exception { + // When + Metrics metrics = MetricsBuilder.builder() + .withService("CustomService") + .withNamespace("TestNamespace") + .build(); + + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("CustomService"); + } + + @Test + void shouldBuildWithRaiseOnEmptyMetrics() { + // When + Metrics metrics = MetricsBuilder.builder() + .withRaiseOnEmptyMetrics(true) + .withNamespace("TestNamespace") + .build(); + + // Then + assertThat(metrics).isNotNull(); + assertThatThrownBy(metrics::flush) + .isInstanceOf(IllegalStateException.class) + .hasMessage("No metrics were emitted"); + } + + @Test + void shouldBuildWithDefaultDimension() throws Exception { + // When + Metrics metrics = MetricsBuilder.builder() + .withDefaultDimension("Environment", "Test") + .withNamespace("TestNamespace") + .build(); + + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Environment")).isTrue(); + assertThat(rootNode.get("Environment").asText()).isEqualTo("Test"); + } + + @Test + void shouldBuildWithMultipleDefaultDimensions() throws Exception { + // When + Metrics metrics = MetricsBuilder.builder() + .withDefaultDimensions(DimensionSet.of("Environment", "Test", "Region", "us-west-2")) + .withNamespace("TestNamespace") + .build(); + + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Environment")).isTrue(); + assertThat(rootNode.get("Environment").asText()).isEqualTo("Test"); + assertThat(rootNode.has("Region")).isTrue(); + assertThat(rootNode.get("Region").asText()).isEqualTo("us-west-2"); + } + + @Test + void shouldBuildWithCustomMetricsProvider() { + // Given + MetricsProvider testProvider = new TestMetricsProvider(); + + // When + Metrics metrics = MetricsBuilder.builder() + .withMetricsProvider(testProvider) + .build(); + + // Then + assertThat(metrics).isInstanceOf(TestMetrics.class); + } + + @Test + void shouldOverrideServiceWithDefaultDimensions() throws Exception { + // When + Metrics metrics = MetricsBuilder.builder() + .withService("OriginalService") + .withDefaultDimensions(DimensionSet.of("Service", "OverriddenService")) + .withNamespace("TestNamespace") + .build(); + + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("OverriddenService"); + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsFactoryTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsFactoryTest.java new file mode 100644 index 000000000..962f2c2d7 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsFactoryTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.SetEnvironmentVariable; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; +import software.amazon.lambda.powertools.metrics.testutils.TestMetrics; +import software.amazon.lambda.powertools.metrics.testutils.TestMetricsProvider; + +class MetricsFactoryTest { + + private static final String TEST_NAMESPACE = "TestNamespace"; + private static final String TEST_SERVICE = "TestService"; + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() throws Exception { + System.setOut(new PrintStream(outputStreamCaptor)); + + // Reset LambdaHandlerProcessor's SERVICE_NAME + Method resetServiceName = LambdaHandlerProcessor.class.getDeclaredMethod("resetServiceName"); + resetServiceName.setAccessible(true); + resetServiceName.invoke(null); + + // Reset IS_COLD_START + java.lang.reflect.Field coldStartField = LambdaHandlerProcessor.class.getDeclaredField("IS_COLD_START"); + coldStartField.setAccessible(true); + coldStartField.set(null, null); + } + + @AfterEach + void tearDown() throws Exception { + System.setOut(standardOut); + + // Reset the singleton state between tests + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); + field.setAccessible(true); + field.set(null, null); + + field = MetricsFactory.class.getDeclaredField("provider"); + field.setAccessible(true); + field.set(null, new software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider()); + } + + @Test + void shouldGetMetricsInstance() { + // When + Metrics metrics = MetricsFactory.getMetricsInstance(); + + // Then + assertThat(metrics).isNotNull(); + } + + @Test + void shouldReturnSameInstanceOnMultipleCalls() { + // When + Metrics firstInstance = MetricsFactory.getMetricsInstance(); + Metrics secondInstance = MetricsFactory.getMetricsInstance(); + + // Then + assertThat(firstInstance).isSameAs(secondInstance); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = TEST_NAMESPACE) + void shouldUseNamespaceFromEnvironmentVariable() throws Exception { + // When + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo(TEST_NAMESPACE); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = TEST_SERVICE) + void shouldUseServiceNameFromEnvironmentVariable() throws Exception { + // When + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.setNamespace("TestNamespace"); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo(TEST_SERVICE); + } + + @Test + void shouldSetCustomMetricsProvider() { + // Given + MetricsProvider testProvider = new TestMetricsProvider(); + + // When + MetricsFactory.setMetricsProvider(testProvider); + Metrics metrics = MetricsFactory.getMetricsInstance(); + + // Then + assertThat(metrics).isInstanceOf(TestMetrics.class); + } + + @Test + void shouldThrowExceptionWhenSettingNullProvider() { + // When/Then + assertThatThrownBy(() -> MetricsFactory.setMetricsProvider(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metrics provider cannot be null"); + } + + @Test + void shouldNotSetServiceDimensionWhenServiceUndefined() throws Exception { + // Given - no POWERTOOLS_SERVICE_NAME set, so it will use the default undefined value + + // When + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.setNamespace("TestNamespace"); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // Service dimension should not be present + assertThat(rootNode.has("Service")).isFalse(); + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java deleted file mode 100644 index 5f99c950a..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package software.amazon.lambda.powertools.metrics; - -import static java.util.Collections.emptyMap; -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.function.Consumer; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junitpioneer.jupiter.SetEnvironmentVariable; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.StorageResolution; -import software.amazon.cloudwatchlogs.emf.model.Unit; - -@SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") -class MetricsLoggerTest { - - 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); - } - - @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - @SetEnvironmentVariable(key = "_X_AMZN_TRACE_ID", value = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1") - void singleMetricsCaptureUtilityWithDefaultDimension() { - MetricsUtils.defaultDimensions(DimensionSet.of("Service", "Booking")); - - MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, "test", - metricsLogger -> - { - }); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "Booking") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - }); - } - - @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - @SetEnvironmentVariable(key = "_X_AMZN_TRACE_ID", value = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1") - void singleMetricsCaptureUtility() { - MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, "test", - metricsLogger -> metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1"))); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=test"); - }); - } - - @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - void singleMetricsCaptureUtilityWithNullNamespace() { - // POWERTOOLS_METRICS_NAMESPACE is not defined - - MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, - metricsLogger -> metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1"))); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=aws-embedded-metrics"); - }); - } - - @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "GlobalName") - @SetEnvironmentVariable(key = "_X_AMZN_TRACE_ID", value = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1") - void singleMetricsCaptureUtilityWithDefaultNameSpace() { - MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, - metricsLogger -> metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1"))); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=GlobalName"); - }); - } - - @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "GlobalName") - @SetEnvironmentVariable(key = "_X_AMZN_TRACE_ID", value = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1") - void metricsLoggerCaptureUtilityWithDefaultNameSpace() { - testLogger(MetricsUtils::withMetricsLogger); - } - - @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "GlobalName") - @SetEnvironmentVariable(key = "_X_AMZN_TRACE_ID", value = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1") - void shouldUseTraceIdFromSystemPropertyIfEnvVarNotPresent() { - MetricsUtils.withSingleMetric("Metric1", 1, Unit.COUNT, - metricsLogger -> metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1"))); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=GlobalName"); - }); - } - - private void testLogger(Consumer> methodToTest) { - methodToTest.accept(metricsLogger -> - { - metricsLogger.setDimensions(DimensionSet.of("Dimension1", "Value1")); - metricsLogger.putMetric("Metric1", 1, Unit.COUNT); - metricsLogger.putMetric("Metric2", 1, Unit.COUNT, StorageResolution.HIGH); - }); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=GlobalName"); - - ArrayList cloudWatchMetrics = (ArrayList) aws.get("CloudWatchMetrics"); - LinkedHashMap values = - (java.util.LinkedHashMap) cloudWatchMetrics.get(0); - ArrayList metricArray = (ArrayList) values.get("Metrics"); - LinkedHashMap metricValues = (LinkedHashMap) metricArray.get(1); - assertThat(metricValues).containsEntry("StorageResolution", 1); - }); - } - - private Map readAsJson(String s) { - try { - return mapper.readValue(s, Map.class); - } catch (JsonProcessingException e) { - e.printStackTrace(); - } - return emptyMap(); - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java deleted file mode 100644 index e3a0fa22e..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.Unit; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsColdStartEnabledHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking", captureColdStart = true) - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("Metric1", 1, Unit.BYTES); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultDimensionHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultDimensionHandler.java deleted file mode 100644 index 761c20caa..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultDimensionHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.defaultDimensions; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsEnabledDefaultDimensionHandler implements RequestHandler { - - static { - defaultDimensions(DimensionSet.of("CustomDimension", "booking")); - } - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("Metric1", 1, Unit.BYTES); - - withSingleMetric("Metric2", 1, Unit.COUNT, log -> - { - }); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultNoDimensionHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultNoDimensionHandler.java deleted file mode 100644 index d968f94f5..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultNoDimensionHandler.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.Unit; -import software.amazon.lambda.powertools.metrics.Metrics; -import software.amazon.lambda.powertools.metrics.MetricsUtils; - -public class PowertoolsMetricsEnabledDefaultNoDimensionHandler implements RequestHandler { - - static { - MetricsUtils.defaultDimensions(); - } - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("Metric1", 1, Unit.BYTES); - - withSingleMetric("Metric2", 1, Unit.COUNT, log -> - { - }); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java deleted file mode 100644 index 7cfee533d..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; -import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.cloudwatchlogs.emf.model.Unit; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsEnabledHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("Metric1", 1, Unit.BYTES); - - - withSingleMetric("Metric2", 1, Unit.COUNT, - log -> log.setDimensions(DimensionSet.of("Dimension1", "Value1"))); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java deleted file mode 100644 index 1600f4a64..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import java.io.InputStream; -import java.io.OutputStream; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.Unit; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsEnabledStreamHandler implements RequestStreamHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public void handleRequest(InputStream input, OutputStream output, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("Metric1", 1, Unit.BYTES); - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java deleted file mode 100644 index 42e0b3ad4..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsExceptionWhenNoMetricsHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking", raiseOnEmptyMetrics = true) - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetadata("MetaData", "MetaDataValue"); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java deleted file mode 100644 index 04b02e166..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsNoDimensionsHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("CoolMetric", 1); - metricsLogger.setDimensions(new DimensionSet()); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java deleted file mode 100644 index c08ce2f86..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsNoExceptionWhenNoMetricsHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetadata("MetaData", "MetaDataValue"); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java deleted file mode 100644 index fd406b9cd..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import java.util.stream.IntStream; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.cloudwatchlogs.emf.model.DimensionSet; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsTooManyDimensionsHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication",service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - DimensionSet dimensionSet = new DimensionSet(); - for (int i = 0; i < 35; i++) { - dimensionSet.addDimension("Dimension" + i, "value" + i); - } - metricsLogger.setDimensions(dimensionSet); - - return null; - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsWithExceptionInHandler.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsWithExceptionInHandler.java deleted file mode 100644 index da9028a70..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsWithExceptionInHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2023 Amazon.com, Inc. or its affiliates. - * Licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package software.amazon.lambda.powertools.metrics.handlers; - -import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.lambda.powertools.metrics.Metrics; - -public class PowertoolsMetricsWithExceptionInHandler implements RequestHandler { - - @Override - @Metrics(namespace = "ExampleApplication", service = "booking") - public Object handleRequest(Object input, Context context) { - MetricsLogger metricsLogger = metricsLogger(); - metricsLogger.putMetric("CoolMetric", 1); - throw new IllegalStateException("Whoops, unexpected exception"); - } -} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLoggerTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLoggerTest.java new file mode 100644 index 000000000..1b7106ece --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLoggerTest.java @@ -0,0 +1,521 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.Instant; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junitpioneer.jupiter.SetEnvironmentVariable; + +import com.amazonaws.services.lambda.runtime.Context; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.cloudwatchlogs.emf.model.Unit; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.metrics.testutils.TestContext; + +class EmfMetricsLoggerTest { + + private Metrics metrics; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + + @BeforeEach + void setUp() throws Exception { + // Reset LambdaHandlerProcessor's SERVICE_NAME + Method resetServiceName = LambdaHandlerProcessor.class.getDeclaredMethod("resetServiceName"); + resetServiceName.setAccessible(true); + resetServiceName.invoke(null); + + // Reset IS_COLD_START + java.lang.reflect.Field coldStartField = LambdaHandlerProcessor.class.getDeclaredField("IS_COLD_START"); + coldStartField.setAccessible(true); + coldStartField.set(null, null); + + metrics = MetricsFactory.getMetricsInstance(); + metrics.setNamespace("TestNamespace"); + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @AfterEach + void tearDown() throws Exception { + System.setOut(standardOut); + + // Reset the singleton state between tests + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); + field.setAccessible(true); + field.set(null, null); + } + + @ParameterizedTest + @MethodSource("unitConversionTestCases") + void shouldConvertMetricUnits(MetricUnit inputUnit, Unit expectedUnit) throws Exception { + // Given + // We access using reflection here for simplicity (even though this is not best practice) + Method convertUnitMethod = EmfMetricsLogger.class.getDeclaredMethod("convertUnit", MetricUnit.class); + convertUnitMethod.setAccessible(true); + + // When + Unit actualUnit = (Unit) convertUnitMethod.invoke(metrics, inputUnit); + + // Then + assertThat(actualUnit).isEqualTo(expectedUnit); + } + + private static Stream unitConversionTestCases() { + return Stream.of( + Arguments.of(MetricUnit.SECONDS, Unit.SECONDS), + Arguments.of(MetricUnit.MICROSECONDS, Unit.MICROSECONDS), + Arguments.of(MetricUnit.MILLISECONDS, Unit.MILLISECONDS), + Arguments.of(MetricUnit.BYTES, Unit.BYTES), + Arguments.of(MetricUnit.KILOBYTES, Unit.KILOBYTES), + Arguments.of(MetricUnit.MEGABYTES, Unit.MEGABYTES), + Arguments.of(MetricUnit.GIGABYTES, Unit.GIGABYTES), + Arguments.of(MetricUnit.TERABYTES, Unit.TERABYTES), + Arguments.of(MetricUnit.BITS, Unit.BITS), + Arguments.of(MetricUnit.KILOBITS, Unit.KILOBITS), + Arguments.of(MetricUnit.MEGABITS, Unit.MEGABITS), + Arguments.of(MetricUnit.GIGABITS, Unit.GIGABITS), + Arguments.of(MetricUnit.TERABITS, Unit.TERABITS), + Arguments.of(MetricUnit.PERCENT, Unit.PERCENT), + Arguments.of(MetricUnit.COUNT, Unit.COUNT), + Arguments.of(MetricUnit.BYTES_SECOND, Unit.BYTES_SECOND), + Arguments.of(MetricUnit.KILOBYTES_SECOND, Unit.KILOBYTES_SECOND), + Arguments.of(MetricUnit.MEGABYTES_SECOND, Unit.MEGABYTES_SECOND), + Arguments.of(MetricUnit.GIGABYTES_SECOND, Unit.GIGABYTES_SECOND), + Arguments.of(MetricUnit.TERABYTES_SECOND, Unit.TERABYTES_SECOND), + Arguments.of(MetricUnit.BITS_SECOND, Unit.BITS_SECOND), + Arguments.of(MetricUnit.KILOBITS_SECOND, Unit.KILOBITS_SECOND), + Arguments.of(MetricUnit.MEGABITS_SECOND, Unit.MEGABITS_SECOND), + Arguments.of(MetricUnit.GIGABITS_SECOND, Unit.GIGABITS_SECOND), + Arguments.of(MetricUnit.TERABITS_SECOND, Unit.TERABITS_SECOND), + Arguments.of(MetricUnit.COUNT_SECOND, Unit.COUNT_SECOND), + Arguments.of(MetricUnit.NONE, Unit.NONE)); + } + + @Test + void shouldCreateMetricWithDefaultResolution() throws Exception { + // When + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("_aws")).isTrue(); + assertThat(rootNode.get("test-metric").asDouble()).isEqualTo(100); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Metrics").get(0).get("Unit").asText()) + .isEqualTo("Count"); + } + + @Test + void shouldCreateMetricWithHighResolution() throws Exception { + // When + metrics.addMetric("test-metric", 100, MetricUnit.COUNT, MetricResolution.HIGH); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("_aws")).isTrue(); + assertThat(rootNode.get("test-metric").asDouble()).isEqualTo(100); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Metrics").get(0).get("Unit").asText()) + .isEqualTo("Count"); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Metrics").get(0).get("StorageResolution") + .asInt()).isEqualTo(1); + } + + @Test + void shouldAddDimension() throws Exception { + // When + metrics.clearDefaultDimensions(); // Clear default Service dimension first for easier assertions + metrics.addDimension("CustomDimension", "CustomValue"); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("CustomDimension")).isTrue(); + assertThat(rootNode.get("CustomDimension").asText()).isEqualTo("CustomValue"); + + // Check that the dimension is in the CloudWatchMetrics section + JsonNode dimensions = rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Dimensions").get(0); + boolean hasDimension = false; + for (JsonNode dimension : dimensions) { + if (dimension.asText().equals("CustomDimension")) { + hasDimension = true; + break; + } + } + assertThat(hasDimension).isTrue(); + } + + @Test + void shouldSetCustomTimestamp() throws Exception { + // Given + Instant customTimestamp = Instant.now(); + + // When + metrics.setTimestamp(customTimestamp); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("_aws")).isTrue(); + assertThat(rootNode.get("_aws").has("Timestamp")).isTrue(); + assertThat(rootNode.get("_aws").get("Timestamp").asLong()).isEqualTo(customTimestamp.toEpochMilli()); + } + + @Test + void shouldAddDimensionSet() throws Exception { + // Given + DimensionSet dimensionSet = DimensionSet.of("Dim1", "Value1", "Dim2", "Value2"); + + // When + metrics.clearDefaultDimensions(); // Clear default Service dimension first for easier assertions + metrics.addDimension(dimensionSet); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Dim1")).isTrue(); + assertThat(rootNode.get("Dim1").asText()).isEqualTo("Value1"); + assertThat(rootNode.has("Dim2")).isTrue(); + assertThat(rootNode.get("Dim2").asText()).isEqualTo("Value2"); + + // Check that the dimensions are in the CloudWatchMetrics section + JsonNode dimensions = rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Dimensions").get(0); + boolean hasDim1 = false; + boolean hasDim2 = false; + for (JsonNode dimension : dimensions) { + String dimName = dimension.asText(); + if (dimName.equals("Dim1")) { + hasDim1 = true; + } else if (dimName.equals("Dim2")) { + hasDim2 = true; + } + } + assertThat(hasDim1).isTrue(); + assertThat(hasDim2).isTrue(); + } + + @Test + void shouldThrowExceptionWhenDimensionSetIsNull() { + // When/Then + assertThatThrownBy(() -> metrics.addDimension((DimensionSet) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("DimensionSet cannot be null"); + } + + @Test + void shouldAddMetadata() throws Exception { + // When + metrics.addMetadata("CustomMetadata", "MetadataValue"); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // The metadata is added to the _aws section in the EMF output + assertThat(rootNode.get("_aws").has("CustomMetadata")).isTrue(); + assertThat(rootNode.get("_aws").get("CustomMetadata").asText()).isEqualTo("MetadataValue"); + } + + @Test + void shouldSetDefaultDimensions() throws Exception { + // Given + DimensionSet dimensionSet = DimensionSet.of("Service", "TestService", "Environment", "Test"); + + // When + metrics.setDefaultDimensions(dimensionSet); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("TestService"); + assertThat(rootNode.has("Environment")).isTrue(); + assertThat(rootNode.get("Environment").asText()).isEqualTo("Test"); + } + + @Test + void shouldGetDefaultDimensions() { + // Given + DimensionSet dimensionSet = DimensionSet.of("Service", "TestService", "Environment", "Test"); + + // When + metrics.setDefaultDimensions(dimensionSet); + DimensionSet dimensions = metrics.getDefaultDimensions(); + + // Then + assertThat(dimensions.getDimensions()).containsEntry("Service", "TestService"); + assertThat(dimensions.getDimensions()).containsEntry("Environment", "Test"); + } + + @Test + void shouldThrowExceptionWhenDefaultDimensionSetIsNull() { + // When/Then + assertThatThrownBy(() -> metrics.setDefaultDimensions((DimensionSet) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("DimensionSet cannot be null"); + } + + @Test + void shouldSetNamespace() throws Exception { + // When + metrics.setNamespace("CustomNamespace"); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("CustomNamespace"); + } + + @Test + void shouldRaiseExceptionOnEmptyMetrics() { + // When + metrics.setRaiseOnEmptyMetrics(true); + + // Then + assertThatThrownBy(() -> metrics.flush()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("No metrics were emitted"); + } + + @Test + void shouldLogWarningOnEmptyMetrics() throws Exception { + // Given + File logFile = new File("target/metrics-test.log"); + + // When + // Flushing without adding metrics + metrics.flush(); + + // Then + // Read the log file and check for the warning + String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8); + assertThat(logContent).contains("No metrics were emitted"); + } + + @Test + void shouldClearDefaultDimensions() throws Exception { + // Given + metrics.setDefaultDimensions(DimensionSet.of("Service", "TestService", "Environment", "Test")); + + // When + metrics.clearDefaultDimensions(); + metrics.addMetric("test-metric", 100); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("Service")).isFalse(); + assertThat(rootNode.has("Environment")).isFalse(); + } + + @Test + void shouldCaptureColdStartMetric() throws Exception { + // Given + Context testContext = new TestContext(); + + // When + metrics.captureColdStartMetric(testContext); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("ColdStart")).isTrue(); + assertThat(rootNode.get("ColdStart").asDouble()).isEqualTo(1.0); + assertThat(rootNode.has("function_request_id")).isTrue(); + assertThat(rootNode.get("function_request_id").asText()).isEqualTo(testContext.getAwsRequestId()); + } + + @Test + void shouldCaptureColdStartMetricWithDimensions() throws Exception { + // Given + DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); + + // When + metrics.captureColdStartMetric(dimensions); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("ColdStart")).isTrue(); + assertThat(rootNode.get("ColdStart").asDouble()).isEqualTo(1.0); + assertThat(rootNode.has("CustomDim")).isTrue(); + assertThat(rootNode.get("CustomDim").asText()).isEqualTo("CustomValue"); + } + + @Test + void shouldCaptureColdStartMetricWithoutDimensions() throws Exception { + // When + metrics.captureColdStartMetric(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("ColdStart")).isTrue(); + assertThat(rootNode.get("ColdStart").asDouble()).isEqualTo(1.0); + } + + @Test + void shouldReuseNamespaceForColdStartMetric() throws Exception { + // Given + String customNamespace = "CustomNamespace"; + metrics.setNamespace(customNamespace); + + Context testContext = new TestContext(); + + DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); + + // When + metrics.captureColdStartMetric(testContext, dimensions); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("ColdStart")).isTrue(); + assertThat(rootNode.get("ColdStart").asDouble()).isEqualTo(1.0); + assertThat(rootNode.has("CustomDim")).isTrue(); + assertThat(rootNode.get("CustomDim").asText()).isEqualTo("CustomValue"); + assertThat(rootNode.has("function_request_id")).isTrue(); + assertThat(rootNode.get("function_request_id").asText()).isEqualTo(testContext.getAwsRequestId()); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo(customNamespace); + } + + @Test + void shouldFlushSingleMetric() throws Exception { + // Given + DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); + + // When + metrics.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace", dimensions); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("single-metric")).isTrue(); + assertThat(rootNode.get("single-metric").asDouble()).isEqualTo(200.0); + assertThat(rootNode.has("CustomDim")).isTrue(); + assertThat(rootNode.get("CustomDim").asText()).isEqualTo("CustomValue"); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("SingleNamespace"); + } + + @Test + void shouldFlushSingleMetricWithoutDimensions() throws Exception { + // When + metrics.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace"); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("single-metric")).isTrue(); + assertThat(rootNode.get("single-metric").asDouble()).isEqualTo(200.0); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("SingleNamespace"); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_DISABLED", value = "true") + void shouldNotFlushMetricsWhenDisabled() { + // When + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + assertThat(emfOutput).isEmpty(); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_DISABLED", value = "true") + void shouldNotCaptureColdStartMetricWhenDisabled() { + // Given + Context testContext = new TestContext(); + + // When + metrics.captureColdStartMetric(testContext); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + assertThat(emfOutput).isEmpty(); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_DISABLED", value = "true") + void shouldNotFlushSingleMetricWhenDisabled() { + // Given + DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); + + // When + metrics.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace", dimensions); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + assertThat(emfOutput).isEmpty(); + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java index 5df6003c8..068d19ccb 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java @@ -14,371 +14,315 @@ package software.amazon.lambda.powertools.metrics.internal; -import static java.util.Collections.emptyMap; -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.SetEnvironmentVariable; -import org.mockito.Mock; 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.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import software.amazon.cloudwatchlogs.emf.exception.DimensionSetExceededException; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; -import software.amazon.lambda.powertools.metrics.MetricsUtils; -import software.amazon.lambda.powertools.metrics.ValidationException; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsColdStartEnabledHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledDefaultDimensionHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledDefaultNoDimensionHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsEnabledStreamHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsExceptionWhenNoMetricsHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsNoDimensionsHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsNoExceptionWhenNoMetricsHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsTooManyDimensionsHandler; -import software.amazon.lambda.powertools.metrics.handlers.PowertoolsMetricsWithExceptionInHandler; - -@SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") -public class LambdaMetricsAspectTest { - private final ByteArrayOutputStream out = new ByteArrayOutputStream(); - private final PrintStream originalOut = System.out; - private final ObjectMapper mapper = new ObjectMapper(); - @Mock - private Context context; - private RequestHandler requestHandler; +import software.amazon.lambda.powertools.metrics.FlushMetrics; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.metrics.testutils.TestContext; + +class LambdaMetricsAspectTest { + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach - void setUp() throws IllegalAccessException { - openMocks(this); - setupContext(); - writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); - System.setOut(new PrintStream(out)); + void setUp() throws Exception { + System.setOut(new PrintStream(outputStreamCaptor)); + + // Reset LambdaHandlerProcessor's SERVICE_NAME + Method resetServiceName = LambdaHandlerProcessor.class.getDeclaredMethod("resetServiceName"); + resetServiceName.setAccessible(true); + resetServiceName.invoke(null); + + // Reset IS_COLD_START + java.lang.reflect.Field coldStartField = LambdaHandlerProcessor.class.getDeclaredField("IS_COLD_START"); + coldStartField.setAccessible(true); + coldStartField.set(null, null); } @AfterEach - void tearDown() { - System.setOut(originalOut); - } + void tearDown() throws Exception { + System.setOut(standardOut); - @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - @SetEnvironmentVariable(key = "_X_AMZN_TRACE_ID", value = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1") - public void metricsWithoutColdStart() { - - MetricsUtils.defaultDimensions(null); - requestHandler = new PowertoolsMetricsEnabledHandler(); - requestHandler.handleRequest("input", context); - - assertThat(out.toString().split("\n")) - .hasSize(2) - .satisfies(s -> - { - Map logAsJson = readAsJson(s[0]); - - assertThat(logAsJson) - .containsEntry("Metric2", 1.0) - .containsEntry("Dimension1", "Value1") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793") - .containsEntry("function_request_id", "123ABC"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=ExampleApplication"); - - logAsJson = readAsJson(s[1]); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + // Reset the singleton state between tests + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); + field.setAccessible(true); + field.set(null, null); } @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - @SetEnvironmentVariable(key = "_X_AMZN_TRACE_ID", value = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1") - public void metricsWithDefaultDimensionSpecified() { - requestHandler = new PowertoolsMetricsEnabledDefaultDimensionHandler(); - - requestHandler.handleRequest("input", context); - - assertThat(out.toString().split("\n")) - .hasSize(2) - .satisfies(s -> - { - Map logAsJson = readAsJson(s[0]); - - assertThat(logAsJson) - .containsEntry("Metric2", 1.0) - .containsEntry("CustomDimension", "booking") - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793") - .containsEntry("function_request_id", "123ABC"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=ExampleApplication"); - - logAsJson = readAsJson(s[1]); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("CustomDimension", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + void shouldCaptureMetricsFromAnnotatedHandler() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("test-metric")).isTrue(); + assertThat(rootNode.get("test-metric").asDouble()).isEqualTo(100.0); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("CustomNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("CustomService"); } @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - @SetEnvironmentVariable(key = "_X_AMZN_TRACE_ID", value = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1") - public void metricsWithDefaultNoDimensionSpecified() { - requestHandler = new PowertoolsMetricsEnabledDefaultNoDimensionHandler(); - - requestHandler.handleRequest("input", context); - - assertThat(out.toString().split("\n")) - .hasSize(2) - .satisfies(s -> - { - Map logAsJson = readAsJson(s[0]); - - assertThat(logAsJson) - .containsEntry("Metric2", 1.0) - .containsKey("_aws") - .containsEntry("xray_trace_id", "1-5759e988-bd862e3fe1be46a994272793") - .containsEntry("function_request_id", "123ABC"); - - Map aws = (Map) logAsJson.get("_aws"); - - assertThat(aws.get("CloudWatchMetrics")) - .asString() - .contains("Namespace=ExampleApplication"); - - logAsJson = readAsJson(s[1]); - - assertThat(logAsJson) - .containsEntry("Metric1", 1.0) - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "EnvNamespace") + @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = "EnvService") + void shouldOverrideEnvironmentVariablesWithAnnotation() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("CustomNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("CustomService"); } @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - public void metricsWithColdStart() { - MetricsUtils.defaultDimensions(null); - requestHandler = new PowertoolsMetricsColdStartEnabledHandler(); - - 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") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - - logAsJson = readAsJson(s[1]); - - assertThat(logAsJson) - .doesNotContainKey("ColdStart") - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = "EnvNamespace") + @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = "EnvService") + void shouldUseEnvironmentVariablesWhenNoAnnotationOverrides() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithDefaultMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("EnvNamespace"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("EnvService"); } @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - public void noColdStartMetricsWhenColdStartDone() { - MetricsUtils.defaultDimensions(null); - requestHandler = new PowertoolsMetricsColdStartEnabledHandler(); - - requestHandler.handleRequest("input", context); - requestHandler.handleRequest("input", context); - - assertThat(out.toString().split("\n")) - .hasSize(3) - .satisfies(s -> - { - Map logAsJson = readAsJson(s[0]); - - assertThat(logAsJson) - .doesNotContainKey("Metric1") - .containsEntry("ColdStart", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - - logAsJson = readAsJson(s[1]); - - assertThat(logAsJson) - .doesNotContainKey("ColdStart") - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - - logAsJson = readAsJson(s[2]); - - assertThat(logAsJson) - .doesNotContainKey("ColdStart") - .containsEntry("Metric1", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + void shouldCaptureColdStartMetricWhenConfigured() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithColdStartMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + String[] emfOutputs = emfOutput.split("\\n"); + + // There should be two EMF outputs - one for cold start and one for the handler metrics + assertThat(emfOutputs).hasSize(2); + + JsonNode coldStartNode = objectMapper.readTree(emfOutputs[0]); + assertThat(coldStartNode.has("ColdStart")).isTrue(); + assertThat(coldStartNode.get("ColdStart").asDouble()).isEqualTo(1.0); + + JsonNode metricsNode = objectMapper.readTree(emfOutputs[1]); + assertThat(metricsNode.has("test-metric")).isTrue(); } @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - public void metricsWithStreamHandler() throws IOException { - MetricsUtils.defaultDimensions(null); - RequestStreamHandler streamHandler = new PowertoolsMetricsEnabledStreamHandler(); - - 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") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_FUNCTION_NAME", value = "EnvFunctionName") + void shouldNotIncludeServiceDimensionInColdStartMetricWhenServiceUndefined() throws Exception { + // Given - no service name set, so it will use the default undefined value + RequestHandler, String> handler = new HandlerWithColdStartMetricsAnnotation(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + String[] emfOutputs = emfOutput.split("\\n"); + + // There should be two EMF outputs - one for cold start and one for the handler metrics + assertThat(emfOutputs).hasSize(2); + + JsonNode coldStartNode = objectMapper.readTree(emfOutputs[0]); + assertThat(coldStartNode.has("ColdStart")).isTrue(); + assertThat(coldStartNode.get("ColdStart").asDouble()).isEqualTo(1.0); + + // Service dimension should not be present in cold start metrics + assertThat(coldStartNode.has("Service")).isFalse(); + + // FunctionName dimension should be present + assertThat(coldStartNode.has("FunctionName")).isTrue(); + assertThat(coldStartNode.get("FunctionName").asText()).isEqualTo("EnvFunctionName"); } @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - public void exceptionWhenNoMetricsEmitted() { - MetricsUtils.defaultDimensions(null); - requestHandler = new PowertoolsMetricsExceptionWhenNoMetricsHandler(); - - assertThatExceptionOfType(ValidationException.class) - .isThrownBy(() -> requestHandler.handleRequest("input", context)) - .withMessage("No metrics captured, at least one metrics must be emitted"); + void shouldUseCustomFunctionNameWhenProvidedForColdStartMetric() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithCustomFunctionName(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + String[] emfOutputs = emfOutput.split("\\n"); + + // There should be two EMF outputs - one for cold start and one for the handler metrics + assertThat(emfOutputs).hasSize(2); + + JsonNode coldStartNode = objectMapper.readTree(emfOutputs[0]); + assertThat(coldStartNode.has("FunctionName")).isTrue(); + assertThat(coldStartNode.get("FunctionName").asText()).isEqualTo("CustomFunction"); + + // Check that FunctionName is in the dimensions + JsonNode dimensions = coldStartNode.get("_aws").get("CloudWatchMetrics").get(0).get("Dimensions").get(0); + boolean hasFunctionName = false; + for (JsonNode dimension : dimensions) { + if (dimension.asText().equals("FunctionName")) { + hasFunctionName = true; + break; + } + } + assertThat(hasFunctionName).isTrue(); } @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - public void noExceptionWhenNoMetricsEmitted() { - MetricsUtils.defaultDimensions(null); - requestHandler = new PowertoolsMetricsNoExceptionWhenNoMetricsHandler(); - - requestHandler.handleRequest("input", context); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - - assertThat(logAsJson) - .containsEntry("Service", "booking") - .doesNotContainKey("_aws"); - }); + void shouldUseServiceNameWhenProvidedForColdStartMetric() throws Exception { + // Given + RequestHandler, String> handler = new HandlerWithServiceNameAndColdStart(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + // Should use the service name from annotation + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("CustomService"); } @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - public void allowWhenNoDimensionsSet() { - MetricsUtils.defaultDimensions(null); - - requestHandler = new PowertoolsMetricsNoDimensionsHandler(); - requestHandler.handleRequest("input", context); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - assertThat(logAsJson) - .containsEntry("CoolMetric", 1.0) - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + void shouldHaveNoEffectOnNonHandlerMethod() { + // Given + RequestHandler, String> handler = new HandlerWithAnnotationOnWrongMethod(); + Context context = new TestContext(); + Map input = new HashMap<>(); + + // When + handler.handleRequest(input, context); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + + // Should be empty because we do not flush any metrics manually + assertThat(emfOutput).isEmpty(); } - @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - public void exceptionWhenTooManyDimensionsSet() { - MetricsUtils.defaultDimensions(null); + static class HandlerWithMetricsAnnotation implements RequestHandler, String> { + @Override + @FlushMetrics(namespace = "CustomNamespace", service = "CustomService") + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } - requestHandler = new PowertoolsMetricsTooManyDimensionsHandler(); + static class HandlerWithDefaultMetricsAnnotation implements RequestHandler, String> { + @Override + @FlushMetrics + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } - assertThatExceptionOfType(DimensionSetExceededException.class) - .isThrownBy(() -> requestHandler.handleRequest("input", context)) - .withMessage( - "Maximum number of dimensions allowed are 30. Account for default dimensions if not using setDimensions."); + static class HandlerWithColdStartMetricsAnnotation implements RequestHandler, String> { + @Override + @FlushMetrics(captureColdStart = true, namespace = "TestNamespace") + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } } - @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - public void metricsPublishedEvenHandlerThrowsException() { - MetricsUtils.defaultDimensions(null); - requestHandler = new PowertoolsMetricsWithExceptionInHandler(); - - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> requestHandler.handleRequest("input", context)) - .withMessage("Whoops, unexpected exception"); - - assertThat(out.toString()) - .satisfies(s -> - { - Map logAsJson = readAsJson(s); - assertThat(logAsJson) - .containsEntry("CoolMetric", 1.0) - .containsEntry("Service", "booking") - .containsEntry("function_request_id", "123ABC") - .containsKey("_aws"); - }); + static class HandlerWithCustomFunctionName implements RequestHandler, String> { + @Override + @FlushMetrics(captureColdStart = true, functionName = "CustomFunction", namespace = "TestNamespace") + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } } - private void setupContext() { - when(context.getFunctionName()).thenReturn("testFunction"); - when(context.getInvokedFunctionArn()).thenReturn("testArn"); - when(context.getFunctionVersion()).thenReturn("1"); - when(context.getMemoryLimitInMB()).thenReturn(10); - when(context.getAwsRequestId()).thenReturn("123ABC"); + static class HandlerWithServiceNameAndColdStart implements RequestHandler, String> { + @Override + @FlushMetrics(service = "CustomService", captureColdStart = true, namespace = "TestNamespace") + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } } - private Map readAsJson(String s) { - try { - return mapper.readValue(s, Map.class); - } catch (JsonProcessingException e) { - e.printStackTrace(); + static class HandlerWithAnnotationOnWrongMethod implements RequestHandler, String> { + @Override + public String handleRequest(Map input, Context context) { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + someOtherMethod(); + return "OK"; + } + + @FlushMetrics + public void someOtherMethod() { + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); } - return emptyMap(); } } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/ValidatorTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/ValidatorTest.java new file mode 100644 index 000000000..e5d780f9f --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/ValidatorTest.java @@ -0,0 +1,224 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics.internal; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.Instant; + +import org.junit.jupiter.api.Test; + +class ValidatorTest { + + @Test + void shouldThrowExceptionWhenNamespaceIsNull() { + // When/Then + assertThatThrownBy(() -> Validator.validateNamespace(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Namespace must be specified before flushing metrics"); + } + + @Test + void shouldThrowExceptionWhenNamespaceIsEmpty() { + // When/Then + assertThatThrownBy(() -> Validator.validateNamespace("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Namespace must be specified before flushing metrics"); + } + + @Test + void shouldThrowExceptionWhenNamespaceIsBlank() { + // When/Then + assertThatThrownBy(() -> Validator.validateNamespace(" ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Namespace must be specified before flushing metrics"); + } + + @Test + void shouldThrowExceptionWhenNamespaceExceedsMaxLength() { + // Given + String tooLongNamespace = "a".repeat(256); + + // When/Then + assertThatThrownBy(() -> Validator.validateNamespace(tooLongNamespace)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Namespace exceeds maximum length of 255"); + } + + @Test + void shouldThrowExceptionWhenNamespaceContainsInvalidCharacters() { + // When/Then + assertThatThrownBy(() -> Validator.validateNamespace("Invalid Namespace")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Namespace contains invalid characters"); + } + + @Test + void shouldAcceptValidNamespace() { + // When/Then + assertThatCode(() -> Validator.validateNamespace("Valid.Namespace_123#/")) + .doesNotThrowAnyException(); + } + + @Test + void shouldThrowExceptionWhenDimensionKeyIsNull() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension(null, "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension key cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenDimensionKeyIsEmpty() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("", "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension key cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenDimensionValueIsNull() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("Key", null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenDimensionValueIsEmpty() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("Key", "")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenDimensionKeyContainsWhitespace() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("Key With Space", "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension key cannot contain whitespaces: Key With Space"); + } + + @Test + void shouldThrowExceptionWhenDimensionValueContainsWhitespace() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("Key", "Value With Space")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value cannot contain whitespaces: Value With Space"); + } + + @Test + void shouldThrowExceptionWhenDimensionKeyStartsWithColon() { + // When/Then + assertThatThrownBy(() -> Validator.validateDimension(":Key", "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension key cannot start with colon: :Key"); + } + + @Test + void shouldThrowExceptionWhenDimensionKeyExceedsMaxLength() { + // Given + String longKey = "a".repeat(251); // MAX_DIMENSION_NAME_LENGTH + 1 + + // When/Then + assertThatThrownBy(() -> Validator.validateDimension(longKey, "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension name exceeds maximum length of 250: " + longKey); + } + + @Test + void shouldThrowExceptionWhenDimensionValueExceedsMaxLength() { + // Given + String longValue = "a".repeat(1025); // MAX_DIMENSION_VALUE_LENGTH + 1 + + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("Key", longValue)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value exceeds maximum length of 1024: " + longValue); + } + + @Test + void shouldThrowExceptionWhenDimensionKeyContainsNonAsciiCharacters() { + // Given + String keyWithNonAscii = "Key\u0080"; // Non-ASCII character + + // When/Then + assertThatThrownBy(() -> Validator.validateDimension(keyWithNonAscii, "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension name has invalid characters: " + keyWithNonAscii); + } + + @Test + void shouldThrowExceptionWhenDimensionValueContainsNonAsciiCharacters() { + // Given + String valueWithNonAscii = "Value\u0080"; // Non-ASCII character + + // When/Then + assertThatThrownBy(() -> Validator.validateDimension("Key", valueWithNonAscii)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value has invalid characters: " + valueWithNonAscii); + } + + @Test + void shouldAcceptValidDimension() { + // When/Then + assertThatCode(() -> Validator.validateDimension("ValidKey", "ValidValue")) + .doesNotThrowAnyException(); + } + + @Test + void shouldThrowExceptionWhenTimestampIsNull() { + // When/Then + assertThatThrownBy(() -> Validator.validateTimestamp(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Timestamp cannot be null"); + } + + @Test + void shouldThrowExceptionWhenTimestampIsTooFarInFuture() { + // Given + Instant futureTooFar = Instant.now().plusSeconds(Validator.MAX_TIMESTAMP_FUTURE_AGE_SECONDS + 1); + + // When/Then + assertThatThrownBy(() -> Validator.validateTimestamp(futureTooFar)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Timestamp cannot be more than " + Validator.MAX_TIMESTAMP_FUTURE_AGE_SECONDS + + " seconds in the future"); + } + + @Test + void shouldThrowExceptionWhenTimestampIsTooFarInPast() { + // Given + Instant pastTooFar = Instant.now().minusSeconds(Validator.MAX_TIMESTAMP_PAST_AGE_SECONDS + 1); + + // When/Then + assertThatThrownBy(() -> Validator.validateTimestamp(pastTooFar)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Timestamp cannot be more than " + Validator.MAX_TIMESTAMP_PAST_AGE_SECONDS + + " seconds in the past"); + } + + @Test + void shouldAcceptValidTimestamp() { + // Given + Instant validTimestamp = Instant.now(); + + // When/Then + assertThatCode(() -> Validator.validateTimestamp(validTimestamp)) + .doesNotThrowAnyException(); + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/model/DimensionSetTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/model/DimensionSetTest.java new file mode 100644 index 000000000..ac059f117 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/model/DimensionSetTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class DimensionSetTest { + + @Test + void shouldCreateEmptyDimensionSet() { + // When + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + + // Then + assertThat(dimensionSet.getDimensions()).isEmpty(); + assertThat(dimensionSet.getDimensionKeys()).isEmpty(); + } + + @Test + void shouldCreateDimensionSetWithSingleKeyValue() { + // When + DimensionSet dimensionSet = DimensionSet.of("Key", "Value"); + + // Then + assertThat(dimensionSet.getDimensions()).containsExactly(Map.entry("Key", "Value")); + assertThat(dimensionSet.getDimensionKeys()).containsExactly("Key"); + } + + @Test + void shouldCreateDimensionSetWithTwoKeyValues() { + // When + DimensionSet dimensionSet = DimensionSet.of("Key1", "Value1", "Key2", "Value2"); + + // Then + assertThat(dimensionSet.getDimensions()) + .containsEntry("Key1", "Value1") + .containsEntry("Key2", "Value2"); + assertThat(dimensionSet.getDimensionKeys()).containsExactly("Key1", "Key2"); + } + + @Test + void shouldCreateDimensionSetWithThreeKeyValues() { + // When + DimensionSet dimensionSet = DimensionSet.of( + "Key1", "Value1", + "Key2", "Value2", + "Key3", "Value3"); + + // Then + assertThat(dimensionSet.getDimensions()) + .containsEntry("Key1", "Value1") + .containsEntry("Key2", "Value2") + .containsEntry("Key3", "Value3"); + assertThat(dimensionSet.getDimensionKeys()).containsExactly("Key1", "Key2", "Key3"); + } + + @Test + void shouldCreateDimensionSetWithFourKeyValues() { + // When + DimensionSet dimensionSet = DimensionSet.of( + "Key1", "Value1", + "Key2", "Value2", + "Key3", "Value3", + "Key4", "Value4"); + + // Then + assertThat(dimensionSet.getDimensions()) + .containsEntry("Key1", "Value1") + .containsEntry("Key2", "Value2") + .containsEntry("Key3", "Value3") + .containsEntry("Key4", "Value4"); + assertThat(dimensionSet.getDimensionKeys()).containsExactly("Key1", "Key2", "Key3", "Key4"); + } + + @Test + void shouldCreateDimensionSetWithFiveKeyValues() { + // When + DimensionSet dimensionSet = DimensionSet.of( + "Key1", "Value1", + "Key2", "Value2", + "Key3", "Value3", + "Key4", "Value4", + "Key5", "Value5"); + + // Then + assertThat(dimensionSet.getDimensions()) + .containsEntry("Key1", "Value1") + .containsEntry("Key2", "Value2") + .containsEntry("Key3", "Value3") + .containsEntry("Key4", "Value4") + .containsEntry("Key5", "Value5"); + assertThat(dimensionSet.getDimensionKeys()).containsExactly("Key1", "Key2", "Key3", "Key4", "Key5"); + } + + @Test + void shouldCreateDimensionSetFromMap() { + // Given + Map dimensions = Map.of( + "Key1", "Value1", + "Key2", "Value2"); + + // When + DimensionSet dimensionSet = DimensionSet.of(dimensions); + + // Then + assertThat(dimensionSet.getDimensions()).isEqualTo(dimensions); + assertThat(dimensionSet.getDimensionKeys()).containsExactlyInAnyOrder("Key1", "Key2"); + } + + @Test + void shouldGetDimensionValue() { + // Given + DimensionSet dimensionSet = DimensionSet.of("Key1", "Value1", "Key2", "Value2"); + + // When + String value = dimensionSet.getDimensionValue("Key1"); + + // Then + assertThat(value).isEqualTo("Value1"); + } + + @Test + void shouldReturnNullForNonExistentDimension() { + // Given + DimensionSet dimensionSet = DimensionSet.of("Key1", "Value1"); + + // When + String value = dimensionSet.getDimensionValue("NonExistentKey"); + + // Then + assertThat(value).isNull(); + } + + @Test + void shouldThrowExceptionWhenExceedingMaxDimensions() { + // Given + // Create a dimension set with 30 dimensions (30 is maximum) + DimensionSet dimensionSet = new DimensionSet(); + for (int i = 1; i <= 30; i++) { + dimensionSet.addDimension("Key" + i, "Value" + i); + } + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension("Key31", "Value31")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Cannot exceed 30 dimensions per dimension set"); + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProviderTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProviderTest.java new file mode 100644 index 000000000..2b2268ea8 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProviderTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics.provider; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.internal.EmfMetricsLogger; + +class EmfMetricsProviderTest { + + @Test + void shouldCreateEmfMetricsLogger() { + // Given + EmfMetricsProvider provider = new EmfMetricsProvider(); + + // When + Metrics metrics = provider.getMetricsInstance(); + + // Then + assertThat(metrics) + .isNotNull() + .isInstanceOf(EmfMetricsLogger.class); + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestContext.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestContext.java new file mode 100644 index 000000000..c4f5e4455 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestContext.java @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics.testutils; + +import com.amazonaws.services.lambda.runtime.Context; + +/** + * Simple Lambda context implementation for unit tests + */ +public class TestContext implements Context { + @Override + public String getAwsRequestId() { + return "test-request-id"; + } + + @Override + public String getLogGroupName() { + return "test-log-group"; + } + + @Override + public String getLogStreamName() { + return "test-log-stream"; + } + + @Override + public String getFunctionName() { + return "test-function"; + } + + @Override + public String getFunctionVersion() { + return "test-version"; + } + + @Override + public String getInvokedFunctionArn() { + return "test-arn"; + } + + @Override + public com.amazonaws.services.lambda.runtime.CognitoIdentity getIdentity() { + return null; + } + + @Override + public com.amazonaws.services.lambda.runtime.ClientContext getClientContext() { + return null; + } + + @Override + public int getRemainingTimeInMillis() { + return 1000; + } + + @Override + public int getMemoryLimitInMB() { + return 128; + } + + @Override + public com.amazonaws.services.lambda.runtime.LambdaLogger getLogger() { + return null; + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetrics.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetrics.java new file mode 100644 index 000000000..949828a13 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetrics.java @@ -0,0 +1,85 @@ +package software.amazon.lambda.powertools.metrics.testutils; + +import java.time.Instant; +import java.util.Collections; + +import com.amazonaws.services.lambda.runtime.Context; + +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; + +public class TestMetrics implements Metrics { + @Override + public void addMetric(String name, double value, MetricUnit unit) { + // Test placeholder + } + + @Override + public void flush() { + // Test placeholder + } + + @Override + public void addMetric(String key, double value, MetricUnit unit, MetricResolution resolution) { + // Test placeholder + } + + @Override + public void addDimension(DimensionSet dimensionSet) { + // Test placeholder + } + + @Override + public void addMetadata(String key, Object value) { + // Test placeholder + } + + @Override + public void setDefaultDimensions(DimensionSet dimensionSet) { + // Test placeholder + } + + @Override + public DimensionSet getDefaultDimensions() { + // Test placeholder + return DimensionSet.of(Collections.emptyMap()); + } + + @Override + public void setNamespace(String namespace) { + // Test placeholder + } + + @Override + public void setRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics) { + // Test placeholder + } + + @Override + public void clearDefaultDimensions() { + // Test placeholder + } + + @Override + public void setTimestamp(Instant timestamp) { + // Test placeholder + } + + @Override + public void captureColdStartMetric(Context context, DimensionSet dimensions) { + // Test placeholder + } + + @Override + public void captureColdStartMetric(DimensionSet dimensions) { + // Test placeholder + } + + @Override + public void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, + DimensionSet dimensions) { + // Test placeholder + } +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsProvider.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsProvider.java new file mode 100644 index 000000000..4a5fc23dd --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsProvider.java @@ -0,0 +1,11 @@ +package software.amazon.lambda.powertools.metrics.testutils; + +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; + +public class TestMetricsProvider implements MetricsProvider { + @Override + public Metrics getMetricsInstance() { + return new TestMetrics(); + } +} diff --git a/powertools-metrics/src/test/resources/simplelogger.properties b/powertools-metrics/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..6c626ab4d --- /dev/null +++ b/powertools-metrics/src/test/resources/simplelogger.properties @@ -0,0 +1,7 @@ +org.slf4j.simpleLogger.logFile=target/metrics-test.log +org.slf4j.simpleLogger.defaultLogLevel=warn +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS +org.slf4j.simpleLogger.showThreadName=false +org.slf4j.simpleLogger.showLogName=true +org.slf4j.simpleLogger.showShortLogName=false diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index e959204ad..9a42ebf16 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -234,8 +234,8 @@ - - + +