From 2a79c02b7a964ed9442c8d8b4f81f7b9b444fc84 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 2 Jun 2025 18:31:52 +0200 Subject: [PATCH 01/36] Rewrite metrics module with interface abstraction and provider pattern to be able to support multiple metrics providers. --- .../cdk/app/src/main/java/helloworld/App.java | 24 +- .../gradle/src/main/java/helloworld/App.java | 23 +- .../kotlin/src/main/kotlin/helloworld/App.kt | 29 +- .../src/main/java/helloworld/App.java | 27 +- .../sam/src/main/java/helloworld/App.java | 38 +- .../src/main/java/helloworld/App.java | 23 +- .../src/main/java/helloworld/App.java | 23 +- .../emf/model/MetricsLoggerHelper.java | 42 -- .../lambda/powertools/metrics/Metrics.java | 10 +- .../powertools/metrics/MetricsLogger.java | 170 ++++++++ .../metrics/MetricsLoggerBuilder.java | 140 +++++++ .../metrics/MetricsLoggerFactory.java | 76 ++++ .../powertools/metrics/MetricsUtils.java | 172 -------- .../metrics/internal/EmfMetricsLogger.java | 271 ++++++++++++ .../metrics/internal/LambdaMetricsAspect.java | 122 ++---- .../metrics/model/DimensionSet.java | 196 +++++++++ .../MetricResolution.java} | 21 +- .../powertools/metrics/model/MetricUnit.java | 58 +++ .../metrics/provider/EmfMetricsProvider.java | 31 ++ .../metrics/provider/MetricsProvider.java | 30 ++ .../powertools/metrics/MetricsLoggerTest.java | 232 ----------- ...ertoolsMetricsColdStartEnabledHandler.java | 35 -- ...MetricsEnabledDefaultDimensionHandler.java | 46 --- ...tricsEnabledDefaultNoDimensionHandler.java | 45 -- .../PowertoolsMetricsEnabledHandler.java | 41 -- ...PowertoolsMetricsEnabledStreamHandler.java | 35 -- ...sMetricsExceptionWhenNoMetricsHandler.java | 34 -- .../PowertoolsMetricsNoDimensionsHandler.java | 36 -- ...etricsNoExceptionWhenNoMetricsHandler.java | 34 -- ...rtoolsMetricsTooManyDimensionsHandler.java | 40 -- ...wertoolsMetricsWithExceptionInHandler.java | 33 -- .../internal/LambdaMetricsAspectTest.java | 384 ------------------ 32 files changed, 1138 insertions(+), 1383 deletions(-) delete mode 100644 powertools-metrics/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsLoggerHelper.java create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java delete mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsUtils.java create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/DimensionSet.java rename powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/{ValidationException.java => model/MetricResolution.java} (66%) create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricUnit.java create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProvider.java create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/provider/MetricsProvider.java delete mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerTest.java delete mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsColdStartEnabledHandler.java delete mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultDimensionHandler.java delete mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledDefaultNoDimensionHandler.java delete mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledHandler.java delete mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsEnabledStreamHandler.java delete mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsExceptionWhenNoMetricsHandler.java delete mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoDimensionsHandler.java delete mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsNoExceptionWhenNoMetricsHandler.java delete mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsTooManyDimensionsHandler.java delete mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/handlers/PowertoolsMetricsWithExceptionInHandler.java delete mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java 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..cd9d467d5 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.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsLogger; +import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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,6 +45,8 @@ */ public class App implements RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) @@ -54,13 +56,13 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metricsLogger.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" + ); + metricsLogger.pushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); @@ -103,4 +105,4 @@ private String getPageContents(String address) throws IOException { return br.lines().collect(Collectors.joining(System.lineSeparator())); } } -} +} \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java index b1a701b8f..e25ce5c3b 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.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsLogger; +import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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,6 +46,7 @@ */ public class App implements RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @@ -56,13 +57,13 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metricsLogger.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" + ); + metricsLogger.pushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); @@ -105,4 +106,4 @@ private String getPageContents(String address) throws IOException { return br.lines().collect(Collectors.joining(System.lineSeparator())); } } -} +} \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt b/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt index 8e8857079..8752928bc 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.MetricsLogger +import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory +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 metricsLogger: MetricsLogger = MetricsLoggerFactory.getMetricsLogger() + @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @Metrics(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")) - } + + metricsLogger.addMetric("CustomMetric1", 1.0, MetricUnit.COUNT) + + val dimensionSet = DimensionSet.of( + "AnotherService", "CustomService", + "AnotherService1", "CustomService1" + ) + metricsLogger.pushSingleMetric("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 { private static final Logger log = LoggerFactory.getLogger(App.class); + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @@ -57,15 +58,15 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metricsLogger.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" + ); + metricsLogger.pushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); - metricsLogger().putMetric("CustomMetric3", 1, Unit.COUNT, StorageResolution.HIGH); + metricsLogger.addMetric("CustomMetric3", 1, MetricUnit.COUNT, MetricResolution.HIGH); MDC.put("test", "willBeLogged"); @@ -108,4 +109,4 @@ private String getPageContents(String address) throws IOException { return br.lines().collect(Collectors.joining(System.lineSeparator())); } } -} +} \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java index e7c410042..8e5ef1d08 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.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsLogger; +import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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,6 +50,7 @@ */ public class App implements RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @@ -57,15 +61,14 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metricsLogger.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"); + metricsLogger.pushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); - metricsLogger().putMetric("CustomMetric3", 1, Unit.COUNT, StorageResolution.HIGH); + metricsLogger.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/serverless/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/serverless/src/main/java/helloworld/App.java index e0b1a2979..2a69da5bb 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.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsLogger; +import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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,6 +46,7 @@ */ public class App implements RequestHandler { private static final Logger log = LogManager.getLogger(App.class); + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @@ -56,13 +57,13 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metricsLogger.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" + ); + metricsLogger.pushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); @@ -100,4 +101,4 @@ private String getPageContents(String address) throws IOException { return br.lines().collect(Collectors.joining(System.lineSeparator())); } } -} +} \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java index e0b1a2979..2a69da5bb 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.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsLogger; +import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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,6 +46,7 @@ */ public class App implements RequestHandler { private static final Logger log = LogManager.getLogger(App.class); + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @@ -56,13 +57,13 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + metricsLogger.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" + ); + metricsLogger.pushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); @@ -100,4 +101,4 @@ private String getPageContents(String address) throws IOException { return br.lines().collect(Collectors.joining(System.lineSeparator())); } } -} +} \ No newline at end of file diff --git a/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/Metrics.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java index fb92c900d..df2c8fe32 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 @@ -41,7 +41,7 @@ *

* *

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. + *
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 @@ -53,6 +53,10 @@ * 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.

+ * + *

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

*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @@ -60,8 +64,10 @@ String namespace() default ""; String service() default ""; + + String functionName() default ""; boolean captureColdStart() default false; boolean raiseOnEmptyMetrics() default false; -} +} \ No newline at end of file diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java new file mode 100644 index 000000000..53877ed5d --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java @@ -0,0 +1,170 @@ +/* + * 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 com.amazonaws.services.lambda.runtime.Context; +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.util.Map; + +/** + * Interface for metrics logging + */ +public interface MetricsLogger { + + /** + * Add a metric to the metrics logger + * + * @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 to the metrics logger 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 to the metrics logger 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 to the metrics logger + * + * @param key the name of the dimension + * @param value the value of the dimension + */ + void addDimension(String key, String value); + + /** + * Add metadata to the metrics logger + * + * @param key the name of the metadata + * @param value the value of the metadata + */ + void addMetadata(String key, Object value); + + /** + * Set default dimensions for the metrics logger + * + * @param defaultDimensions map of default dimensions + */ + void setDefaultDimensions(Map defaultDimensions); + + /** + * Get the default dimensions for the metrics logger + * + * @return the default dimensions as a DimensionSet + */ + DimensionSet getDefaultDimensions(); + + /** + * Set the namespace for the metrics logger + * + * @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); + + /** + * Capture cold start metric without Lambda context and flush immediately + */ + default void captureColdStartMetric() { + captureColdStartMetric((DimensionSet)null); + } + + /** + * Push 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 pushSingleMetric(String name, double value, MetricUnit unit, String namespace, + DimensionSet dimensions); + + /** + * Push 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 pushSingleMetric(String name, double value, MetricUnit unit, String namespace) { + pushSingleMetric(name, value, unit, namespace, null); + } +} \ No newline at end of file diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java new file mode 100644 index 000000000..02ee4799e --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java @@ -0,0 +1,140 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics; + +import java.util.LinkedHashMap; +import java.util.Map; + +import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; + +/** + * Builder for configuring the singleton MetricsLogger instance + */ +public class MetricsLoggerBuilder { + private MetricsProvider provider; + private String namespace; + private String service; + private boolean raiseOnEmptyMetrics = false; + private final Map defaultDimensions = new LinkedHashMap<>(); + + private MetricsLoggerBuilder() { + } + + /** + * Create a new builder instance + * + * @return a new builder instance + */ + public static MetricsLoggerBuilder builder() { + return new MetricsLoggerBuilder(); + } + + /** + * Set the metrics provider + * + * @param provider the metrics provider + * @return this builder + */ + public MetricsLoggerBuilder withMetricsProvider(MetricsProvider provider) { + this.provider = provider; + return this; + } + + /** + * Set the namespace + * + * @param namespace the namespace + * @return this builder + */ + public MetricsLoggerBuilder withNamespace(String namespace) { + this.namespace = namespace; + return this; + } + + /** + * Set the service name + * + * @param service the service name + * @return this builder + */ + public MetricsLoggerBuilder 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 MetricsLoggerBuilder 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 MetricsLoggerBuilder withDefaultDimension(String key, String value) { + this.defaultDimensions.put(key, value); + return this; + } + + /** + * Add default dimensions + * + * @param dimensions map of dimensions + * @return this builder + */ + public MetricsLoggerBuilder withDefaultDimensions(Map dimensions) { + this.defaultDimensions.putAll(dimensions); + return this; + } + + /** + * Configure and return the singleton MetricsLogger instance + * + * @return the configured singleton MetricsLogger instance + */ + public MetricsLogger build() { + if (provider != null) { + MetricsLoggerFactory.setMetricsProvider(provider); + } + + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + + if (namespace != null) { + metricsLogger.setNamespace(namespace); + } + + metricsLogger.setRaiseOnEmptyMetrics(raiseOnEmptyMetrics); + + if (!defaultDimensions.isEmpty()) { + metricsLogger.setDefaultDimensions(defaultDimensions); + } + + // Add Service dimension separately to ensure it's not mixed with other default dimensions + if (service != null) { + metricsLogger.addDimension("Service", service); + } + + return metricsLogger; + } +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java new file mode 100644 index 000000000..f13e89fe3 --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java @@ -0,0 +1,76 @@ +/* + * 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.metrics.provider.EmfMetricsProvider; +import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; + +import java.util.HashMap; +import java.util.Map; + +/** + * Factory for accessing the singleton MetricsLogger instance + */ +public final class MetricsLoggerFactory { + private static MetricsProvider provider = new EmfMetricsProvider(); + private static MetricsLogger metricsLogger; + + private MetricsLoggerFactory() { + } + + /** + * Get the singleton instance of the MetricsLogger + * + * @return the singleton MetricsLogger instance + */ + public static synchronized MetricsLogger getMetricsLogger() { + if (metricsLogger == null) { + metricsLogger = provider.getMetricsLogger(); + + // Apply default configuration from environment variables + String envNamespace = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); + if (envNamespace != null) { + metricsLogger.setNamespace(envNamespace); + } + + String envService = System.getenv("POWERTOOLS_SERVICE_NAME"); + if (envService != null) { + Map dimensions = new HashMap<>(); + dimensions.put("Service", envService); + metricsLogger.setDefaultDimensions(dimensions); + } else { + Map dimensions = new HashMap<>(); + dimensions.put("Service", "service_undefined"); + metricsLogger.setDefaultDimensions(dimensions); + } + } + + return metricsLogger; + } + + /** + * 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 logger so it will be recreated with the new provider + metricsLogger = null; + } +} \ No newline at end of file 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..1b162f616 --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java @@ -0,0 +1,271 @@ +/* + * 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 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.MetricsLogger; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.getXrayTraceId; +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isColdStart; + +/** + * Implementation of MetricsLogger that uses the EMF library + */ +public class EmfMetricsLogger implements MetricsLogger { + + 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 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(String key, String value) { + DimensionSet dimensionSet = new DimensionSet(); + try { + dimensionSet.addDimension(key, value); + emfLogger.putDimensions(dimensionSet); + // Update our local copy of default dimensions + defaultDimensions.put(key, value); + } catch (Exception e) { + // Ignore dimension errors + } + } + + @Override + public void addMetadata(String key, Object value) { + emfLogger.putMetadata(key, value); + } + + @Override + public void setDefaultDimensions(Map defaultDimensions) { + DimensionSet dimensionSet = new DimensionSet(); + defaultDimensions.forEach((key, value) -> { + try { + dimensionSet.addDimension(key, value); + } catch (Exception e) { + // Ignore dimension errors + } + }); + emfLogger.setDimensions(dimensionSet); + // Store a copy of the default dimensions + this.defaultDimensions = new HashMap<>(defaultDimensions); + } + + @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) { + this.namespace = namespace; + try { + emfLogger.setNamespace(namespace); + } catch (Exception e) { + // Ignore namespace errors + } + } + + @Override + public void setRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics) { + this.raiseOnEmptyMetrics = raiseOnEmptyMetrics; + } + + @Override + public void clearDefaultDimensions() { + emfLogger.resetDimensions(false); + defaultDimensions.clear(); + } + + @Override + public void flush() { + if (raiseOnEmptyMetrics && !hasMetrics.get()) { + throw new IllegalStateException("No metrics were emitted"); + } + emfLogger.flush(); + } + + @Override + public void captureColdStartMetric(Context context, + software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) { + if (isColdStart()) { + software.amazon.cloudwatchlogs.emf.logger.MetricsLogger coldStartLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger(); + + // Set namespace if available + if (namespace != null) { + try { + coldStartLogger.setNamespace(namespace); + } catch (Exception e) { + // Ignore namespace errors + } + } + + 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 pushSingleMetric(String name, double value, MetricUnit unit, String namespace, + software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) { + // Create a new logger for this single metric + software.amazon.cloudwatchlogs.emf.logger.MetricsLogger singleMetricLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger( + environmentProvider); + + // Set namespace (now mandatory) + try { + singleMetricLogger.setNamespace(namespace); + } catch (Exception e) { + // Ignore namespace errors + } + + // 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 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; + } + } +} \ No newline at end of file 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..8887678b0 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,30 +14,21 @@ 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 software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; 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.MetricsLogger; +import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; @Aspect public class LambdaMetricsAspect { @@ -48,23 +39,16 @@ public class LambdaMetricsAspect { private static String service(Metrics metrics) { return !"".equals(metrics.service()) ? metrics.service() : serviceName(); } - - // 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(); - - DimensionSet[] defaultDimensions = hasDefaultDimension() ? MetricsUtils.getDefaultDimensions() - : new DimensionSet[] {DimensionSet.of("Service", service(metrics))}; - - context.setDimensions(defaultDimensions); - - f.set(metricsLogger(), context); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); + + private String namespace(Metrics metrics) { + return !"".equals(metrics.namespace()) ? metrics.namespace() : NAMESPACE; + } + + private String functionName(Metrics metrics, Context context) { + if (!"".equals(metrics.functionName())) { + return metrics.functionName(); } + return context != null ? context.getFunctionName() : null; } @SuppressWarnings({"EmptyMethod"}) @@ -78,74 +62,52 @@ public Object around(ProceedingJoinPoint pjp, Object[] proceedArgs = pjp.getArgs(); if (isHandlerMethod(pjp)) { + MetricsLogger logger = MetricsLoggerFactory.getMetricsLogger(); - MetricsLogger logger = metricsLogger(); + // Add service dimension separately + logger.addDimension("Service", service(metrics)); - refreshMetricsContext(metrics); + // Set namespace + String metricsNamespace = namespace(metrics); + if (metricsNamespace != null) { + logger.setNamespace(metricsNamespace); + } - logger.setNamespace(namespace(metrics)); + // Configure other settings + logger.setRaiseOnEmptyMetrics(metrics.raiseOnEmptyMetrics()); + + // Add trace ID metadata if available + LambdaHandlerProcessor.getXrayTraceId() + .ifPresent(traceId -> logger.addMetadata(TRACE_ID_PROPERTY, traceId)); Context extractedContext = extractContext(pjp); if (null != extractedContext) { - coldStartSingleMetricIfApplicable(extractedContext.getAwsRequestId(), - extractedContext.getFunctionName(), metrics); - logger.putProperty(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); + logger.addMetadata(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); + + // Only capture cold start metrics if configured + if (metrics.captureColdStart()) { + // Get function name from annotation or context + String funcName = functionName(metrics, extractedContext); + + // Create dimensions with service and function name + DimensionSet coldStartDimensions = DimensionSet.of( + "Service", service(metrics), + "FunctionName", funcName != null ? funcName : extractedContext.getFunctionName() + ); + + logger.captureColdStartMetric(extractedContext, coldStartDimensions); + } } - LambdaHandlerProcessor.getXrayTraceId() - .ifPresent(traceId -> logger.putProperty(TRACE_ID_PROPERTY, traceId)); - try { return pjp.proceed(proceedArgs); - } finally { coldStartDone(); - validateMetricsAndRefreshOnFailure(metrics); logger.flush(); - refreshMetricsContext(metrics); } } 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 validateBeforeFlushingMetrics(Metrics metrics) { - if (metrics.raiseOnEmptyMetrics() && hasNoMetrics()) { - throw new ValidationException("No metrics captured, at least one metrics must be emitted"); - } - - if (dimensionsCount() > 9) { - throw new ValidationException(String.format("Number of Dimensions must be in range of 0-9." + - " Actual size: %d.", dimensionsCount())); - } - } - - private String namespace(Metrics metrics) { - return !"".equals(metrics.namespace()) ? metrics.namespace() : NAMESPACE; - } - - private void validateMetricsAndRefreshOnFailure(Metrics metrics) { - try { - validateBeforeFlushingMetrics(metrics); - } catch (ValidationException e) { - refreshMetricsContext(metrics); - throw e; - } - } -} +} \ No newline at end of file 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..c71049568 --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/DimensionSet.java @@ -0,0 +1,196 @@ +/* + * 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; + +/** + * Represents a set of dimensions for CloudWatch metrics + */ +public class DimensionSet { + private static final int MAX_DIMENSION_SET_SIZE = 9; + + 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 + * + * @return map of dimensions + */ + public Map getDimensions() { + return new LinkedHashMap<>(dimensions); + } + + private void validateDimension(String key, String value) { + if (key == null || key.isEmpty()) { + throw new IllegalArgumentException("Dimension key cannot be null or empty"); + } + + if (value == null) { + throw new IllegalArgumentException("Dimension value cannot be null"); + } + } +} \ No newline at end of file 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..2524f429c 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,22 @@ * */ -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; } -} +} \ No newline at end of file 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..134dc939d --- /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; + } +} \ No newline at end of file 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..e9a6f6b85 --- /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.MetricsLogger; +import software.amazon.lambda.powertools.metrics.internal.EmfMetricsLogger; + +/** + * Provider implementation for EMF metrics + */ +public class EmfMetricsProvider implements MetricsProvider { + + @Override + public MetricsLogger getMetricsLogger() { + return new EmfMetricsLogger(new EnvironmentProvider(), new MetricsContext()); + } +} \ No newline at end of file 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..198a8cd83 --- /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.MetricsLogger; + +/** + * Interface for metrics provider implementations + */ +public interface MetricsProvider { + + /** + * Get a new instance of a metrics logger + * + * @return a new metrics logger instance + */ + MetricsLogger getMetricsLogger(); +} \ No newline at end of file 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/LambdaMetricsAspectTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java deleted file mode 100644 index 5df6003c8..000000000 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java +++ /dev/null @@ -1,384 +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.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.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.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; - - @BeforeEach - void setUp() throws IllegalAccessException { - openMocks(this); - setupContext(); - writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); - 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") - 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"); - }); - } - - @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"); - }); - } - - @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"); - }); - } - - @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"); - }); - } - - @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"); - }); - } - - @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"); - }); - } - - @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"); - } - - @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"); - }); - } - - @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"); - }); - } - - @Test - @SetEnvironmentVariable(key = "AWS_EMF_ENVIRONMENT", value = "Lambda") - public void exceptionWhenTooManyDimensionsSet() { - MetricsUtils.defaultDimensions(null); - - requestHandler = new PowertoolsMetricsTooManyDimensionsHandler(); - - assertThatExceptionOfType(DimensionSetExceededException.class) - .isThrownBy(() -> requestHandler.handleRequest("input", context)) - .withMessage( - "Maximum number of dimensions allowed are 30. Account for default dimensions if not using setDimensions."); - } - - @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"); - }); - } - - 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"); - } - - private Map readAsJson(String s) { - try { - return mapper.readValue(s, Map.class); - } catch (JsonProcessingException e) { - e.printStackTrace(); - } - return emptyMap(); - } -} From 424976682b9025822441203e6689b3da6b90d173 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 3 Jun 2025 13:32:06 +0200 Subject: [PATCH 02/36] Update default dimension and namespace logic to have the precedence: @Metrics annotation. MetricsLoggerBuilder, Environment variables. --- .../lambda/powertools/metrics/Metrics.java | 11 ++-- .../metrics/MetricsLoggerBuilder.java | 15 ++--- .../metrics/MetricsLoggerFactory.java | 27 +++----- .../metrics/internal/EmfMetricsLogger.java | 4 +- .../metrics/internal/LambdaMetricsAspect.java | 61 +++++++++++-------- .../metrics/provider/MetricsProvider.java | 4 +- 6 files changed, 60 insertions(+), 62 deletions(-) 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 df2c8fe32..3d75e6710 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 @@ -20,13 +20,12 @@ import java.lang.annotation.Target; /** - * {@code Metrics} is used to signal that the annotated method should be - * extended with Metrics functionality. + * {@code Metrics} is used to signal that the annotated Lambda handler method should be + * extended with Metrics functionality. Will have no effect when used on a method that is not a Lambda handler. * *

{@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} manages the life-cycle and configuration of the MetricsLogger 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 @@ -64,10 +63,10 @@ String namespace() default ""; String service() default ""; - + String functionName() default ""; boolean captureColdStart() default false; boolean raiseOnEmptyMetrics() default false; -} \ No newline at end of file +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java index 02ee4799e..41785828e 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java @@ -64,7 +64,8 @@ public MetricsLoggerBuilder withNamespace(String namespace) { } /** - * Set the service name + * 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 @@ -86,7 +87,7 @@ public MetricsLoggerBuilder withRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics) } /** - * Add a default dimension + * Add a default dimension. * * @param key the dimension key * @param value the dimension value @@ -126,13 +127,13 @@ public MetricsLogger build() { metricsLogger.setRaiseOnEmptyMetrics(raiseOnEmptyMetrics); - if (!defaultDimensions.isEmpty()) { - metricsLogger.setDefaultDimensions(defaultDimensions); + if (service != null) { + metricsLogger.setDefaultDimensions(Map.of("Service", service)); } - // Add Service dimension separately to ensure it's not mixed with other default dimensions - if (service != null) { - metricsLogger.addDimension("Service", service); + // If the user provided default dimension, we overwrite the default Service dimension again + if (!defaultDimensions.isEmpty()) { + metricsLogger.setDefaultDimensions(defaultDimensions); } return metricsLogger; diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java index f13e89fe3..054a889fe 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java @@ -14,12 +14,12 @@ package software.amazon.lambda.powertools.metrics; +import java.util.Map; + +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider; import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; -import java.util.HashMap; -import java.util.Map; - /** * Factory for accessing the singleton MetricsLogger instance */ @@ -38,28 +38,19 @@ private MetricsLoggerFactory() { public static synchronized MetricsLogger getMetricsLogger() { if (metricsLogger == null) { metricsLogger = provider.getMetricsLogger(); - + // Apply default configuration from environment variables String envNamespace = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); if (envNamespace != null) { metricsLogger.setNamespace(envNamespace); } - - String envService = System.getenv("POWERTOOLS_SERVICE_NAME"); - if (envService != null) { - Map dimensions = new HashMap<>(); - dimensions.put("Service", envService); - metricsLogger.setDefaultDimensions(dimensions); - } else { - Map dimensions = new HashMap<>(); - dimensions.put("Service", "service_undefined"); - metricsLogger.setDefaultDimensions(dimensions); - } + + metricsLogger.setDefaultDimensions(Map.of("Service", LambdaHandlerProcessor.serviceName())); } - + return metricsLogger; } - + /** * Set the metrics provider * @@ -73,4 +64,4 @@ public static synchronized void setMetricsProvider(MetricsProvider metricsProvid // Reset the logger so it will be recreated with the new provider metricsLogger = null; } -} \ No newline at end of file +} 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 index 1b162f616..f3063c80f 100644 --- 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 @@ -93,7 +93,7 @@ public void setDefaultDimensions(Map defaultDimensions) { // Store a copy of the default dimensions this.defaultDimensions = new HashMap<>(defaultDimensions); } - + @Override public software.amazon.lambda.powertools.metrics.model.DimensionSet getDefaultDimensions() { return software.amazon.lambda.powertools.metrics.model.DimensionSet.of(defaultDimensions); @@ -268,4 +268,4 @@ private Unit convertUnit(MetricUnit unit) { return Unit.NONE; } } -} \ No newline at end of file +} 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 8887678b0..0c9cac9c7 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 @@ -17,13 +17,16 @@ import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.coldStartDone; import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.extractContext; import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isHandlerMethod; -import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.serviceName; -import com.amazonaws.services.lambda.runtime.Context; +import java.util.Map; + import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; + +import com.amazonaws.services.lambda.runtime.Context; + import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; @@ -34,16 +37,7 @@ 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 String namespace(Metrics metrics) { - return !"".equals(metrics.namespace()) ? metrics.namespace() : NAMESPACE; - } - private String functionName(Metrics metrics, Context context) { if (!"".equals(metrics.functionName())) { return metrics.functionName(); @@ -51,29 +45,41 @@ private String functionName(Metrics metrics, Context context) { return context != null ? context.getFunctionName() : null; } - @SuppressWarnings({"EmptyMethod"}) + private String serviceNameWithFallback(Metrics metrics) { + if (!"".equals(metrics.service())) { + return metrics.service(); + } + return LambdaHandlerProcessor.serviceName(); + } + + @SuppressWarnings({ "EmptyMethod" }) @Pointcut("@annotation(metrics)") public void callAt(Metrics metrics) { } @Around(value = "callAt(metrics) && execution(@Metrics * *.*(..))", argNames = "pjp,metrics") public Object around(ProceedingJoinPoint pjp, - Metrics metrics) throws Throwable { + Metrics metrics) throws Throwable { Object[] proceedArgs = pjp.getArgs(); if (isHandlerMethod(pjp)) { MetricsLogger logger = MetricsLoggerFactory.getMetricsLogger(); - // Add service dimension separately - logger.addDimension("Service", service(metrics)); + // The MetricsLoggerFactory applies default settings from the environment or can be configured by the + // MetricsLoggerBuilder. We only overwrite settings if they are explicitly set in the @Metrics annotation. + if (!"".equals(metrics.namespace())) { + logger.setNamespace(metrics.namespace()); + } - // Set namespace - String metricsNamespace = namespace(metrics); - if (metricsNamespace != null) { - logger.setNamespace(metricsNamespace); + // If the default dimensions are larger than 1 or do not contain the "Service" dimension, it means that the + // user overwrote them manually e.g. using MetricsLoggerBuilder. In this case, we don't set the service + // default dimension. + if (!"".equals(metrics.service()) + && logger.getDefaultDimensions().getDimensionKeys().size() <= 1 + && logger.getDefaultDimensions().getDimensionKeys().contains("Service")) { + logger.setDefaultDimensions(Map.of("Service", metrics.service())); } - // Configure other settings logger.setRaiseOnEmptyMetrics(metrics.raiseOnEmptyMetrics()); // Add trace ID metadata if available @@ -84,18 +90,19 @@ public Object around(ProceedingJoinPoint pjp, if (null != extractedContext) { logger.addMetadata(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); - + // Only capture cold start metrics if configured if (metrics.captureColdStart()) { // Get function name from annotation or context String funcName = functionName(metrics, extractedContext); - + // Create dimensions with service and function name DimensionSet coldStartDimensions = DimensionSet.of( - "Service", service(metrics), - "FunctionName", funcName != null ? funcName : extractedContext.getFunctionName() - ); - + "Service", + logger.getDefaultDimensions().getDimensions().getOrDefault("Service", + serviceNameWithFallback(metrics)), + "FunctionName", funcName != null ? funcName : extractedContext.getFunctionName()); + logger.captureColdStartMetric(extractedContext, coldStartDimensions); } } @@ -110,4 +117,4 @@ public Object around(ProceedingJoinPoint pjp, return pjp.proceed(proceedArgs); } -} \ No newline at end of file +} 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 index 198a8cd83..7eb5a94c7 100644 --- 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 @@ -20,11 +20,11 @@ * Interface for metrics provider implementations */ public interface MetricsProvider { - + /** * Get a new instance of a metrics logger * * @return a new metrics logger instance */ MetricsLogger getMetricsLogger(); -} \ No newline at end of file +} From 9961072213f3db633a83ce61105cc1a84e082141 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 3 Jun 2025 13:37:14 +0200 Subject: [PATCH 03/36] Small cosmetic changes. --- .../metrics/internal/EmfMetricsLogger.java | 22 ++++++----- .../metrics/model/DimensionSet.java | 39 ++++++++++--------- .../metrics/model/MetricResolution.java | 5 +-- .../powertools/metrics/model/MetricUnit.java | 2 +- 4 files changed, 35 insertions(+), 33 deletions(-) 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 index f3063c80f..0dec6f4ab 100644 --- 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 @@ -14,7 +14,16 @@ 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.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + 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; @@ -24,18 +33,11 @@ import software.amazon.lambda.powertools.metrics.model.MetricResolution; import software.amazon.lambda.powertools.metrics.model.MetricUnit; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - -import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.getXrayTraceId; -import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isColdStart; - /** - * Implementation of MetricsLogger that uses the EMF library + * Implementation of MetricsLogger that uses the EMF library. Proxies MetricsLogger interface calls to underlying + * library {@link software.amazon.cloudwatchlogs.emf.logger.MetricsLogger}. */ public class EmfMetricsLogger implements MetricsLogger { - 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"; @@ -91,7 +93,7 @@ public void setDefaultDimensions(Map defaultDimensions) { }); emfLogger.setDimensions(dimensionSet); // Store a copy of the default dimensions - this.defaultDimensions = new HashMap<>(defaultDimensions); + this.defaultDimensions = new LinkedHashMap<>(defaultDimensions); } @Override 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 index c71049568..be0d085ad 100644 --- 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 @@ -23,7 +23,7 @@ */ public class DimensionSet { private static final int MAX_DIMENSION_SET_SIZE = 9; - + private final Map dimensions = new LinkedHashMap<>(); /** @@ -54,7 +54,7 @@ public static DimensionSet of(String key1, String value1, String key2, String va dimensionSet.addDimension(key2, value2); return dimensionSet; } - + /** * Create a dimension set with three key-value pairs * @@ -73,7 +73,7 @@ public static DimensionSet of(String key1, String value1, String key2, String va dimensionSet.addDimension(key3, value3); return dimensionSet; } - + /** * Create a dimension set with four key-value pairs * @@ -87,8 +87,8 @@ public static DimensionSet of(String key1, String value1, String key2, String va * @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) { + 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); @@ -96,7 +96,7 @@ public static DimensionSet of(String key1, String value1, String key2, String va dimensionSet.addDimension(key4, value4); return dimensionSet; } - + /** * Create a dimension set with five key-value pairs * @@ -112,9 +112,9 @@ public static DimensionSet of(String key1, String value1, String key2, String va * @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) { + 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); @@ -135,7 +135,7 @@ public static DimensionSet of(Map dimensions) { dimensions.forEach(dimensionSet::addDimension); return dimensionSet; } - + /** * Add a dimension to this dimension set * @@ -147,15 +147,16 @@ public static DimensionSet of(Map 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"); + 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 * @@ -164,7 +165,7 @@ public DimensionSet addDimension(String key, String value) { public Set getDimensionKeys() { return dimensions.keySet(); } - + /** * Get the value for a dimension key * @@ -176,21 +177,21 @@ public String getDimensionValue(String key) { } /** - * Get the dimensions as a map + * 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) { if (key == null || key.isEmpty()) { throw new IllegalArgumentException("Dimension key cannot be null or empty"); } - + if (value == null) { throw new IllegalArgumentException("Dimension value cannot be null"); } } -} \ No newline at end of file +} diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricResolution.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricResolution.java index 2524f429c..db514c8b7 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricResolution.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/model/MetricResolution.java @@ -18,8 +18,7 @@ * Resolution for metrics */ public enum MetricResolution { - STANDARD(60), - HIGH(1); + STANDARD(60), HIGH(1); private final int seconds; @@ -30,4 +29,4 @@ public enum MetricResolution { public int getSeconds() { return seconds; } -} \ No newline at end of file +} 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 index 134dc939d..445d950b2 100644 --- 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 @@ -55,4 +55,4 @@ public enum MetricUnit { public String getName() { return name; } -} \ No newline at end of file +} From f1e9acb0e78f20b4679ef4fc03f4052aaa49158c Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 10:25:34 +0200 Subject: [PATCH 04/36] Add unit tests. --- pom.xml | 18 +- powertools-metrics/pom.xml | 58 ++- .../metrics/ConfigurationPrecedenceTest.java | 202 +++++++++ .../metrics/MetricsLoggerBuilderTest.java | 183 ++++++++ .../metrics/MetricsLoggerFactoryTest.java | 149 +++++++ .../internal/EmfMetricsLoggerTest.java | 390 ++++++++++++++++++ .../internal/LambdaMetricsAspectTest.java | 298 +++++++++++++ .../metrics/model/DimensionSetTest.java | 199 +++++++++ .../provider/EmfMetricsProviderTest.java | 39 ++ .../metrics/testutils/TestContext.java | 77 ++++ 10 files changed, 1593 insertions(+), 20 deletions(-) create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/ConfigurationPrecedenceTest.java create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLoggerTest.java create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/model/DimensionSetTest.java create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/provider/EmfMetricsProviderTest.java create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestContext.java diff --git a/pom.xml b/pom.xml index cb761183e..06e371e25 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 @@ -341,6 +341,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-metrics/pom.xml b/powertools-metrics/pom.xml index d51ea5b33..0f713cc91 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 @@ -82,6 +82,11 @@ mockito-core test + + org.mockito + mockito-junit-jupiter + test + org.slf4j slf4j-simple @@ -116,7 +121,6 @@ org.mockito mockito-subclass - 5.17.0 test @@ -125,9 +129,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 +146,6 @@ org.mockito mockito-subclass - 5.17.0 test @@ -151,7 +154,7 @@ org.graalvm.buildtools native-maven-plugin - 0.10.2 + 0.10.6 true @@ -178,16 +181,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 +220,16 @@ src/main/resources + + + org.apache.maven.plugins + maven-surefire-plugin + + + Lambda + + + + 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..9f732672a --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/ConfigurationPrecedenceTest.java @@ -0,0 +1,202 @@ +/* + * 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. Metrics annotation + * 2. MetricsLoggerBuilder + * 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 = MetricsLoggerFactory.class.getDeclaredField("metricsLogger"); + field.setAccessible(true); + field.set(null, null); + + field = MetricsLoggerFactory.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 + MetricsLoggerBuilder.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 + MetricsLoggerBuilder.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 + 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("aws-embedded-metrics"); + assertThat(rootNode.has("Service")).isTrue(); + assertThat(rootNode.get("Service").asText()).isEqualTo("service_undefined"); + } + + private static class HandlerWithMetricsAnnotation implements RequestHandler, String> { + @Override + @Metrics(namespace = "AnnotationNamespace", service = "AnnotationService") + public String handleRequest(Map input, Context context) { + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } + + private static class HandlerWithDefaultMetricsAnnotation implements RequestHandler, String> { + @Override + @Metrics + public String handleRequest(Map input, Context context) { + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } + +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java new file mode 100644 index 000000000..05967d90d --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java @@ -0,0 +1,183 @@ +/* + * 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 static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Map; + +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.MetricUnit; +import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; + +class MetricsLoggerBuilderTest { + + 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 = MetricsLoggerFactory.class.getDeclaredField("metricsLogger"); + field.setAccessible(true); + field.set(null, null); + + field = MetricsLoggerFactory.class.getDeclaredField("provider"); + field.setAccessible(true); + field.set(null, new software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider()); + } + + @Test + void shouldBuildWithCustomNamespace() throws Exception { + // When + MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + .withNamespace("CustomNamespace") + .build(); + + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + metricsLogger.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 + MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + .withService("CustomService") + .build(); + + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + metricsLogger.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 + MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + .withRaiseOnEmptyMetrics(true) + .build(); + + // Then + assertThat(metricsLogger).isNotNull(); + assertThatThrownBy(metricsLogger::flush) + .isInstanceOf(IllegalStateException.class) + .hasMessage("No metrics were emitted"); + } + + @Test + void shouldBuildWithDefaultDimension() throws Exception { + // When + MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + .withDefaultDimension("Environment", "Test") + .build(); + + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + metricsLogger.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 + MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + .withDefaultDimensions(Map.of("Environment", "Test", "Region", "us-west-2")) + .build(); + + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + metricsLogger.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 mockProvider = mock(MetricsProvider.class); + MetricsLogger mockLogger = mock(MetricsLogger.class); + when(mockProvider.getMetricsLogger()).thenReturn(mockLogger); + + // When + MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + .withMetricsProvider(mockProvider) + .build(); + + // Then + assertThat(metricsLogger).isSameAs(mockLogger); + } + + @Test + void shouldOverrideServiceWithDefaultDimensions() throws Exception { + // When + MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + .withService("OriginalService") + .withDefaultDimensions(Map.of("Service", "OverriddenService")) + .build(); + + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + metricsLogger.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/MetricsLoggerFactoryTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java new file mode 100644 index 000000000..515fbe410 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +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; + +class MetricsLoggerFactoryTest { + + 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 + public 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 + public void tearDown() throws Exception { + System.setOut(standardOut); + + // Reset the singleton state between tests + java.lang.reflect.Field field = MetricsLoggerFactory.class.getDeclaredField("metricsLogger"); + field.setAccessible(true); + field.set(null, null); + + field = MetricsLoggerFactory.class.getDeclaredField("provider"); + field.setAccessible(true); + field.set(null, new software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider()); + } + + @Test + public void shouldGetMetricsLoggerInstance() { + // When + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + + // Then + assertThat(metricsLogger).isNotNull(); + } + + @Test + public void shouldReturnSameInstanceOnMultipleCalls() { + // When + MetricsLogger firstInstance = MetricsLoggerFactory.getMetricsLogger(); + MetricsLogger secondInstance = MetricsLoggerFactory.getMetricsLogger(); + + // Then + assertThat(firstInstance).isSameAs(secondInstance); + } + + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = TEST_NAMESPACE) + public void shouldUseNamespaceFromEnvironmentVariable() throws Exception { + // When + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + metricsLogger.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) + public void shouldUseServiceNameFromEnvironmentVariable() throws Exception { + // When + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + metricsLogger.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 + public void shouldSetCustomMetricsProvider() { + // Given + MetricsProvider mockProvider = mock(MetricsProvider.class); + MetricsLogger mockLogger = mock(MetricsLogger.class); + when(mockProvider.getMetricsLogger()).thenReturn(mockLogger); + + // When + MetricsLoggerFactory.setMetricsProvider(mockProvider); + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + + // Then + assertThat(metricsLogger).isSameAs(mockLogger); + } + + @Test + public void shouldThrowExceptionWhenSettingNullProvider() { + // When/Then + assertThatThrownBy(() -> MetricsLoggerFactory.setMetricsProvider(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metrics provider cannot be null"); + } +} 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..b2dca8592 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLoggerTest.java @@ -0,0 +1,390 @@ +/* + * 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 static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.Map; +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 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.MetricsLogger; +import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; +import software.amazon.lambda.powertools.metrics.model.MetricResolution; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; + +class EmfMetricsLoggerTest { + + private MetricsLogger metricsLogger; + 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); + + metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @AfterEach + void tearDown() throws Exception { + System.setOut(standardOut); + + // Reset the singleton state between tests + java.lang.reflect.Field field = MetricsLoggerFactory.class.getDeclaredField("metricsLogger"); + 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(metricsLogger, 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 + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + metricsLogger.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 + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT, MetricResolution.HIGH); + metricsLogger.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 + metricsLogger.clearDefaultDimensions(); // Clear default Service dimension first for easier assertions + metricsLogger.addDimension("CustomDimension", "CustomValue"); + metricsLogger.addMetric("test-metric", 100); + metricsLogger.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 shouldAddMetadata() throws Exception { + // When + metricsLogger.addMetadata("CustomMetadata", "MetadataValue"); + metricsLogger.addMetric("test-metric", 100); + metricsLogger.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 { + // When + metricsLogger.setDefaultDimensions(Map.of("Service", "TestService", "Environment", "Test")); + metricsLogger.addMetric("test-metric", 100); + metricsLogger.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() { + // When + metricsLogger.setDefaultDimensions(Map.of("Service", "TestService", "Environment", "Test")); + DimensionSet dimensions = metricsLogger.getDefaultDimensions(); + + // Then + assertThat(dimensions.getDimensions()).containsEntry("Service", "TestService"); + assertThat(dimensions.getDimensions()).containsEntry("Environment", "Test"); + } + + @Test + void shouldSetNamespace() throws Exception { + // When + metricsLogger.setNamespace("CustomNamespace"); + metricsLogger.addMetric("test-metric", 100); + metricsLogger.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 + metricsLogger.setRaiseOnEmptyMetrics(true); + + // Then + assertThatThrownBy(() -> metricsLogger.flush()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("No metrics were emitted"); + } + + @Test + void shouldClearDefaultDimensions() throws Exception { + // Given + metricsLogger.setDefaultDimensions(Map.of("Service", "TestService", "Environment", "Test")); + + // When + metricsLogger.clearDefaultDimensions(); + metricsLogger.addMetric("test-metric", 100); + metricsLogger.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 context = mock(Context.class); + when(context.getFunctionName()).thenReturn("testFunction"); + when(context.getAwsRequestId()).thenReturn("testRequestId"); + + // When + metricsLogger.captureColdStartMetric(context); + + // 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("testRequestId"); + } + + @Test + void shouldCaptureColdStartMetricWithDimensions() throws Exception { + // Given + DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); + + // When + metricsLogger.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 + metricsLogger.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"; + metricsLogger.setNamespace(customNamespace); + + Context context = mock(Context.class); + when(context.getFunctionName()).thenReturn("testFunction"); + when(context.getAwsRequestId()).thenReturn("testRequestId"); + + DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); + + // When + metricsLogger.captureColdStartMetric(context, 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("testRequestId"); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo(customNamespace); + } + + @Test + void shouldPushSingleMetric() throws Exception { + // Given + DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); + + // When + metricsLogger.pushSingleMetric("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 shouldPushSingleMetricWithoutDimensions() throws Exception { + // When + metricsLogger.pushSingleMetric("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"); + } + +} diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java new file mode 100644 index 000000000..2564f5c15 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java @@ -0,0 +1,298 @@ +/* + * 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 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.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsLogger; +import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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 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 = MetricsLoggerFactory.class.getDeclaredField("metricsLogger"); + field.setAccessible(true); + field.set(null, null); + } + + @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"); + } + + @Test + @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 = "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 + 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 + 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 + 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 + 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(); + } + + static class HandlerWithMetricsAnnotation implements RequestHandler, String> { + @Override + @Metrics(namespace = "CustomNamespace", service = "CustomService") + public String handleRequest(Map input, Context context) { + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } + + static class HandlerWithDefaultMetricsAnnotation implements RequestHandler, String> { + @Override + @Metrics + public String handleRequest(Map input, Context context) { + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } + + static class HandlerWithColdStartMetricsAnnotation implements RequestHandler, String> { + @Override + @Metrics(captureColdStart = true) + public String handleRequest(Map input, Context context) { + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } + + static class HandlerWithCustomFunctionName implements RequestHandler, String> { + @Override + @Metrics(captureColdStart = true, functionName = "CustomFunction") + public String handleRequest(Map input, Context context) { + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } + + static class HandlerWithServiceNameAndColdStart implements RequestHandler, String> { + @Override + @Metrics(service = "CustomService", captureColdStart = true) + public String handleRequest(Map input, Context context) { + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } + + static class HandlerWithAnnotationOnWrongMethod implements RequestHandler, String> { + @Override + public String handleRequest(Map input, Context context) { + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + someOtherMethod(); + return "OK"; + } + + @Metrics + public void someOtherMethod() { + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + } + } +} 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..78fac98b7 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/model/DimensionSetTest.java @@ -0,0 +1,199 @@ +/* + * 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 map with 9 dimensions (9 is maximum) + Map dimensions = Map.of( + "Key1", "Value1", "Key2", "Value2", "Key3", "Value3", "Key4", "Value4", "Key5", "Value5", + "Key6", "Value6", "Key7", "Value7", "Key8", "Value8", "Key9", "Value9"); + DimensionSet dimensionSet = DimensionSet.of(dimensions); + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension("Key10", "Value10")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Cannot exceed 9 dimensions per dimension set"); + } + + @Test + void shouldThrowExceptionWhenKeyIsNull() { + // Given + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension(null, "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension key cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenKeyIsEmpty() { + // Given + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension("", "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension key cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenValueIsNull() { + // Given + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension("Key", null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value cannot be null"); + } +} 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..f961366e4 --- /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.MetricsLogger; +import software.amazon.lambda.powertools.metrics.internal.EmfMetricsLogger; + +class EmfMetricsProviderTest { + + @Test + void shouldCreateEmfMetricsLogger() { + // Given + EmfMetricsProvider provider = new EmfMetricsProvider(); + + // When + MetricsLogger logger = provider.getMetricsLogger(); + + // Then + assertThat(logger) + .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; + } +} From 6b818059d5fa8b4d4a0191b138d3cf59cf16ab7f Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 11:58:11 +0200 Subject: [PATCH 05/36] Update metrics documentation with new interface. --- docs/core/metrics.md | 302 +++++++++++++++++++++++++++++-------------- 1 file changed, 204 insertions(+), 98 deletions(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 6083d935a..0871d788d 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 `e-commerce-app`. +- **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: `CartUpdated` or `ProductAdded`. +- **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,41 @@ 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 two 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. `e-commerce-app` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` | +| **Service** | Optionally, sets **service** metric dimension across all metrics e.g. `product-service` | `POWERTOOLS_SERVICE_NAME` | `service` | !!! tip "Use your application or main service as the metric namespace to easily group all metrics" + +!!!info "Order of Precedence of `MetricsLogger` configuration" + The `MetricsLogger` Singleton can be configured by three different interfaces. The following order of precedence applies: + + 1. `@Metrics` annotation (recommended) + 2. `MetricsLoggerBuilder` 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 `@Metrics` annotation or `MetricsLoggerBuilder` if the annotation cannot be used. + + === "template.yaml" ```yaml hl_lines="9 10" @@ -123,95 +140,107 @@ Metric has two global settings that will be used across all metrics emitted: Runtime: java8 Environment: Variables: - POWERTOOLS_SERVICE_NAME: payment - POWERTOOLS_METRICS_NAMESPACE: ServerlessAirline + POWERTOOLS_SERVICE_NAME: product-service + POWERTOOLS_METRICS_NAMESPACE: e-commerce-app ``` === "MetricsEnabledHandler.java" - ```java hl_lines="8" + ```java hl_lines="9" import software.amazon.lambda.powertools.metrics.Metrics; - + import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; + public class MetricsEnabledHandler implements RequestHandler { - - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); - + + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + @Override - @Metrics(namespace = "ExampleApplication", service = "booking") + @Metrics(namespace = "e-commerce-app", service = "product-service") 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. +`MetricsLogger` 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 `@Metrics` annotation must be added on the lambda handler. + +!!!info "You can use the Metrics utility without the `@Metrics` 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 `MetricsLogger` Singleton using the `MetricsLoggerFactory`. === "MetricsEnabledHandler.java" - ```java hl_lines="11 12" + ```java hl_lines="13" import software.amazon.lambda.powertools.metrics.Metrics; - import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; + import software.amazon.lambda.powertools.metrics.MetricsLogger; + import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; + import software.amazon.lambda.powertools.metrics.model.MetricUnit; public class MetricsEnabledHandler implements RequestHandler { - - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); - + + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + @Override - @Metrics(namespace = "ExampleApplication", service = "booking") + @Metrics(namespace = "e-commerce-app", service = "product-service") public Object handleRequest(Object input, Context context) { - metricsLogger.putDimensions(DimensionSet.of("environment", "prod")); - metricsLogger.putMetric("SuccessfulBooking", 1, Unit.COUNT); + metricsLogger.addDimension("environment", "prod"); + metricsLogger.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 `MetricResolution.HIGH` to the `addMetric` method: === "HigResMetricsHandler.java" ```java hl_lines="3 13" 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.MetricsLogger; + import software.amazon.lambda.powertools.metrics.model.MetricResolution; public class MetricsEnabledHandler implements RequestHandler { - - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); - + + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + @Override - @Metrics(namespace = "ExampleApplication", service = "booking") + @Metrics(namespace = "e-commerce-app", service = "product-service") public Object handleRequest(Object input, Context context) { // ... - metricsLogger.putMetric("SuccessfulBooking", 1, Unit.COUNT, StorageResolution.HIGH); + metricsLogger.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. + ### 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 `@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, `IllegalStateException` or `IllegalArgumentException` exceptions will be raised. + !!! tip "Metric validation" - * Maximum of 9 dimensions + - Maximum of 9 dimensions + - 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: @@ -251,33 +280,58 @@ 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.Metrics; + + public class MetricsColdStartCustomFunction implements RequestHandler { + + @Override + @Metrics(captureColdStart = true, functionName = "CustomFunction") + public Object handleRequest(Object input, Context context) { + ... + } + } + ``` + ## Advanced -## Adding metadata +### Adding metadata + +You can use `addMetadata` for advanced use cases, where you want to add metadata as part of the serialized metrics object. -You can use `putMetadata` for advanced use cases, where you want to metadata as part of the serialized metrics object. + +!!! info + This will not be available during metrics visualization, use Dimensions for this purpose. !!! info - **This will not be available during metrics visualization, use `dimensions` for this purpose.** + 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.Metrics; - import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; + import software.amazon.lambda.powertools.metrics.MetricsLogger; + import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; public class App implements RequestHandler { + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + @Override - @Metrics(namespace = "ServerlessAirline", service = "payment") + @Metrics(namespace = "e-commerce-app", service = "booking-service") public Object handleRequest(Object input, Context context) { - metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); - metricsLogger().putMetadata("booking_id", "1234567890"); + metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + metricsLogger.addMetadata("booking_id", "1234567890"); ... } } @@ -285,79 +339,131 @@ 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 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()`. +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. + +If you wish to set custom default dimensions, it can be done via `#!java metricsLogger.setDefaultDimensions()`. You can also use the `MetricsLoggerBuilder` instead of the `MetricsLoggerFactory` to configure **and** retrieve the `MetricsLogger` Singleton at the same time (see `MetricsLoggerBuilder.java` tab). === "App.java" - ```java hl_lines="8 9 10" + ```java hl_lines="13" import software.amazon.lambda.powertools.metrics.Metrics; - import static software.amazon.lambda.powertools.metrics.MetricsUtils; - + import software.amazon.lambda.powertools.metrics.MetricsLogger; + import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; + 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 MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + + @Override + @Metrics(namespace = "e-commerce-app", service = "product-service") + public Object handleRequest(Object input, Context context) { + metricsLogger.setDefaultDimensions(Map.of("CustomDimension", "booking", "Environment", "prod")); + ... } - + } + ``` + +=== "MetricsLoggerBuilder.java" + + ```java hl_lines="8-10" + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.MetricsLogger; + import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + + public class App implements RequestHandler { + + private static final MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + .withDefaultDimensions(Map.of("CustomDimension", "booking", "Environment", "prod")) + .build(); + @Override - @Metrics(namespace = "ExampleApplication", service = "booking") + @Metrics(namespace = "e-commerce-app", service = "product-service") public Object handleRequest(Object input, Context context) { + metricsLogger.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 `pushSingleMetric`: === "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.MetricsLogger; + import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + import software.amazon.lambda.powertools.metrics.model.MetricUnit; public class App implements RequestHandler { + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override + @Metrics(namespace = "e-commerce-app", service = "product-service") public Object handleRequest(Object input, Context context) { - withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> { - metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); - }); + metricsLogger.pushSingleMetric( + "CustomMetric", + 1, + MetricUnit.COUNT, + "CustomNamespace", + DimensionSet.of("CustomDimension", "value") // Dimensions are optional + ); } } ``` -## Creating metrics with different configurations + +!!! info + Generally, this would be an edge case since you [pay for unique metric](https://aws.amazon.com/cloudwatch/pricing). Keep the following formula in mind: + + **unique metric = (metric_name + dimension_name + dimension_value)** + + +### Usage without `@Metrics` annotation + +The `MetricsLogger` provides all configuration options via `MetricsLoggerBuilder` in addition to the `@Metrics` 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 `MetricsLoggerBuilder` if needed." -Use `withMetricsLogger` if you have one or more metrics that should have different configurations e.g. dimensions or namespace. +The following example shows how to configure a custom `MetricsLogger` 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.MetricsLogger; + import software.amazon.lambda.powertools.metrics.MetricsLoggerBuilder; + 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 MetricsLogger singleton without annotation + private static final MetricsLogger customLogger = MetricsLoggerBuilder.builder() + .withNamespace("e-commerce-app") + .withRaiseOnEmptyMetrics(true) + .withService("product-service") + .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. + customLogger.captureColdStartMetric(context, DimensionSet.of("FunctionName", "MyFunction", "Service", "product-service")); + + // Add metrics to the custom logger + customLogger.addMetric("CustomMetric", 1, MetricUnit.COUNT); + customLogger.flush(); } } ``` From 86cb3ce3bb5affbf4cdbdc7a432c7cffad136f27 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 13:51:58 +0200 Subject: [PATCH 06/36] Make unit tests compatible with GraalVM. It is not supported to mock interface with Mockito. --- .../metrics/MetricsLoggerBuilderTest.java | 12 ++- .../metrics/MetricsLoggerFactoryTest.java | 13 ++- .../internal/EmfMetricsLoggerTest.java | 17 ++-- .../metrics/testutils/TestMetricsLogger.java | 80 +++++++++++++++++++ .../testutils/TestMetricsProvider.java | 11 +++ 5 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsProvider.java diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java index 05967d90d..0fe7d20d7 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java @@ -16,8 +16,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import java.io.ByteArrayOutputStream; import java.io.PrintStream; @@ -32,6 +30,8 @@ import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; +import software.amazon.lambda.powertools.metrics.testutils.TestMetricsLogger; +import software.amazon.lambda.powertools.metrics.testutils.TestMetricsProvider; class MetricsLoggerBuilderTest { @@ -149,17 +149,15 @@ void shouldBuildWithMultipleDefaultDimensions() throws Exception { @Test void shouldBuildWithCustomMetricsProvider() { // Given - MetricsProvider mockProvider = mock(MetricsProvider.class); - MetricsLogger mockLogger = mock(MetricsLogger.class); - when(mockProvider.getMetricsLogger()).thenReturn(mockLogger); + MetricsProvider testProvider = new TestMetricsProvider(); // When MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() - .withMetricsProvider(mockProvider) + .withMetricsProvider(testProvider) .build(); // Then - assertThat(metricsLogger).isSameAs(mockLogger); + assertThat(metricsLogger).isInstanceOf(TestMetricsLogger.class); } @Test diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java index 515fbe410..28a976707 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java @@ -16,8 +16,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import java.io.ByteArrayOutputStream; import java.io.PrintStream; @@ -34,6 +32,8 @@ 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.TestMetricsLogger; +import software.amazon.lambda.powertools.metrics.testutils.TestMetricsProvider; class MetricsLoggerFactoryTest { @@ -127,16 +127,14 @@ public void shouldUseServiceNameFromEnvironmentVariable() throws Exception { @Test public void shouldSetCustomMetricsProvider() { // Given - MetricsProvider mockProvider = mock(MetricsProvider.class); - MetricsLogger mockLogger = mock(MetricsLogger.class); - when(mockProvider.getMetricsLogger()).thenReturn(mockLogger); + MetricsProvider testProvider = new TestMetricsProvider(); // When - MetricsLoggerFactory.setMetricsProvider(mockProvider); + MetricsLoggerFactory.setMetricsProvider(testProvider); MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); // Then - assertThat(metricsLogger).isSameAs(mockLogger); + assertThat(metricsLogger).isInstanceOf(TestMetricsLogger.class); } @Test @@ -146,4 +144,5 @@ public void shouldThrowExceptionWhenSettingNullProvider() { .isInstanceOf(IllegalArgumentException.class) .hasMessage("Metrics provider cannot be null"); } + } 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 index b2dca8592..9a21f69e4 100644 --- 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 @@ -43,6 +43,7 @@ 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 { @@ -275,12 +276,10 @@ void shouldClearDefaultDimensions() throws Exception { @Test void shouldCaptureColdStartMetric() throws Exception { // Given - Context context = mock(Context.class); - when(context.getFunctionName()).thenReturn("testFunction"); - when(context.getAwsRequestId()).thenReturn("testRequestId"); + Context testContext = new TestContext(); // When - metricsLogger.captureColdStartMetric(context); + metricsLogger.captureColdStartMetric(testContext); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -289,7 +288,7 @@ void shouldCaptureColdStartMetric() throws Exception { 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("testRequestId"); + assertThat(rootNode.get("function_request_id").asText()).isEqualTo(testContext.getAwsRequestId()); } @Test @@ -329,14 +328,12 @@ void shouldReuseNamespaceForColdStartMetric() throws Exception { String customNamespace = "CustomNamespace"; metricsLogger.setNamespace(customNamespace); - Context context = mock(Context.class); - when(context.getFunctionName()).thenReturn("testFunction"); - when(context.getAwsRequestId()).thenReturn("testRequestId"); + Context testContext = new TestContext(); DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); // When - metricsLogger.captureColdStartMetric(context, dimensions); + metricsLogger.captureColdStartMetric(testContext, dimensions); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -347,7 +344,7 @@ void shouldReuseNamespaceForColdStartMetric() throws Exception { 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("testRequestId"); + assertThat(rootNode.get("function_request_id").asText()).isEqualTo(testContext.getAwsRequestId()); assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) .isEqualTo(customNamespace); } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java new file mode 100644 index 000000000..4923c33c6 --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java @@ -0,0 +1,80 @@ +package software.amazon.lambda.powertools.metrics.testutils; + +import java.util.Collections; +import java.util.Map; + +import com.amazonaws.services.lambda.runtime.Context; + +import software.amazon.lambda.powertools.metrics.MetricsLogger; +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 TestMetricsLogger implements MetricsLogger { + @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(String key, String value) { + // Test placeholder + } + + @Override + public void addMetadata(String key, Object value) { + // Test placeholder + } + + @Override + public void setDefaultDimensions(Map defaultDimensions) { + // 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 captureColdStartMetric(Context context, DimensionSet dimensions) { + // Test placeholder + } + + @Override + public void captureColdStartMetric(DimensionSet dimensions) { + // Test placeholder + } + + @Override + public void pushSingleMetric(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..d43a35b3c --- /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.MetricsLogger; +import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; + +public class TestMetricsProvider implements MetricsProvider { + @Override + public MetricsLogger getMetricsLogger() { + return new TestMetricsLogger(); + } +} From 483766cff351aa02d44b1293b28a53084722196e Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 14:02:53 +0200 Subject: [PATCH 07/36] Add GraalVM support bullet points to docs pages. --- docs/core/logging.md | 1 + docs/core/tracing.md | 1 + docs/utilities/parameters.md | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/core/logging.md b/docs/core/logging.md index 1160f62ff..08ce7ad27 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 diff --git a/docs/core/tracing.md b/docs/core/tracing.md index ea3174fba..b6e142609 100644 --- a/docs/core/tracing.md +++ b/docs/core/tracing.md @@ -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/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 From a14c43c773d1540f0ebb3ef62cca9cd0bf287515 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 14:19:44 +0200 Subject: [PATCH 08/36] Update GraalVM metadata files after re-implementing metrics module. --- .../powertools-metrics/jni-config.json | 47 +- .../powertools-metrics/reflect-config.json | 645 ++++++++++-------- .../powertools-metrics/resource-config.json | 28 +- 3 files changed, 400 insertions(+), 320 deletions(-) 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..410a0d0cd 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,29 @@ [ -{ - "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.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..dcf829942 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,364 @@ [ -{ - "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.ConfigurationPrecedenceTest", + "allDeclaredFields": true, + "allDeclaredClasses": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "", "parameterTypes": [] }, + { "name": "annotationShouldOverrideBuilderAndEnvironment", "parameterTypes": [] }, + { "name": "builderShouldOverrideEnvironment", "parameterTypes": [] }, + { "name": "environmentVariablesShouldBeUsedWhenNoOverrides", "parameterTypes": [] }, + { "name": "setUp", "parameterTypes": [] }, + { "name": "shouldUseDefaultsWhenNoConfiguration", "parameterTypes": [] }, + { "name": "tearDown", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.ConfigurationPrecedenceTest$HandlerWithDefaultMetricsAnnotation", + "methods": [ + { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.ConfigurationPrecedenceTest$HandlerWithMetricsAnnotation", + "methods": [ + { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.MetricsLogger", + "allDeclaredClasses": true, + "queryAllPublicMethods": true + }, + { + "name": "software.amazon.lambda.powertools.metrics.MetricsLoggerBuilderTest", + "allDeclaredFields": true, + "allDeclaredClasses": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "", "parameterTypes": [] }, + { "name": "setUp", "parameterTypes": [] }, + { "name": "shouldBuildWithCustomMetricsProvider", "parameterTypes": [] }, + { "name": "shouldBuildWithCustomNamespace", "parameterTypes": [] }, + { "name": "shouldBuildWithCustomService", "parameterTypes": [] }, + { "name": "shouldBuildWithDefaultDimension", "parameterTypes": [] }, + { "name": "shouldBuildWithMultipleDefaultDimensions", "parameterTypes": [] }, + { "name": "shouldBuildWithRaiseOnEmptyMetrics", "parameterTypes": [] }, + { "name": "shouldOverrideServiceWithDefaultDimensions", "parameterTypes": [] }, + { "name": "tearDown", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.MetricsLoggerFactory", + "fields": [{ "name": "metricsLogger" }, { "name": "provider" }] + }, + { + "name": "software.amazon.lambda.powertools.metrics.MetricsLoggerFactoryTest", + "allDeclaredFields": true, + "allDeclaredClasses": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "", "parameterTypes": [] }, + { "name": "setUp", "parameterTypes": [] }, + { "name": "shouldGetMetricsLoggerInstance", "parameterTypes": [] }, + { "name": "shouldReturnSameInstanceOnMultipleCalls", "parameterTypes": [] }, + { "name": "shouldSetCustomMetricsProvider", "parameterTypes": [] }, + { "name": "shouldThrowExceptionWhenSettingNullProvider", "parameterTypes": [] }, + { "name": "shouldUseNamespaceFromEnvironmentVariable", "parameterTypes": [] }, + { "name": "shouldUseServiceNameFromEnvironmentVariable", "parameterTypes": [] }, + { "name": "tearDown", "parameterTypes": [] } + ] + }, + { + "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.internal.EmfMetricsLoggerTest", + "allDeclaredFields": true, + "allDeclaredClasses": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "", "parameterTypes": [] }, + { "name": "setUp", "parameterTypes": [] }, + { "name": "shouldAddDimension", "parameterTypes": [] }, + { "name": "shouldAddMetadata", "parameterTypes": [] }, + { "name": "shouldCaptureColdStartMetric", "parameterTypes": [] }, + { "name": "shouldCaptureColdStartMetricWithDimensions", "parameterTypes": [] }, + { "name": "shouldCaptureColdStartMetricWithoutDimensions", "parameterTypes": [] }, + { "name": "shouldClearDefaultDimensions", "parameterTypes": [] }, + { + "name": "shouldConvertMetricUnits", + "parameterTypes": [ + "software.amazon.lambda.powertools.metrics.model.MetricUnit", + "software.amazon.cloudwatchlogs.emf.model.Unit" + ] + }, + { "name": "shouldCreateMetricWithDefaultResolution", "parameterTypes": [] }, + { "name": "shouldCreateMetricWithHighResolution", "parameterTypes": [] }, + { "name": "shouldGetDefaultDimensions", "parameterTypes": [] }, + { "name": "shouldPushSingleMetric", "parameterTypes": [] }, + { "name": "shouldPushSingleMetricWithoutDimensions", "parameterTypes": [] }, + { "name": "shouldRaiseExceptionOnEmptyMetrics", "parameterTypes": [] }, + { "name": "shouldReuseNamespaceForColdStartMetric", "parameterTypes": [] }, + { "name": "shouldSetDefaultDimensions", "parameterTypes": [] }, + { "name": "shouldSetNamespace", "parameterTypes": [] }, + { "name": "tearDown", "parameterTypes": [] }, + { "name": "unitConversionTestCases", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest", + "allDeclaredFields": true, + "allDeclaredClasses": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "", "parameterTypes": [] }, + { "name": "setUp", "parameterTypes": [] }, + { "name": "shouldCaptureColdStartMetricWhenConfigured", "parameterTypes": [] }, + { "name": "shouldCaptureMetricsFromAnnotatedHandler", "parameterTypes": [] }, + { "name": "shouldHaveNoEffectOnNonHandlerMethod", "parameterTypes": [] }, + { "name": "shouldOverrideEnvironmentVariablesWithAnnotation", "parameterTypes": [] }, + { "name": "shouldUseCustomFunctionNameWhenProvidedForColdStartMetric", "parameterTypes": [] }, + { "name": "shouldUseEnvironmentVariablesWhenNoAnnotationOverrides", "parameterTypes": [] }, + { "name": "shouldUseServiceNameWhenProvidedForColdStartMetric", "parameterTypes": [] }, + { "name": "tearDown", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest$HandlerWithAnnotationOnWrongMethod", + "methods": [{ "name": "someOtherMethod", "parameterTypes": [] }] + }, + { + "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest$HandlerWithColdStartMetricsAnnotation", + "methods": [ + { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest$HandlerWithCustomFunctionName", + "methods": [ + { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest$HandlerWithDefaultMetricsAnnotation", + "methods": [ + { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest$HandlerWithMetricsAnnotation", + "methods": [ + { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest$HandlerWithServiceNameAndColdStart", + "methods": [ + { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.model.DimensionSetTest", + "allDeclaredFields": true, + "allDeclaredClasses": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "", "parameterTypes": [] }, + { "name": "shouldCreateDimensionSetFromMap", "parameterTypes": [] }, + { "name": "shouldCreateDimensionSetWithFiveKeyValues", "parameterTypes": [] }, + { "name": "shouldCreateDimensionSetWithFourKeyValues", "parameterTypes": [] }, + { "name": "shouldCreateDimensionSetWithSingleKeyValue", "parameterTypes": [] }, + { "name": "shouldCreateDimensionSetWithThreeKeyValues", "parameterTypes": [] }, + { "name": "shouldCreateDimensionSetWithTwoKeyValues", "parameterTypes": [] }, + { "name": "shouldCreateEmptyDimensionSet", "parameterTypes": [] }, + { "name": "shouldGetDimensionValue", "parameterTypes": [] }, + { "name": "shouldReturnNullForNonExistentDimension", "parameterTypes": [] }, + { "name": "shouldThrowExceptionWhenExceedingMaxDimensions", "parameterTypes": [] }, + { "name": "shouldThrowExceptionWhenKeyIsEmpty", "parameterTypes": [] }, + { "name": "shouldThrowExceptionWhenKeyIsNull", "parameterTypes": [] }, + { "name": "shouldThrowExceptionWhenValueIsNull", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.provider.EmfMetricsProviderTest", + "allDeclaredFields": true, + "allDeclaredClasses": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "queryAllDeclaredConstructors": true, + "methods": [ + { "name": "", "parameterTypes": [] }, + { "name": "shouldCreateEmfMetricsLogger", "parameterTypes": [] } + ] + }, + { + "name": "software.amazon.lambda.powertools.metrics.provider.MetricsProvider", + "allDeclaredClasses": true, + "queryAllPublicMethods": true + }, + { + "name": "software.amazon.lambda.powertools.metrics.testutils.TestContext", + "allDeclaredClasses": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true + }, + { + "name": "software.amazon.lambda.powertools.metrics.testutils.TestMetricsLogger", + "allDeclaredClasses": true, + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true + }, + { + "name": "software.amazon.lambda.powertools.metrics.testutils.TestMetricsProvider", + "allDeclaredClasses": true, + "queryAllDeclaredMethods": 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": [] } From 17c865dd62bb852d30d25bea132b07001fceba59 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 14:46:00 +0200 Subject: [PATCH 09/36] Fix spotbugs issue. This is expected for this factory class for MetricsLogger to enable user configuration of the MetricsLogger singleton. --- spotbugs-exclude.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index e959204ad..0e69cf902 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -234,8 +234,8 @@ - - + + From 4085b0d4ec13bd774cadfa3e917474f615338cab Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 15:01:00 +0200 Subject: [PATCH 10/36] Fix SonarCube findings. --- .../metrics/internal/LambdaMetricsAspect.java | 9 +++++---- .../metrics/MetricsLoggerFactoryTest.java | 16 ++++++++-------- .../metrics/internal/EmfMetricsLoggerTest.java | 2 -- 3 files changed, 13 insertions(+), 14 deletions(-) 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 0c9cac9c7..a1e52a794 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 @@ -37,6 +37,7 @@ 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 SERVICE_DIMENSION = "Service"; private String functionName(Metrics metrics, Context context) { if (!"".equals(metrics.functionName())) { @@ -76,8 +77,8 @@ public Object around(ProceedingJoinPoint pjp, // default dimension. if (!"".equals(metrics.service()) && logger.getDefaultDimensions().getDimensionKeys().size() <= 1 - && logger.getDefaultDimensions().getDimensionKeys().contains("Service")) { - logger.setDefaultDimensions(Map.of("Service", metrics.service())); + && logger.getDefaultDimensions().getDimensionKeys().contains(SERVICE_DIMENSION)) { + logger.setDefaultDimensions(Map.of(SERVICE_DIMENSION, metrics.service())); } logger.setRaiseOnEmptyMetrics(metrics.raiseOnEmptyMetrics()); @@ -98,8 +99,8 @@ public Object around(ProceedingJoinPoint pjp, // Create dimensions with service and function name DimensionSet coldStartDimensions = DimensionSet.of( - "Service", - logger.getDefaultDimensions().getDimensions().getOrDefault("Service", + SERVICE_DIMENSION, + logger.getDefaultDimensions().getDimensions().getOrDefault(SERVICE_DIMENSION, serviceNameWithFallback(metrics)), "FunctionName", funcName != null ? funcName : extractedContext.getFunctionName()); diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java index 28a976707..f312995b3 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java @@ -45,7 +45,7 @@ class MetricsLoggerFactoryTest { private final ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach - public void setUp() throws Exception { + void setUp() throws Exception { System.setOut(new PrintStream(outputStreamCaptor)); // Reset LambdaHandlerProcessor's SERVICE_NAME @@ -60,7 +60,7 @@ public void setUp() throws Exception { } @AfterEach - public void tearDown() throws Exception { + void tearDown() throws Exception { System.setOut(standardOut); // Reset the singleton state between tests @@ -74,7 +74,7 @@ public void tearDown() throws Exception { } @Test - public void shouldGetMetricsLoggerInstance() { + void shouldGetMetricsLoggerInstance() { // When MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @@ -83,7 +83,7 @@ public void shouldGetMetricsLoggerInstance() { } @Test - public void shouldReturnSameInstanceOnMultipleCalls() { + void shouldReturnSameInstanceOnMultipleCalls() { // When MetricsLogger firstInstance = MetricsLoggerFactory.getMetricsLogger(); MetricsLogger secondInstance = MetricsLoggerFactory.getMetricsLogger(); @@ -94,7 +94,7 @@ public void shouldReturnSameInstanceOnMultipleCalls() { @Test @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = TEST_NAMESPACE) - public void shouldUseNamespaceFromEnvironmentVariable() throws Exception { + void shouldUseNamespaceFromEnvironmentVariable() throws Exception { // When MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -110,7 +110,7 @@ public void shouldUseNamespaceFromEnvironmentVariable() throws Exception { @Test @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = TEST_SERVICE) - public void shouldUseServiceNameFromEnvironmentVariable() throws Exception { + void shouldUseServiceNameFromEnvironmentVariable() throws Exception { // When MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -125,7 +125,7 @@ public void shouldUseServiceNameFromEnvironmentVariable() throws Exception { } @Test - public void shouldSetCustomMetricsProvider() { + void shouldSetCustomMetricsProvider() { // Given MetricsProvider testProvider = new TestMetricsProvider(); @@ -138,7 +138,7 @@ public void shouldSetCustomMetricsProvider() { } @Test - public void shouldThrowExceptionWhenSettingNullProvider() { + void shouldThrowExceptionWhenSettingNullProvider() { // When/Then assertThatThrownBy(() -> MetricsLoggerFactory.setMetricsProvider(null)) .isInstanceOf(IllegalArgumentException.class) 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 index 9a21f69e4..c47b1a462 100644 --- 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 @@ -16,8 +16,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import java.io.ByteArrayOutputStream; import java.io.PrintStream; From ead827baa431cc8b39d2f7fe68290c7ca60981a9 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 16:34:57 +0200 Subject: [PATCH 11/36] Rename back namespace to ServerlessAirline example. --- docs/core/metrics.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 0871d788d..b65c225ca 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -19,9 +19,9 @@ These metrics can be visualized through [Amazon CloudWatch Console](https://aws. 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 `e-commerce-app`. +- **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: `CartUpdated` or `ProductAdded`. +- **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). @@ -155,7 +155,7 @@ Metrics has two global settings that will be used across all metrics emitted. Us private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "e-commerce-app", service = "product-service") + @Metrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { // ... } @@ -183,7 +183,7 @@ You can create metrics using `addMetric`, and manually create dimensions for all private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "e-commerce-app", service = "product-service") + @Metrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { metricsLogger.addDimension("environment", "prod"); metricsLogger.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); @@ -216,7 +216,7 @@ passing a `MetricResolution.HIGH` to the `addMetric` method: private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "e-commerce-app", service = "product-service") + @Metrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { // ... metricsLogger.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT, MetricResolution.HIGH); @@ -328,7 +328,7 @@ You can use `addMetadata` for advanced use cases, where you want to add metadata private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "e-commerce-app", service = "booking-service") + @Metrics(namespace = "ServerlessAirline", service = "booking-service") public Object handleRequest(Object input, Context context) { metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); metricsLogger.addMetadata("booking_id", "1234567890"); @@ -358,7 +358,7 @@ If you wish to set custom default dimensions, it can be done via `#!java metrics private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "e-commerce-app", service = "product-service") + @Metrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { metricsLogger.setDefaultDimensions(Map.of("CustomDimension", "booking", "Environment", "prod")); ... @@ -381,7 +381,7 @@ If you wish to set custom default dimensions, it can be done via `#!java metrics .build(); @Override - @Metrics(namespace = "e-commerce-app", service = "product-service") + @Metrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); ... @@ -410,7 +410,7 @@ You can create a single metric with its own namespace and dimensions using `push private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "e-commerce-app", service = "product-service") + @Metrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { metricsLogger.pushSingleMetric( "CustomMetric", @@ -449,9 +449,9 @@ The following example shows how to configure a custom `MetricsLogger` using the public class App implements RequestHandler { // Create and configure a MetricsLogger singleton without annotation private static final MetricsLogger customLogger = MetricsLoggerBuilder.builder() - .withNamespace("e-commerce-app") + .withNamespace("ServerlessAirline") .withRaiseOnEmptyMetrics(true) - .withService("product-service") + .withService("payment") .build(); @Override From ac56a5b3eed69319f2c811f0ef6334e98d0bcefa Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 17:00:13 +0200 Subject: [PATCH 12/36] Fix DimensionSet validation to be inline with most recent specification. --- pom.xml | 11 +- powertools-metrics/pom.xml | 9 +- .../metrics/model/DimensionSet.java | 42 ++++++- .../metrics/model/DimensionSetTest.java | 108 ++++++++++++++++-- 4 files changed, 147 insertions(+), 23 deletions(-) diff --git a/pom.xml b/pom.xml index 06e371e25..81e513b8c 100644 --- a/pom.xml +++ b/pom.xml @@ -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 diff --git a/powertools-metrics/pom.xml b/powertools-metrics/pom.xml index 0f713cc91..46f7bcd99 100644 --- a/powertools-metrics/pom.xml +++ b/powertools-metrics/pom.xml @@ -65,6 +65,10 @@ com.fasterxml.jackson.core jackson-databind + + org.apache.commons + commons-lang3 + @@ -97,11 +101,6 @@ junit-pioneer test - - org.apache.commons - commons-lang3 - test - org.aspectj aspectjweaver 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 index be0d085ad..b4dfd69aa 100644 --- 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 @@ -18,11 +18,15 @@ import java.util.Map; import java.util.Set; +import org.apache.commons.lang3.StringUtils; + /** * Represents a set of dimensions for CloudWatch metrics */ public class DimensionSet { - private static final int MAX_DIMENSION_SET_SIZE = 9; + private static final int MAX_DIMENSION_SET_SIZE = 30; + private static final int MAX_DIMENSION_NAME_LENGTH = 250; + private static final int MAX_DIMENSION_VALUE_LENGTH = 1024; private final Map dimensions = new LinkedHashMap<>(); @@ -186,12 +190,42 @@ public Map getDimensions() { } private void validateDimension(String key, String value) { - if (key == null || key.isEmpty()) { + if (key == null || key.trim().isEmpty()) { throw new IllegalArgumentException("Dimension key cannot be null or empty"); } - if (value == null) { - throw new IllegalArgumentException("Dimension value cannot be null"); + 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/test/java/software/amazon/lambda/powertools/metrics/model/DimensionSetTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/model/DimensionSetTest.java index 78fac98b7..8a004eb33 100644 --- 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 @@ -152,16 +152,16 @@ void shouldReturnNullForNonExistentDimension() { @Test void shouldThrowExceptionWhenExceedingMaxDimensions() { // Given - // Create a map with 9 dimensions (9 is maximum) - Map dimensions = Map.of( - "Key1", "Value1", "Key2", "Value2", "Key3", "Value3", "Key4", "Value4", "Key5", "Value5", - "Key6", "Value6", "Key7", "Value7", "Key8", "Value8", "Key9", "Value9"); - DimensionSet dimensionSet = DimensionSet.of(dimensions); + // 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("Key10", "Value10")) + assertThatThrownBy(() -> dimensionSet.addDimension("Key31", "Value31")) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Cannot exceed 9 dimensions per dimension set"); + .hasMessageContaining("Cannot exceed 30 dimensions per dimension set"); } @Test @@ -194,6 +194,98 @@ void shouldThrowExceptionWhenValueIsNull() { // When/Then assertThatThrownBy(() -> dimensionSet.addDimension("Key", null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dimension value cannot be null"); + .hasMessage("Dimension value cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenValueIsEmpty() { + // Given + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension("Key", "")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value cannot be null or empty"); + } + + @Test + void shouldThrowExceptionWhenKeyContainsWhitespace() { + // Given + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension("Key With Space", "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension key cannot contain whitespaces: Key With Space"); + } + + @Test + void shouldThrowExceptionWhenValueContainsWhitespace() { + // Given + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension("Key", "Value With Space")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value cannot contain whitespaces: Value With Space"); + } + + @Test + void shouldThrowExceptionWhenKeyStartsWithColon() { + // Given + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension(":Key", "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension key cannot start with colon: :Key"); + } + + @Test + void shouldThrowExceptionWhenKeyExceedsMaxLength() { + // Given + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + String longKey = "a".repeat(251); // MAX_DIMENSION_NAME_LENGTH + 1 + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension(longKey, "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension name exceeds maximum length of 250: " + longKey); + } + + @Test + void shouldThrowExceptionWhenValueExceedsMaxLength() { + // Given + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + String longValue = "a".repeat(1025); // MAX_DIMENSION_VALUE_LENGTH + 1 + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension("Key", longValue)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value exceeds maximum length of 1024: " + longValue); + } + + @Test + void shouldThrowExceptionWhenKeyContainsNonAsciiCharacters() { + // Given + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + String keyWithNonAscii = "Key\u0080"; // Non-ASCII character + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension(keyWithNonAscii, "Value")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension name has invalid characters: " + keyWithNonAscii); + } + + @Test + void shouldThrowExceptionWhenValueContainsNonAsciiCharacters() { + // Given + DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); + String valueWithNonAscii = "Value\u0080"; // Non-ASCII character + + // When/Then + assertThatThrownBy(() -> dimensionSet.addDimension("Key", valueWithNonAscii)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dimension value has invalid characters: " + valueWithNonAscii); } } From feeefdc20b48c67f22399866a6e5a7e46ba8e8eb Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 18:07:15 +0200 Subject: [PATCH 13/36] Overload void addDimension(DimensionSet dimensionSet) to be able to add multi-dimensional dimensions also for non default dimensions. --- .../powertools/metrics/MetricsLogger.java | 32 +++++++++----- .../metrics/internal/EmfMetricsLogger.java | 25 +++++++---- .../internal/EmfMetricsLoggerTest.java | 44 +++++++++++++++++++ .../metrics/testutils/TestMetricsLogger.java | 2 +- 4 files changed, 82 insertions(+), 21 deletions(-) diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java index 53877ed5d..fd0d5b098 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java @@ -58,12 +58,22 @@ default void addMetric(String key, double value) { } /** - * Add a dimension to the metrics logger + * Add a dimension to the metrics logger. + * 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 */ - void addDimension(String key, String value); + default void addDimension(String key, String value) { + addDimension(DimensionSet.of(key, value)); + } + + /** + * Add a dimension set to the metrics logger + * + * @param dimensionSet the dimension set to add + */ + void addDimension(DimensionSet dimensionSet); /** * Add metadata to the metrics logger @@ -79,7 +89,7 @@ default void addMetric(String key, double value) { * @param defaultDimensions map of default dimensions */ void setDefaultDimensions(Map defaultDimensions); - + /** * Get the default dimensions for the metrics logger * @@ -118,7 +128,7 @@ default void addMetric(String key, double value) { * @param dimensions custom dimensions for this metric (optional) */ void captureColdStartMetric(Context context, DimensionSet dimensions); - + /** * Capture cold start metric and flush immediately * @@ -127,19 +137,19 @@ default void addMetric(String key, double value) { 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); - + /** * Capture cold start metric without Lambda context and flush immediately */ default void captureColdStartMetric() { - captureColdStartMetric((DimensionSet)null); + captureColdStartMetric((DimensionSet) null); } /** @@ -152,9 +162,9 @@ default void captureColdStartMetric() { * @param namespace the namespace for the metric * @param dimensions custom dimensions for this metric (optional) */ - void pushSingleMetric(String name, double value, MetricUnit unit, String namespace, - DimensionSet dimensions); - + void pushSingleMetric(String name, double value, MetricUnit unit, String namespace, + DimensionSet dimensions); + /** * Push a single metric with custom dimensions. This creates a separate metrics context * that doesn't affect the default metrics context. @@ -167,4 +177,4 @@ void pushSingleMetric(String name, double value, MetricUnit unit, String namespa default void pushSingleMetric(String name, double value, MetricUnit unit, String namespace) { pushSingleMetric(name, value, unit, namespace, null); } -} \ No newline at end of file +} 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 index 0dec6f4ab..272cc5fb5 100644 --- 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 @@ -64,16 +64,23 @@ public void addMetric(String key, double value, MetricUnit unit, MetricResolutio } @Override - public void addDimension(String key, String value) { - DimensionSet dimensionSet = new DimensionSet(); - try { - dimensionSet.addDimension(key, value); - emfLogger.putDimensions(dimensionSet); - // Update our local copy of default dimensions - defaultDimensions.put(key, value); - } catch (Exception e) { - // Ignore dimension errors + 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 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 index c47b1a462..d93096738 100644 --- 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 @@ -182,6 +182,50 @@ void shouldAddDimension() throws Exception { } assertThat(hasDimension).isTrue(); } + + @Test + void shouldAddDimensionSet() throws Exception { + // Given + DimensionSet dimensionSet = DimensionSet.of("Dim1", "Value1", "Dim2", "Value2"); + + // When + metricsLogger.clearDefaultDimensions(); // Clear default Service dimension first for easier assertions + metricsLogger.addDimension(dimensionSet); + metricsLogger.addMetric("test-metric", 100); + metricsLogger.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(() -> metricsLogger.addDimension((DimensionSet) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("DimensionSet cannot be null"); + } @Test void shouldAddMetadata() throws Exception { diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java index 4923c33c6..32d1dc8ea 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java @@ -27,7 +27,7 @@ public void addMetric(String key, double value, MetricUnit unit, MetricResolutio } @Override - public void addDimension(String key, String value) { + public void addDimension(DimensionSet dimensionSet) { // Test placeholder } From a178128eb4cb9b0ac08b70172a675d2d7b61e0e7 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 18:24:55 +0200 Subject: [PATCH 14/36] Update setDefaultDimensions to take a DimensionSet instead of a Map. --- .../powertools/metrics/MetricsLogger.java | 6 ++--- .../metrics/MetricsLoggerBuilder.java | 13 +++++---- .../metrics/MetricsLoggerFactory.java | 4 +-- .../metrics/internal/EmfMetricsLogger.java | 17 +++++++----- .../metrics/internal/LambdaMetricsAspect.java | 4 +-- .../metrics/MetricsLoggerBuilderTest.java | 6 ++--- .../internal/EmfMetricsLoggerTest.java | 27 ++++++++++++++----- .../metrics/testutils/TestMetricsLogger.java | 2 +- 8 files changed, 49 insertions(+), 30 deletions(-) diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java index fd0d5b098..2aa2462ba 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java @@ -67,7 +67,7 @@ default void addMetric(String key, double value) { default void addDimension(String key, String value) { addDimension(DimensionSet.of(key, value)); } - + /** * Add a dimension set to the metrics logger * @@ -86,9 +86,9 @@ default void addDimension(String key, String value) { /** * Set default dimensions for the metrics logger * - * @param defaultDimensions map of default dimensions + * @param dimensionSet the dimension set to use as default dimensions */ - void setDefaultDimensions(Map defaultDimensions); + void setDefaultDimensions(DimensionSet dimensionSet); /** * Get the default dimensions for the metrics logger diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java index 41785828e..f13c8b2bb 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java @@ -16,6 +16,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; @@ -101,11 +102,13 @@ public MetricsLoggerBuilder withDefaultDimension(String key, String value) { /** * Add default dimensions * - * @param dimensions map of dimensions + * @param dimensionSet the dimension set to add * @return this builder */ - public MetricsLoggerBuilder withDefaultDimensions(Map dimensions) { - this.defaultDimensions.putAll(dimensions); + public MetricsLoggerBuilder withDefaultDimensions(DimensionSet dimensionSet) { + if (dimensionSet != null) { + this.defaultDimensions.putAll(dimensionSet.getDimensions()); + } return this; } @@ -128,12 +131,12 @@ public MetricsLogger build() { metricsLogger.setRaiseOnEmptyMetrics(raiseOnEmptyMetrics); if (service != null) { - metricsLogger.setDefaultDimensions(Map.of("Service", service)); + metricsLogger.setDefaultDimensions(DimensionSet.of("Service", service)); } // If the user provided default dimension, we overwrite the default Service dimension again if (!defaultDimensions.isEmpty()) { - metricsLogger.setDefaultDimensions(defaultDimensions); + metricsLogger.setDefaultDimensions(DimensionSet.of(defaultDimensions)); } return metricsLogger; diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java index 054a889fe..9d772ad14 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java @@ -14,7 +14,7 @@ package software.amazon.lambda.powertools.metrics; -import java.util.Map; +import software.amazon.lambda.powertools.metrics.model.DimensionSet; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider; @@ -45,7 +45,7 @@ public static synchronized MetricsLogger getMetricsLogger() { metricsLogger.setNamespace(envNamespace); } - metricsLogger.setDefaultDimensions(Map.of("Service", LambdaHandlerProcessor.serviceName())); + metricsLogger.setDefaultDimensions(DimensionSet.of("Service", LambdaHandlerProcessor.serviceName())); } return metricsLogger; 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 index 272cc5fb5..92ba9500b 100644 --- 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 @@ -89,18 +89,23 @@ public void addMetadata(String key, Object value) { } @Override - public void setDefaultDimensions(Map defaultDimensions) { - DimensionSet dimensionSet = new DimensionSet(); - defaultDimensions.forEach((key, value) -> { + 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 { - dimensionSet.addDimension(key, value); + emfDimensionSet.addDimension(key, value); } catch (Exception e) { // Ignore dimension errors } }); - emfLogger.setDimensions(dimensionSet); + emfLogger.setDimensions(emfDimensionSet); // Store a copy of the default dimensions - this.defaultDimensions = new LinkedHashMap<>(defaultDimensions); + this.defaultDimensions = new LinkedHashMap<>(dimensions); } @Override 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 a1e52a794..084988c6d 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 @@ -18,8 +18,6 @@ import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.extractContext; import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isHandlerMethod; -import java.util.Map; - import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -78,7 +76,7 @@ public Object around(ProceedingJoinPoint pjp, if (!"".equals(metrics.service()) && logger.getDefaultDimensions().getDimensionKeys().size() <= 1 && logger.getDefaultDimensions().getDimensionKeys().contains(SERVICE_DIMENSION)) { - logger.setDefaultDimensions(Map.of(SERVICE_DIMENSION, metrics.service())); + logger.setDefaultDimensions(DimensionSet.of(SERVICE_DIMENSION, metrics.service())); } logger.setRaiseOnEmptyMetrics(metrics.raiseOnEmptyMetrics()); diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java index 0fe7d20d7..2f6827ce3 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java @@ -19,7 +19,6 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; -import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -28,6 +27,7 @@ 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.TestMetricsLogger; @@ -130,7 +130,7 @@ void shouldBuildWithDefaultDimension() throws Exception { void shouldBuildWithMultipleDefaultDimensions() throws Exception { // When MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() - .withDefaultDimensions(Map.of("Environment", "Test", "Region", "us-west-2")) + .withDefaultDimensions(DimensionSet.of("Environment", "Test", "Region", "us-west-2")) .build(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -165,7 +165,7 @@ void shouldOverrideServiceWithDefaultDimensions() throws Exception { // When MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() .withService("OriginalService") - .withDefaultDimensions(Map.of("Service", "OverriddenService")) + .withDefaultDimensions(DimensionSet.of("Service", "OverriddenService")) .build(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); 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 index d93096738..d9bcb1253 100644 --- 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 @@ -20,7 +20,6 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.lang.reflect.Method; -import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; @@ -182,12 +181,12 @@ void shouldAddDimension() throws Exception { } assertThat(hasDimension).isTrue(); } - + @Test void shouldAddDimensionSet() throws Exception { // Given DimensionSet dimensionSet = DimensionSet.of("Dim1", "Value1", "Dim2", "Value2"); - + // When metricsLogger.clearDefaultDimensions(); // Clear default Service dimension first for easier assertions metricsLogger.addDimension(dimensionSet); @@ -218,7 +217,7 @@ void shouldAddDimensionSet() throws Exception { assertThat(hasDim1).isTrue(); assertThat(hasDim2).isTrue(); } - + @Test void shouldThrowExceptionWhenDimensionSetIsNull() { // When/Then @@ -245,8 +244,11 @@ void shouldAddMetadata() throws Exception { @Test void shouldSetDefaultDimensions() throws Exception { + // Given + DimensionSet dimensionSet = DimensionSet.of("Service", "TestService", "Environment", "Test"); + // When - metricsLogger.setDefaultDimensions(Map.of("Service", "TestService", "Environment", "Test")); + metricsLogger.setDefaultDimensions(dimensionSet); metricsLogger.addMetric("test-metric", 100); metricsLogger.flush(); @@ -262,8 +264,11 @@ void shouldSetDefaultDimensions() throws Exception { @Test void shouldGetDefaultDimensions() { + // Given + DimensionSet dimensionSet = DimensionSet.of("Service", "TestService", "Environment", "Test"); + // When - metricsLogger.setDefaultDimensions(Map.of("Service", "TestService", "Environment", "Test")); + metricsLogger.setDefaultDimensions(dimensionSet); DimensionSet dimensions = metricsLogger.getDefaultDimensions(); // Then @@ -271,6 +276,14 @@ void shouldGetDefaultDimensions() { assertThat(dimensions.getDimensions()).containsEntry("Environment", "Test"); } + @Test + void shouldThrowExceptionWhenDefaultDimensionSetIsNull() { + // When/Then + assertThatThrownBy(() -> metricsLogger.setDefaultDimensions((DimensionSet) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("DimensionSet cannot be null"); + } + @Test void shouldSetNamespace() throws Exception { // When @@ -300,7 +313,7 @@ void shouldRaiseExceptionOnEmptyMetrics() { @Test void shouldClearDefaultDimensions() throws Exception { // Given - metricsLogger.setDefaultDimensions(Map.of("Service", "TestService", "Environment", "Test")); + metricsLogger.setDefaultDimensions(DimensionSet.of("Service", "TestService", "Environment", "Test")); // When metricsLogger.clearDefaultDimensions(); diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java index 32d1dc8ea..0a1b631e8 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java @@ -37,7 +37,7 @@ public void addMetadata(String key, Object value) { } @Override - public void setDefaultDimensions(Map defaultDimensions) { + public void setDefaultDimensions(DimensionSet dimensionSet) { // Test placeholder } From 4515ee4b31a70801c34d61fb55e78abe0fb1d08d Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 19:28:36 +0200 Subject: [PATCH 15/36] Add warning if not emitting metrics and raiseOnEmptyMetrics is false. --- .../metrics/internal/EmfMetricsLogger.java | 12 ++++++++++-- .../metrics/internal/EmfMetricsLoggerTest.java | 18 ++++++++++++++++++ .../src/test/resources/simplelogger.properties | 7 +++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 powertools-metrics/src/test/resources/simplelogger.properties 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 index 92ba9500b..a72c3c902 100644 --- 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 @@ -22,6 +22,9 @@ 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; @@ -38,6 +41,7 @@ * library {@link software.amazon.cloudwatchlogs.emf.logger.MetricsLogger}. */ public class EmfMetricsLogger implements MetricsLogger { + 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"; @@ -136,8 +140,12 @@ public void clearDefaultDimensions() { @Override public void flush() { - if (raiseOnEmptyMetrics && !hasMetrics.get()) { - throw new IllegalStateException("No metrics were emitted"); + if (!hasMetrics.get()) { + if (raiseOnEmptyMetrics) { + throw new IllegalStateException("No metrics were emitted"); + } else { + LOGGER.warn("No metrics were emitted"); + } } emfLogger.flush(); } 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 index d9bcb1253..7ef80bc56 100644 --- 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 @@ -18,8 +18,11 @@ 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.util.stream.Stream; import org.junit.jupiter.api.AfterEach; @@ -310,6 +313,21 @@ void shouldRaiseExceptionOnEmptyMetrics() { .hasMessage("No metrics were emitted"); } + @Test + void shouldLogWarningOnEmptyMetrics() throws Exception { + // Given + File logFile = new File("target/metrics-test.log"); + + // When + // Flushing without adding metrics + metricsLogger.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 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 From cc74f3fe5b5bb565a87e69113d34eb1bd3a5be96 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 4 Jun 2025 19:30:16 +0200 Subject: [PATCH 16/36] Remove unused imports. --- .../amazon/lambda/powertools/metrics/MetricsLogger.java | 3 +-- .../lambda/powertools/metrics/testutils/TestMetricsLogger.java | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java index 2aa2462ba..7dabde456 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java @@ -15,12 +15,11 @@ package software.amazon.lambda.powertools.metrics; import com.amazonaws.services.lambda.runtime.Context; + 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.util.Map; - /** * Interface for metrics logging */ diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java index 0a1b631e8..6e54c3d75 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java @@ -1,7 +1,6 @@ package software.amazon.lambda.powertools.metrics.testutils; import java.util.Collections; -import java.util.Map; import com.amazonaws.services.lambda.runtime.Context; From 37de57d6be506dc942ede89258f7dbbbf0dd297c Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 12:12:57 +0200 Subject: [PATCH 17/36] Add namespace validation. Refacator into own Validator class. Require namespace to be set or raise an exception otherwise. --- .../lambda/powertools/metrics/Metrics.java | 5 +- .../metrics/internal/EmfMetricsLogger.java | 24 ++- .../metrics/internal/Validator.java | 99 ++++++++++ .../metrics/model/DimensionSet.java | 42 +--- .../metrics/ConfigurationPrecedenceTest.java | 6 +- .../metrics/MetricsLoggerBuilderTest.java | 5 + .../metrics/MetricsLoggerFactoryTest.java | 1 + .../internal/EmfMetricsLoggerTest.java | 1 + .../internal/LambdaMetricsAspectTest.java | 6 +- .../metrics/internal/ValidatorTest.java | 180 ++++++++++++++++++ .../metrics/model/DimensionSetTest.java | 125 ------------ 11 files changed, 312 insertions(+), 182 deletions(-) create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/Validator.java create mode 100644 powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/ValidatorTest.java 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 3d75e6710..1d827a7a2 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 @@ -48,9 +48,8 @@ * 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")}. + *

A namespace must be specified for metrics. This can be set with the environment variable {@code POWERTOOLS_METRICS_NAMESPACE} + * or the annotation variable {@code @Metrics(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 @Metrics(functionName = "MyFunction")}. 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 index a72c3c902..104bc0cf7 100644 --- 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 @@ -119,11 +119,13 @@ public software.amazon.lambda.powertools.metrics.model.DimensionSet getDefaultDi @Override public void setNamespace(String namespace) { + Validator.validateNamespace(namespace); + this.namespace = namespace; try { emfLogger.setNamespace(namespace); } catch (Exception e) { - // Ignore namespace errors + LOGGER.error("Namespace cannot be set due to an error in EMF", e); } } @@ -140,6 +142,8 @@ public void clearDefaultDimensions() { @Override public void flush() { + Validator.validateNamespace(namespace); + if (!hasMetrics.get()) { if (raiseOnEmptyMetrics) { throw new IllegalStateException("No metrics were emitted"); @@ -154,15 +158,14 @@ public void flush() { public void captureColdStartMetric(Context context, software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) { if (isColdStart()) { + Validator.validateNamespace(namespace); + software.amazon.cloudwatchlogs.emf.logger.MetricsLogger coldStartLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger(); - // Set namespace if available - if (namespace != null) { - try { - coldStartLogger.setNamespace(namespace); - } catch (Exception e) { - // Ignore namespace errors - } + 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); @@ -200,15 +203,16 @@ public void captureColdStartMetric(software.amazon.lambda.powertools.metrics.mod @Override public void pushSingleMetric(String name, double value, MetricUnit unit, String namespace, software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) { + 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); - // Set namespace (now mandatory) try { singleMetricLogger.setNamespace(namespace); } catch (Exception e) { - // Ignore namespace errors + LOGGER.error("Namespace cannot be set for single metric due to an error in EMF", e); } // Add the metric 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..89639fbde --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/Validator.java @@ -0,0 +1,99 @@ +/* + * 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 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._#/]+$"; + + 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 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 index b4dfd69aa..e93f34237 100644 --- 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 @@ -18,15 +18,13 @@ import java.util.Map; import java.util.Set; -import org.apache.commons.lang3.StringUtils; +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 static final int MAX_DIMENSION_NAME_LENGTH = 250; - private static final int MAX_DIMENSION_VALUE_LENGTH = 1024; private final Map dimensions = new LinkedHashMap<>(); @@ -190,42 +188,6 @@ public Map getDimensions() { } private 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); - } + Validator.validateDimension(key, value); } } 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 index 9f732672a..0cae4e2da 100644 --- 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 @@ -161,6 +161,10 @@ void environmentVariablesShouldBeUsedWhenNoOverrides() throws Exception { @Test void shouldUseDefaultsWhenNoConfiguration() throws Exception { // Given + MetricsLoggerBuilder.builder() + .withNamespace("TestNamespace") + .build(); + RequestHandler, String> handler = new HandlerWithDefaultMetricsAnnotation(); Context context = new TestContext(); Map input = new HashMap<>(); @@ -174,7 +178,7 @@ void shouldUseDefaultsWhenNoConfiguration() throws Exception { // Default values should be used assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) - .isEqualTo("aws-embedded-metrics"); + .isEqualTo("TestNamespace"); assertThat(rootNode.has("Service")).isTrue(); assertThat(rootNode.get("Service").asText()).isEqualTo("service_undefined"); } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java index 2f6827ce3..c13188171 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java @@ -81,6 +81,7 @@ void shouldBuildWithCustomService() throws Exception { // When MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() .withService("CustomService") + .withNamespace("TestNamespace") .build(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -99,6 +100,7 @@ void shouldBuildWithRaiseOnEmptyMetrics() { // When MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() .withRaiseOnEmptyMetrics(true) + .withNamespace("TestNamespace") .build(); // Then @@ -113,6 +115,7 @@ void shouldBuildWithDefaultDimension() throws Exception { // When MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() .withDefaultDimension("Environment", "Test") + .withNamespace("TestNamespace") .build(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -131,6 +134,7 @@ void shouldBuildWithMultipleDefaultDimensions() throws Exception { // When MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() .withDefaultDimensions(DimensionSet.of("Environment", "Test", "Region", "us-west-2")) + .withNamespace("TestNamespace") .build(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -166,6 +170,7 @@ void shouldOverrideServiceWithDefaultDimensions() throws Exception { MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() .withService("OriginalService") .withDefaultDimensions(DimensionSet.of("Service", "OverriddenService")) + .withNamespace("TestNamespace") .build(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java index f312995b3..c758d1041 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java @@ -113,6 +113,7 @@ void shouldUseNamespaceFromEnvironmentVariable() throws Exception { void shouldUseServiceNameFromEnvironmentVariable() throws Exception { // When MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.setNamespace("TestNamespace"); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); metricsLogger.flush(); 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 index 7ef80bc56..1f75b00d7 100644 --- 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 @@ -65,6 +65,7 @@ void setUp() throws Exception { coldStartField.set(null, null); metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.setNamespace("TestNamespace"); System.setOut(new PrintStream(outputStreamCaptor)); } 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 2564f5c15..1ece4edba 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 @@ -252,7 +252,7 @@ public String handleRequest(Map input, Context context) { static class HandlerWithColdStartMetricsAnnotation implements RequestHandler, String> { @Override - @Metrics(captureColdStart = true) + @Metrics(captureColdStart = true, namespace = "TestNamespace") public String handleRequest(Map input, Context context) { MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -262,7 +262,7 @@ public String handleRequest(Map input, Context context) { static class HandlerWithCustomFunctionName implements RequestHandler, String> { @Override - @Metrics(captureColdStart = true, functionName = "CustomFunction") + @Metrics(captureColdStart = true, functionName = "CustomFunction", namespace = "TestNamespace") public String handleRequest(Map input, Context context) { MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -272,7 +272,7 @@ public String handleRequest(Map input, Context context) { static class HandlerWithServiceNameAndColdStart implements RequestHandler, String> { @Override - @Metrics(service = "CustomService", captureColdStart = true) + @Metrics(service = "CustomService", captureColdStart = true, namespace = "TestNamespace") public String handleRequest(Map input, Context context) { MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); 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..9e61abf2c --- /dev/null +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/ValidatorTest.java @@ -0,0 +1,180 @@ +/* + * 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 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(); + } +} 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 index 8a004eb33..ac059f117 100644 --- 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 @@ -163,129 +163,4 @@ void shouldThrowExceptionWhenExceedingMaxDimensions() { .isInstanceOf(IllegalStateException.class) .hasMessageContaining("Cannot exceed 30 dimensions per dimension set"); } - - @Test - void shouldThrowExceptionWhenKeyIsNull() { - // Given - DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); - - // When/Then - assertThatThrownBy(() -> dimensionSet.addDimension(null, "Value")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dimension key cannot be null or empty"); - } - - @Test - void shouldThrowExceptionWhenKeyIsEmpty() { - // Given - DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); - - // When/Then - assertThatThrownBy(() -> dimensionSet.addDimension("", "Value")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dimension key cannot be null or empty"); - } - - @Test - void shouldThrowExceptionWhenValueIsNull() { - // Given - DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); - - // When/Then - assertThatThrownBy(() -> dimensionSet.addDimension("Key", null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dimension value cannot be null or empty"); - } - - @Test - void shouldThrowExceptionWhenValueIsEmpty() { - // Given - DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); - - // When/Then - assertThatThrownBy(() -> dimensionSet.addDimension("Key", "")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dimension value cannot be null or empty"); - } - - @Test - void shouldThrowExceptionWhenKeyContainsWhitespace() { - // Given - DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); - - // When/Then - assertThatThrownBy(() -> dimensionSet.addDimension("Key With Space", "Value")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dimension key cannot contain whitespaces: Key With Space"); - } - - @Test - void shouldThrowExceptionWhenValueContainsWhitespace() { - // Given - DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); - - // When/Then - assertThatThrownBy(() -> dimensionSet.addDimension("Key", "Value With Space")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dimension value cannot contain whitespaces: Value With Space"); - } - - @Test - void shouldThrowExceptionWhenKeyStartsWithColon() { - // Given - DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); - - // When/Then - assertThatThrownBy(() -> dimensionSet.addDimension(":Key", "Value")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dimension key cannot start with colon: :Key"); - } - - @Test - void shouldThrowExceptionWhenKeyExceedsMaxLength() { - // Given - DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); - String longKey = "a".repeat(251); // MAX_DIMENSION_NAME_LENGTH + 1 - - // When/Then - assertThatThrownBy(() -> dimensionSet.addDimension(longKey, "Value")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dimension name exceeds maximum length of 250: " + longKey); - } - - @Test - void shouldThrowExceptionWhenValueExceedsMaxLength() { - // Given - DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); - String longValue = "a".repeat(1025); // MAX_DIMENSION_VALUE_LENGTH + 1 - - // When/Then - assertThatThrownBy(() -> dimensionSet.addDimension("Key", longValue)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dimension value exceeds maximum length of 1024: " + longValue); - } - - @Test - void shouldThrowExceptionWhenKeyContainsNonAsciiCharacters() { - // Given - DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); - String keyWithNonAscii = "Key\u0080"; // Non-ASCII character - - // When/Then - assertThatThrownBy(() -> dimensionSet.addDimension(keyWithNonAscii, "Value")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dimension name has invalid characters: " + keyWithNonAscii); - } - - @Test - void shouldThrowExceptionWhenValueContainsNonAsciiCharacters() { - // Given - DimensionSet dimensionSet = DimensionSet.of(Collections.emptyMap()); - String valueWithNonAscii = "Value\u0080"; // Non-ASCII character - - // When/Then - assertThatThrownBy(() -> dimensionSet.addDimension("Key", valueWithNonAscii)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dimension value has invalid characters: " + valueWithNonAscii); - } } From 36dd713ea9ff1d0fabc4a64b3f1702e424c009b3 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 12:16:23 +0200 Subject: [PATCH 18/36] Update docs with DimensionSet.of instead of Map.of. Fix naming consistency. --- docs/core/metrics.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index b65c225ca..db382165a 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -111,10 +111,10 @@ Visit the AWS documentation for a complete explanation for [Amazon CloudWatch co Metrics has two 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 | Decorator parameter | -| -------------------- | --------------------------------------------------------------------------------------- | ------------------------------ | ------------------- | -| **Metric namespace** | Logical container where all metrics will be placed e.g. `e-commerce-app` | `POWERTOOLS_METRICS_NAMESPACE` | `namespace` | -| **Service** | Optionally, sets **service** metric dimension across all metrics e.g. `product-service` | `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` | !!! tip "Use your application or main service as the metric namespace to easily group all metrics" @@ -140,8 +140,8 @@ Metrics has two global settings that will be used across all metrics emitted. Us Runtime: java8 Environment: Variables: - POWERTOOLS_SERVICE_NAME: product-service - POWERTOOLS_METRICS_NAMESPACE: e-commerce-app + POWERTOOLS_SERVICE_NAME: payment + POWERTOOLS_METRICS_NAMESPACE: ServerlessAirline ``` === "MetricsEnabledHandler.java" @@ -360,7 +360,7 @@ If you wish to set custom default dimensions, it can be done via `#!java metrics @Override @Metrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - metricsLogger.setDefaultDimensions(Map.of("CustomDimension", "booking", "Environment", "prod")); + metricsLogger.setDefaultDimensions(DimensionSet.of("CustomDimension", "booking", "Environment", "prod")); ... } } @@ -377,7 +377,7 @@ If you wish to set custom default dimensions, it can be done via `#!java metrics public class App implements RequestHandler { private static final MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() - .withDefaultDimensions(Map.of("CustomDimension", "booking", "Environment", "prod")) + .withDefaultDimensions(DimensionSet.of("CustomDimension", "booking", "Environment", "prod")) .build(); @Override @@ -459,7 +459,7 @@ The following example shows how to configure a custom `MetricsLogger` using the // You can manually capture the cold start metric // Lambda context is an optional argument if not available in your environment // Dimensions are also optional. - customLogger.captureColdStartMetric(context, DimensionSet.of("FunctionName", "MyFunction", "Service", "product-service")); + customLogger.captureColdStartMetric(context, DimensionSet.of("FunctionName", "MyFunction", "Service", "payment")); // Add metrics to the custom logger customLogger.addMetric("CustomMetric", 1, MetricUnit.COUNT); From e0df19b98ec9237c7482e894dfdafa99e7f49192 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 12:26:22 +0200 Subject: [PATCH 19/36] Add example for overwriting dimension of cold start metric. --- docs/core/metrics.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index db382165a..49bf0f8cc 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -118,16 +118,15 @@ Metrics has two global settings that will be used across all metrics emitted. Us !!! tip "Use your application or main service as the metric namespace to easily group all metrics" - -!!!info "Order of Precedence of `MetricsLogger` configuration" - The `MetricsLogger` Singleton can be configured by three different interfaces. The following order of precedence applies: +### Order of Precedence of `MetricsLogger` configuration - 1. `@Metrics` annotation (recommended) - 2. `MetricsLoggerBuilder` using Builder pattern (see [Advanced section](#usage-without-metrics-annotation)) - 3. Environment variables (recommended) +The `MetricsLogger` Singleton can be configured by three different interfaces. The following order of precedence applies: - For most use-cases, we recommend using Environment variables and only overwrite settings in code where needed using either the `@Metrics` annotation or `MetricsLoggerBuilder` if the annotation cannot be used. - +1. `@Metrics` annotation +2. `MetricsLoggerBuilder` 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 `@Metrics` annotation or `MetricsLoggerBuilder` if the annotation cannot be used. === "template.yaml" @@ -302,6 +301,25 @@ You can also specify a custom function name to be used in the cold start metric: } ``` + +!!!tip "You can overwrite the default `Service` and `FunctionName` dimensions of the cold start metric" + Set `#!java @Metrics(captureColdStart = false)` and use the `captureColdStartMetric` method manually: + + ```java hl_lines="6 8" + public class MetricsColdStartCustomFunction implements RequestHandler { + + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + + @Override + @Metrics(captureColdStart = false) + public Object handleRequest(Object input, Context context) { + metricsLogger.captureColdStartMetric(context, DimensionSet.of("CustomDimension", "CustomValue")); + ... + } + } + ``` + + ## Advanced ### Adding metadata From 0a79cec70f4d2afed72b988e73814d87072ca69e Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 12:27:27 +0200 Subject: [PATCH 20/36] Add note that metadata needs to be added before flushing. --- docs/core/metrics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 49bf0f8cc..2d5e53cd5 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -349,7 +349,7 @@ You can use `addMetadata` for advanced use cases, where you want to add metadata @Metrics(namespace = "ServerlessAirline", service = "booking-service") public Object handleRequest(Object input, Context context) { metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - metricsLogger.addMetadata("booking_id", "1234567890"); + metricsLogger.addMetadata("booking_id", "1234567890"); // Needs to be added BEFORE flushing ... } } From 7ef4e692636388b13fd363ad1301277bfc5f7dc7 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 12:33:01 +0200 Subject: [PATCH 21/36] Remove test classes from GRM reflect-config.json. --- .../powertools-metrics/reflect-config.json | 212 ------------------ 1 file changed, 212 deletions(-) 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 dcf829942..c301750cf 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 @@ -129,236 +129,24 @@ "fields": [{ "name": "IS_COLD_START" }], "methods": [{ "name": "resetServiceName", "parameterTypes": [] }] }, - { - "name": "software.amazon.lambda.powertools.metrics.ConfigurationPrecedenceTest", - "allDeclaredFields": true, - "allDeclaredClasses": true, - "queryAllDeclaredMethods": true, - "queryAllPublicMethods": true, - "queryAllDeclaredConstructors": true, - "methods": [ - { "name": "", "parameterTypes": [] }, - { "name": "annotationShouldOverrideBuilderAndEnvironment", "parameterTypes": [] }, - { "name": "builderShouldOverrideEnvironment", "parameterTypes": [] }, - { "name": "environmentVariablesShouldBeUsedWhenNoOverrides", "parameterTypes": [] }, - { "name": "setUp", "parameterTypes": [] }, - { "name": "shouldUseDefaultsWhenNoConfiguration", "parameterTypes": [] }, - { "name": "tearDown", "parameterTypes": [] } - ] - }, - { - "name": "software.amazon.lambda.powertools.metrics.ConfigurationPrecedenceTest$HandlerWithDefaultMetricsAnnotation", - "methods": [ - { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } - ] - }, - { - "name": "software.amazon.lambda.powertools.metrics.ConfigurationPrecedenceTest$HandlerWithMetricsAnnotation", - "methods": [ - { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } - ] - }, { "name": "software.amazon.lambda.powertools.metrics.MetricsLogger", "allDeclaredClasses": true, "queryAllPublicMethods": true }, - { - "name": "software.amazon.lambda.powertools.metrics.MetricsLoggerBuilderTest", - "allDeclaredFields": true, - "allDeclaredClasses": true, - "queryAllDeclaredMethods": true, - "queryAllPublicMethods": true, - "queryAllDeclaredConstructors": true, - "methods": [ - { "name": "", "parameterTypes": [] }, - { "name": "setUp", "parameterTypes": [] }, - { "name": "shouldBuildWithCustomMetricsProvider", "parameterTypes": [] }, - { "name": "shouldBuildWithCustomNamespace", "parameterTypes": [] }, - { "name": "shouldBuildWithCustomService", "parameterTypes": [] }, - { "name": "shouldBuildWithDefaultDimension", "parameterTypes": [] }, - { "name": "shouldBuildWithMultipleDefaultDimensions", "parameterTypes": [] }, - { "name": "shouldBuildWithRaiseOnEmptyMetrics", "parameterTypes": [] }, - { "name": "shouldOverrideServiceWithDefaultDimensions", "parameterTypes": [] }, - { "name": "tearDown", "parameterTypes": [] } - ] - }, { "name": "software.amazon.lambda.powertools.metrics.MetricsLoggerFactory", "fields": [{ "name": "metricsLogger" }, { "name": "provider" }] }, - { - "name": "software.amazon.lambda.powertools.metrics.MetricsLoggerFactoryTest", - "allDeclaredFields": true, - "allDeclaredClasses": true, - "queryAllDeclaredMethods": true, - "queryAllPublicMethods": true, - "queryAllDeclaredConstructors": true, - "methods": [ - { "name": "", "parameterTypes": [] }, - { "name": "setUp", "parameterTypes": [] }, - { "name": "shouldGetMetricsLoggerInstance", "parameterTypes": [] }, - { "name": "shouldReturnSameInstanceOnMultipleCalls", "parameterTypes": [] }, - { "name": "shouldSetCustomMetricsProvider", "parameterTypes": [] }, - { "name": "shouldThrowExceptionWhenSettingNullProvider", "parameterTypes": [] }, - { "name": "shouldUseNamespaceFromEnvironmentVariable", "parameterTypes": [] }, - { "name": "shouldUseServiceNameFromEnvironmentVariable", "parameterTypes": [] }, - { "name": "tearDown", "parameterTypes": [] } - ] - }, { "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.internal.EmfMetricsLoggerTest", - "allDeclaredFields": true, - "allDeclaredClasses": true, - "queryAllDeclaredMethods": true, - "queryAllPublicMethods": true, - "queryAllDeclaredConstructors": true, - "methods": [ - { "name": "", "parameterTypes": [] }, - { "name": "setUp", "parameterTypes": [] }, - { "name": "shouldAddDimension", "parameterTypes": [] }, - { "name": "shouldAddMetadata", "parameterTypes": [] }, - { "name": "shouldCaptureColdStartMetric", "parameterTypes": [] }, - { "name": "shouldCaptureColdStartMetricWithDimensions", "parameterTypes": [] }, - { "name": "shouldCaptureColdStartMetricWithoutDimensions", "parameterTypes": [] }, - { "name": "shouldClearDefaultDimensions", "parameterTypes": [] }, - { - "name": "shouldConvertMetricUnits", - "parameterTypes": [ - "software.amazon.lambda.powertools.metrics.model.MetricUnit", - "software.amazon.cloudwatchlogs.emf.model.Unit" - ] - }, - { "name": "shouldCreateMetricWithDefaultResolution", "parameterTypes": [] }, - { "name": "shouldCreateMetricWithHighResolution", "parameterTypes": [] }, - { "name": "shouldGetDefaultDimensions", "parameterTypes": [] }, - { "name": "shouldPushSingleMetric", "parameterTypes": [] }, - { "name": "shouldPushSingleMetricWithoutDimensions", "parameterTypes": [] }, - { "name": "shouldRaiseExceptionOnEmptyMetrics", "parameterTypes": [] }, - { "name": "shouldReuseNamespaceForColdStartMetric", "parameterTypes": [] }, - { "name": "shouldSetDefaultDimensions", "parameterTypes": [] }, - { "name": "shouldSetNamespace", "parameterTypes": [] }, - { "name": "tearDown", "parameterTypes": [] }, - { "name": "unitConversionTestCases", "parameterTypes": [] } - ] - }, - { - "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest", - "allDeclaredFields": true, - "allDeclaredClasses": true, - "queryAllDeclaredMethods": true, - "queryAllPublicMethods": true, - "queryAllDeclaredConstructors": true, - "methods": [ - { "name": "", "parameterTypes": [] }, - { "name": "setUp", "parameterTypes": [] }, - { "name": "shouldCaptureColdStartMetricWhenConfigured", "parameterTypes": [] }, - { "name": "shouldCaptureMetricsFromAnnotatedHandler", "parameterTypes": [] }, - { "name": "shouldHaveNoEffectOnNonHandlerMethod", "parameterTypes": [] }, - { "name": "shouldOverrideEnvironmentVariablesWithAnnotation", "parameterTypes": [] }, - { "name": "shouldUseCustomFunctionNameWhenProvidedForColdStartMetric", "parameterTypes": [] }, - { "name": "shouldUseEnvironmentVariablesWhenNoAnnotationOverrides", "parameterTypes": [] }, - { "name": "shouldUseServiceNameWhenProvidedForColdStartMetric", "parameterTypes": [] }, - { "name": "tearDown", "parameterTypes": [] } - ] - }, - { - "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest$HandlerWithAnnotationOnWrongMethod", - "methods": [{ "name": "someOtherMethod", "parameterTypes": [] }] - }, - { - "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest$HandlerWithColdStartMetricsAnnotation", - "methods": [ - { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } - ] - }, - { - "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest$HandlerWithCustomFunctionName", - "methods": [ - { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } - ] - }, - { - "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest$HandlerWithDefaultMetricsAnnotation", - "methods": [ - { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } - ] - }, - { - "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest$HandlerWithMetricsAnnotation", - "methods": [ - { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } - ] - }, - { - "name": "software.amazon.lambda.powertools.metrics.internal.LambdaMetricsAspectTest$HandlerWithServiceNameAndColdStart", - "methods": [ - { "name": "handleRequest", "parameterTypes": ["java.util.Map", "com.amazonaws.services.lambda.runtime.Context"] } - ] - }, - { - "name": "software.amazon.lambda.powertools.metrics.model.DimensionSetTest", - "allDeclaredFields": true, - "allDeclaredClasses": true, - "queryAllDeclaredMethods": true, - "queryAllPublicMethods": true, - "queryAllDeclaredConstructors": true, - "methods": [ - { "name": "", "parameterTypes": [] }, - { "name": "shouldCreateDimensionSetFromMap", "parameterTypes": [] }, - { "name": "shouldCreateDimensionSetWithFiveKeyValues", "parameterTypes": [] }, - { "name": "shouldCreateDimensionSetWithFourKeyValues", "parameterTypes": [] }, - { "name": "shouldCreateDimensionSetWithSingleKeyValue", "parameterTypes": [] }, - { "name": "shouldCreateDimensionSetWithThreeKeyValues", "parameterTypes": [] }, - { "name": "shouldCreateDimensionSetWithTwoKeyValues", "parameterTypes": [] }, - { "name": "shouldCreateEmptyDimensionSet", "parameterTypes": [] }, - { "name": "shouldGetDimensionValue", "parameterTypes": [] }, - { "name": "shouldReturnNullForNonExistentDimension", "parameterTypes": [] }, - { "name": "shouldThrowExceptionWhenExceedingMaxDimensions", "parameterTypes": [] }, - { "name": "shouldThrowExceptionWhenKeyIsEmpty", "parameterTypes": [] }, - { "name": "shouldThrowExceptionWhenKeyIsNull", "parameterTypes": [] }, - { "name": "shouldThrowExceptionWhenValueIsNull", "parameterTypes": [] } - ] - }, - { - "name": "software.amazon.lambda.powertools.metrics.provider.EmfMetricsProviderTest", - "allDeclaredFields": true, - "allDeclaredClasses": true, - "queryAllDeclaredMethods": true, - "queryAllPublicMethods": true, - "queryAllDeclaredConstructors": true, - "methods": [ - { "name": "", "parameterTypes": [] }, - { "name": "shouldCreateEmfMetricsLogger", "parameterTypes": [] } - ] - }, { "name": "software.amazon.lambda.powertools.metrics.provider.MetricsProvider", "allDeclaredClasses": true, "queryAllPublicMethods": true - }, - { - "name": "software.amazon.lambda.powertools.metrics.testutils.TestContext", - "allDeclaredClasses": true, - "queryAllDeclaredMethods": true, - "queryAllPublicMethods": true - }, - { - "name": "software.amazon.lambda.powertools.metrics.testutils.TestMetricsLogger", - "allDeclaredClasses": true, - "queryAllDeclaredMethods": true, - "queryAllPublicMethods": true - }, - { - "name": "software.amazon.lambda.powertools.metrics.testutils.TestMetricsProvider", - "allDeclaredClasses": true, - "queryAllDeclaredMethods": true, - "queryAllPublicMethods": true } ] From b05634d94abb0b2933b4c0ca8cd6189d8503b0c6 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 12:34:37 +0200 Subject: [PATCH 22/36] Rename pushSingleMetric to flushSingleMetric. --- docs/core/metrics.md | 4 ++-- .../cdk/app/src/main/java/helloworld/App.java | 4 ++-- .../gradle/src/main/java/helloworld/App.java | 4 ++-- .../kotlin/src/main/kotlin/helloworld/App.kt | 4 ++-- .../sam-graalvm/src/main/java/helloworld/App.java | 4 ++-- .../sam/src/main/java/helloworld/App.java | 2 +- .../serverless/src/main/java/helloworld/App.java | 4 ++-- .../terraform/src/main/java/helloworld/App.java | 4 ++-- .../lambda/powertools/metrics/MetricsLogger.java | 11 +++++------ .../powertools/metrics/internal/EmfMetricsLogger.java | 2 +- .../metrics/internal/EmfMetricsLoggerTest.java | 8 ++++---- .../metrics/testutils/TestMetricsLogger.java | 2 +- 12 files changed, 26 insertions(+), 27 deletions(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 2d5e53cd5..c883d52e4 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -414,7 +414,7 @@ If you wish to set custom default dimensions, it can be done via `#!java metrics ### Creating a single metric with different configuration -You can create a single metric with its own namespace and dimensions using `pushSingleMetric`: +You can create a single metric with its own namespace and dimensions using `flushSingleMetric`: === "App.java" @@ -430,7 +430,7 @@ You can create a single metric with its own namespace and dimensions using `push @Override @Metrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - metricsLogger.pushSingleMetric( + metricsLogger.flushSingleMetric( "CustomMetric", 1, MetricUnit.COUNT, 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 cd9d467d5..5bd547238 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 @@ -62,7 +62,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv "AnotherService", "CustomService", "AnotherService1", "CustomService1" ); - metricsLogger.pushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); + metricsLogger.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); @@ -105,4 +105,4 @@ private String getPageContents(String address) throws IOException { return br.lines().collect(Collectors.joining(System.lineSeparator())); } } -} \ No newline at end of file +} diff --git a/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java index e25ce5c3b..a4204a867 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 @@ -63,7 +63,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv "AnotherService", "CustomService", "AnotherService1", "CustomService1" ); - metricsLogger.pushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); + metricsLogger.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); @@ -106,4 +106,4 @@ private String getPageContents(String address) throws IOException { return br.lines().collect(Collectors.joining(System.lineSeparator())); } } -} \ No newline at end of file +} diff --git a/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt b/examples/powertools-examples-core-utilities/kotlin/src/main/kotlin/helloworld/App.kt index 8752928bc..8e3e349c6 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 @@ -54,7 +54,7 @@ class App : RequestHandler Date: Thu, 5 Jun 2025 12:45:35 +0200 Subject: [PATCH 23/36] Add example for high cardinality dimensions using DimensionSet. --- docs/core/metrics.md | 52 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index c883d52e4..6e7fcef43 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -201,7 +201,7 @@ You can create metrics using `addMetric`, and manually create dimensions for all ### 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 `MetricResolution.HIGH` to the `addMetric` method: +passing a `#!java MetricResolution.HIGH` to the `addMetric` method. If nothing is passed `#!java MetricResolution.STANDARD` will be used. === "HigResMetricsHandler.java" @@ -228,6 +228,52 @@ passing a `MetricResolution.HIGH` to the `addMetric` method: 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.Metrics; + import software.amazon.lambda.powertools.metrics.MetricsLogger; + import software.amazon.lambda.powertools.metrics.model.MetricResolution; + + public class MetricsEnabledHandler implements RequestHandler { + + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + + @Override + @Metrics(namespace = "ServerlessAirline", service = "payment") + public Object handleRequest(Object input, Context context) { + metricsLogger.addDimension("Dimension", "Value"); + metricsLogger.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); + } + } + ``` + +=== "HighCardinalityDimensionHandler.java" + + ```java hl_lines="4 13-14" + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.MetricsLogger; + import software.amazon.lambda.powertools.metrics.model.MetricResolution; + import software.amazon.lambda.powertools.metrics.model.DimensionSet; + + public class MetricsEnabledHandler implements RequestHandler { + + private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + + @Override + @Metrics(namespace = "ServerlessAirline", service = "payment") + public Object handleRequest(Object input, Context context) { + // You can add up to 30 dimensions in a single DimensionSet + metricsLogger.addDimension(DimensionSet.of("Dimension1", "Value1", "Dimension2", "Value2")); + metricsLogger.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); + } + } + ``` + ### Flushing metrics The `@Metrics` annotation **validates**, **serializes**, and **flushes** all your metrics. During metrics validation, @@ -236,12 +282,12 @@ not met, `IllegalStateException` or `IllegalArgumentException` exceptions will b !!! 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 `@Metrics` annotation: === "MetricsRaiseOnEmpty.java" From 05fe15ece624128595ca3136b95afa244b49ecfd Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 13:02:15 +0200 Subject: [PATCH 24/36] Add support for POWERTOOLS_METRICS_DISABLED environment variable. --- docs/core/metrics.md | 13 +++--- docs/index.md | 2 +- .../metrics/internal/EmfMetricsLogger.java | 21 ++++++++++ .../internal/EmfMetricsLoggerTest.java | 40 +++++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 6e7fcef43..b126e9c3c 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -109,15 +109,18 @@ Visit the AWS documentation for a complete explanation for [Amazon CloudWatch co ## Getting started -Metrics has two 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: +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 | 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` | +| 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` | +| **Disable Powertools Metrics** | Optionally, disables all Powertools metrics | `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 `MetricsLogger` configuration The `MetricsLogger` Singleton can be configured by three different interfaces. The following order of precedence applies: diff --git a/docs/index.md b/docs/index.md index ef30b4197..cc96ff33c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -239,9 +239,9 @@ Use the following [dependency matrix](https://github.com/eclipse-aspectj/aspectj |----------------------------------------|----------------------------------------------------------------------------------------|---------------------------| | **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_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/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 index 3ff4036c2..76337cbc4 100644 --- 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 @@ -45,6 +45,7 @@ public class EmfMetricsLogger implements MetricsLogger { 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; @@ -142,6 +143,11 @@ public void clearDefaultDimensions() { @Override public void flush() { + if (isMetricsDisabled()) { + LOGGER.debug("Metrics are disabled, skipping flush"); + return; + } + Validator.validateNamespace(namespace); if (!hasMetrics.get()) { @@ -158,6 +164,11 @@ public void flush() { 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(); @@ -203,6 +214,11 @@ public void captureColdStartMetric(software.amazon.lambda.powertools.metrics.mod @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 @@ -235,6 +251,11 @@ public void flushSingleMetric(String name, double value, MetricUnit unit, String 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: 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 index 54b78a2d8..5e3bce4ac 100644 --- 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 @@ -31,6 +31,7 @@ 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; @@ -458,4 +459,43 @@ void shouldFlushSingleMetricWithoutDimensions() throws Exception { .isEqualTo("SingleNamespace"); } + @Test + @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_DISABLED", value = "true") + void shouldNotFlushMetricsWhenDisabled() { + // When + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + metricsLogger.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 + metricsLogger.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 + metricsLogger.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace", dimensions); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + assertThat(emfOutput).isEmpty(); + } } From eb167f13c008b26e47bed6500fd9a675b2e50f67 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 13:34:27 +0200 Subject: [PATCH 25/36] Only add service name dimension if the service is actually defined. --- .../metrics/MetricsLoggerFactory.java | 10 ++++-- .../metrics/internal/LambdaMetricsAspect.java | 31 ++++++++++++------- .../metrics/ConfigurationPrecedenceTest.java | 4 +-- .../metrics/MetricsLoggerFactoryTest.java | 17 ++++++++++ .../internal/LambdaMetricsAspectTest.java | 28 +++++++++++++++++ 5 files changed, 73 insertions(+), 17 deletions(-) diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java index 9d772ad14..2c8c9922f 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java @@ -14,9 +14,9 @@ package software.amazon.lambda.powertools.metrics; -import software.amazon.lambda.powertools.metrics.model.DimensionSet; - +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; @@ -45,7 +45,11 @@ public static synchronized MetricsLogger getMetricsLogger() { metricsLogger.setNamespace(envNamespace); } - metricsLogger.setDefaultDimensions(DimensionSet.of("Service", LambdaHandlerProcessor.serviceName())); + // Only set Service dimension if it's not the default undefined value + String serviceName = LambdaHandlerProcessor.serviceName(); + if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) { + metricsLogger.setDefaultDimensions(DimensionSet.of("Service", serviceName)); + } } return metricsLogger; 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 084988c6d..6e100f506 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 @@ -25,6 +25,7 @@ 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.Metrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; @@ -70,12 +71,10 @@ public Object around(ProceedingJoinPoint pjp, logger.setNamespace(metrics.namespace()); } - // If the default dimensions are larger than 1 or do not contain the "Service" dimension, it means that the - // user overwrote them manually e.g. using MetricsLoggerBuilder. In this case, we don't set the service - // default dimension. - if (!"".equals(metrics.service()) - && logger.getDefaultDimensions().getDimensionKeys().size() <= 1 - && logger.getDefaultDimensions().getDimensionKeys().contains(SERVICE_DIMENSION)) { + // 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()) && (logger.getDefaultDimensions().getDimensionKeys().size() <= 1 + || logger.getDefaultDimensions().getDimensionKeys().contains(SERVICE_DIMENSION))) { logger.setDefaultDimensions(DimensionSet.of(SERVICE_DIMENSION, metrics.service())); } @@ -95,12 +94,20 @@ public Object around(ProceedingJoinPoint pjp, // Get function name from annotation or context String funcName = functionName(metrics, extractedContext); - // Create dimensions with service and function name - DimensionSet coldStartDimensions = DimensionSet.of( - SERVICE_DIMENSION, - logger.getDefaultDimensions().getDimensions().getOrDefault(SERVICE_DIMENSION, - serviceNameWithFallback(metrics)), - "FunctionName", funcName != null ? funcName : extractedContext.getFunctionName()); + DimensionSet coldStartDimensions = new DimensionSet(); + + // Get service name from logger default dimensions or fallback + String serviceName = logger.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()); logger.captureColdStartMetric(extractedContext, coldStartDimensions); } 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 index 0cae4e2da..68562d974 100644 --- 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 @@ -179,8 +179,8 @@ void shouldUseDefaultsWhenNoConfiguration() throws Exception { // Default values should be used assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) .isEqualTo("TestNamespace"); - assertThat(rootNode.has("Service")).isTrue(); - assertThat(rootNode.get("Service").asText()).isEqualTo("service_undefined"); + // Service dimension should not be present when service is undefined + assertThat(rootNode.has("Service")).isFalse(); } private static class HandlerWithMetricsAnnotation implements RequestHandler, String> { diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java index c758d1041..6c150ab3e 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java @@ -146,4 +146,21 @@ void shouldThrowExceptionWhenSettingNullProvider() { .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 + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.setNamespace("TestNamespace"); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + metricsLogger.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/internal/LambdaMetricsAspectTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java index 1ece4edba..0f27d9447 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 @@ -161,6 +161,34 @@ void shouldCaptureColdStartMetricWhenConfigured() throws Exception { assertThat(metricsNode.has("test-metric")).isTrue(); } + @Test + 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(); + } + @Test void shouldUseCustomFunctionNameWhenProvidedForColdStartMetric() throws Exception { // Given From 4637fef604678cf60c5ee89777cce211b1d6dd4c Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 14:13:06 +0200 Subject: [PATCH 26/36] Add support for POWERTOOLS_METRICS_FUNCTION_NAME. --- docs/core/metrics.md | 11 ++++++----- docs/index.md | 5 +++-- .../metrics/internal/LambdaMetricsAspect.java | 7 +++++++ .../metrics/internal/LambdaMetricsAspectTest.java | 2 ++ 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index b126e9c3c..60d7d8045 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -111,11 +111,12 @@ Visit the AWS documentation for a complete explanation for [Amazon CloudWatch co 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 | 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` | -| **Disable Powertools Metrics** | Optionally, disables all Powertools metrics | `POWERTOOLS_METRICS_DISABLED` | N/A | +| 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 Powertools Metrics** | Optionally, disables all Powertools metrics | `POWERTOOLS_METRICS_DISABLED` | N/A | !!! tip "Use your application or main service as the metric namespace to easily group all metrics" diff --git a/docs/index.md b/docs/index.md index cc96ff33c..43d1ea03b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -236,10 +236,11 @@ 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_DISABLED** | Disables all flushing of 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) | 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 6e100f506..f067433a7 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 @@ -37,11 +37,18 @@ 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 SERVICE_DIMENSION = "Service"; + private static final String FUNCTION_NAME_ENV_VAR = "POWERTOOLS_METRICS_FUNCTION_NAME"; private String functionName(Metrics metrics, Context context) { if (!"".equals(metrics.functionName())) { return metrics.functionName(); } + + String envFunctionName = System.getenv(FUNCTION_NAME_ENV_VAR); + if (envFunctionName != null && !envFunctionName.isEmpty()) { + return envFunctionName; + } + return context != null ? context.getFunctionName() : null; } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/internal/LambdaMetricsAspectTest.java index 0f27d9447..0376c9ddf 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 @@ -162,6 +162,7 @@ void shouldCaptureColdStartMetricWhenConfigured() throws Exception { } @Test + @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(); @@ -187,6 +188,7 @@ void shouldNotIncludeServiceDimensionInColdStartMetricWhenServiceUndefined() thr // FunctionName dimension should be present assertThat(coldStartNode.has("FunctionName")).isTrue(); + assertThat(coldStartNode.get("FunctionName").asText()).isEqualTo("EnvFunctionName"); } @Test From c7c9ed553dd8e1646ea661ed879afef38c059a6f Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 14:26:54 +0200 Subject: [PATCH 27/36] Add Testing your code section to docs. --- docs/core/metrics.md | 94 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 60d7d8045..6d757b733 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -535,3 +535,97 @@ The following example shows how to configure a custom `MetricsLogger` using the } } ``` + +## 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 `MetricsLogger`. 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 + @Metrics(namespace = "CustomNamespace", service = "CustomService") + public String handleRequest(Map input, Context context) { + MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + return "OK"; + } + } +} +``` From e4a3464fdbdbbaf5573809617c38ebf65442a9e1 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 14:36:56 +0200 Subject: [PATCH 28/36] Rename @Metrics to @FlushMetrics. --- docs/core/metrics.md | 70 +++++++++---------- .../cdk/app/src/main/java/helloworld/App.java | 4 +- .../src/main/java/helloworld/AppStream.java | 4 +- .../gradle/src/main/java/helloworld/App.java | 4 +- .../src/main/java/helloworld/AppStream.java | 4 +- .../kotlin/src/main/kotlin/helloworld/App.kt | 2 +- .../src/main/kotlin/helloworld/AppStream.kt | 2 +- .../src/main/java/helloworld/App.java | 4 +- .../sam/src/main/java/helloworld/App.java | 4 +- .../src/main/java/helloworld/AppStream.java | 4 +- .../src/main/java/helloworld/App.java | 4 +- .../src/main/java/helloworld/AppStream.java | 4 +- .../src/main/java/helloworld/App.java | 4 +- .../src/main/java/helloworld/AppStream.java | 4 +- .../lambda/powertools/e2e/Function.java | 6 +- .../{Metrics.java => FlushMetrics.java} | 24 +++---- .../metrics/internal/LambdaMetricsAspect.java | 15 ++-- .../metrics/ConfigurationPrecedenceTest.java | 4 +- .../internal/LambdaMetricsAspectTest.java | 14 ++-- 19 files changed, 91 insertions(+), 90 deletions(-) rename powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/{Metrics.java => FlushMetrics.java} (71%) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 6d757b733..d3386cb5e 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -126,11 +126,11 @@ Metrics has three global settings that will be used across all metrics emitted. The `MetricsLogger` Singleton can be configured by three different interfaces. The following order of precedence applies: -1. `@Metrics` annotation +1. `@FlushMetrics` annotation 2. `MetricsLoggerBuilder` 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 `@Metrics` annotation or `MetricsLoggerBuilder` if the annotation cannot be used. +For most use-cases, we recommend using Environment variables and only overwrite settings in code where needed using either the `@FlushMetrics` annotation or `MetricsLoggerBuilder` if the annotation cannot be used. === "template.yaml" @@ -150,7 +150,7 @@ For most use-cases, we recommend using Environment variables and only overwrite === "MetricsEnabledHandler.java" ```java hl_lines="9" - import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; public class MetricsEnabledHandler implements RequestHandler { @@ -158,16 +158,16 @@ For most use-cases, we recommend using Environment variables and only overwrite private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "ServerlessAirline", service = "payment") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { // ... } } ``` -`MetricsLogger` 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 `@Metrics` annotation must be added on the lambda handler. +`MetricsLogger` 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 `@Metrics` annotation and flush manually. Read more in the [advanced section below](#usage-without-metrics-annotation)." +!!!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 @@ -176,7 +176,7 @@ You can create metrics using `addMetric`, and manually create dimensions for all === "MetricsEnabledHandler.java" ```java hl_lines="13" - import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; import software.amazon.lambda.powertools.metrics.model.MetricUnit; @@ -186,7 +186,7 @@ You can create metrics using `addMetric`, and manually create dimensions for all private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "ServerlessAirline", service = "payment") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { metricsLogger.addDimension("environment", "prod"); metricsLogger.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); @@ -210,7 +210,7 @@ passing a `#!java MetricResolution.HIGH` to the `addMetric` method. If nothing i === "HigResMetricsHandler.java" ```java hl_lines="3 13" - import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.model.MetricResolution; @@ -219,7 +219,7 @@ passing a `#!java MetricResolution.HIGH` to the `addMetric` method. If nothing i private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "ServerlessAirline", service = "payment") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { // ... metricsLogger.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT, MetricResolution.HIGH); @@ -239,7 +239,7 @@ You can add dimensions to your metrics using the `addDimension` method. You can === "KeyValueDimensionHandler.java" ```java hl_lines="3 13" - import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.model.MetricResolution; @@ -248,7 +248,7 @@ You can add dimensions to your metrics using the `addDimension` method. You can private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "ServerlessAirline", service = "payment") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { metricsLogger.addDimension("Dimension", "Value"); metricsLogger.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); @@ -259,7 +259,7 @@ You can add dimensions to your metrics using the `addDimension` method. You can === "HighCardinalityDimensionHandler.java" ```java hl_lines="4 13-14" - import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.model.MetricResolution; import software.amazon.lambda.powertools.metrics.model.DimensionSet; @@ -269,7 +269,7 @@ You can add dimensions to your metrics using the `addDimension` method. You can private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "ServerlessAirline", service = "payment") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { // You can add up to 30 dimensions in a single DimensionSet metricsLogger.addDimension(DimensionSet.of("Dimension1", "Value1", "Dimension2", "Value2")); @@ -280,7 +280,7 @@ You can add dimensions to your metrics using the `addDimension` method. You can ### Flushing metrics -The `@Metrics` annotation **validates**, **serializes**, and **flushes** all your metrics. During metrics validation, +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. @@ -291,17 +291,17 @@ not met, `IllegalStateException` or `IllegalArgumentException` exceptions will b - 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) { ... } @@ -310,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) { ... } @@ -339,12 +339,12 @@ 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.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; public class MetricsColdStartCustomFunction implements RequestHandler { @Override - @Metrics(captureColdStart = true, functionName = "CustomFunction") + @FlushMetrics(captureColdStart = true, functionName = "CustomFunction") public Object handleRequest(Object input, Context context) { ... } @@ -353,7 +353,7 @@ You can also specify a custom function name to be used in the cold start metric: !!!tip "You can overwrite the default `Service` and `FunctionName` dimensions of the cold start metric" - Set `#!java @Metrics(captureColdStart = false)` and use the `captureColdStartMetric` method manually: + Set `#!java @FlushMetrics(captureColdStart = false)` and use the `captureColdStartMetric` method manually: ```java hl_lines="6 8" public class MetricsColdStartCustomFunction implements RequestHandler { @@ -361,7 +361,7 @@ You can also specify a custom function name to be used in the cold start metric: private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(captureColdStart = false) + @FlushMetrics(captureColdStart = false) public Object handleRequest(Object input, Context context) { metricsLogger.captureColdStartMetric(context, DimensionSet.of("CustomDimension", "CustomValue")); ... @@ -387,7 +387,7 @@ You can use `addMetadata` for advanced use cases, where you want to add metadata === "App.java" ```java hl_lines="13" - import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; @@ -396,7 +396,7 @@ You can use `addMetadata` for advanced use cases, where you want to add metadata private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "ServerlessAirline", service = "booking-service") + @FlushMetrics(namespace = "ServerlessAirline", service = "booking-service") public Object handleRequest(Object input, Context context) { metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); metricsLogger.addMetadata("booking_id", "1234567890"); // Needs to be added BEFORE flushing @@ -416,7 +416,7 @@ If you wish to set custom default dimensions, it can be done via `#!java metrics === "App.java" ```java hl_lines="13" - import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; import software.amazon.lambda.powertools.metrics.model.DimensionSet; @@ -426,7 +426,7 @@ If you wish to set custom default dimensions, it can be done via `#!java metrics private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "ServerlessAirline", service = "payment") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { metricsLogger.setDefaultDimensions(DimensionSet.of("CustomDimension", "booking", "Environment", "prod")); ... @@ -437,7 +437,7 @@ If you wish to set custom default dimensions, it can be done via `#!java metrics === "MetricsLoggerBuilder.java" ```java hl_lines="8-10" - import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; import software.amazon.lambda.powertools.metrics.model.DimensionSet; @@ -449,7 +449,7 @@ If you wish to set custom default dimensions, it can be done via `#!java metrics .build(); @Override - @Metrics(namespace = "ServerlessAirline", service = "payment") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); ... @@ -478,7 +478,7 @@ You can create a single metric with its own namespace and dimensions using `flus private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); @Override - @Metrics(namespace = "ServerlessAirline", service = "payment") + @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { metricsLogger.flushSingleMetric( "CustomMetric", @@ -498,9 +498,9 @@ You can create a single metric with its own namespace and dimensions using `flus **unique metric = (metric_name + dimension_name + dimension_value)** -### Usage without `@Metrics` annotation +### Usage without `@FlushMetrics` annotation -The `MetricsLogger` provides all configuration options via `MetricsLoggerBuilder` in addition to the `@Metrics` annotation. This can be useful if work in an environment or framework that does not leverage the vanilla Lambda `handleRequest` method. +The `MetricsLogger` provides all configuration options via `MetricsLoggerBuilder` 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 `MetricsLoggerBuilder` if needed." @@ -620,7 +620,7 @@ class MetricsTestExample { static class HandlerWithMetricsAnnotation implements RequestHandler, String> { @Override - @Metrics(namespace = "CustomNamespace", service = "CustomService") + @FlushMetrics(namespace = "CustomNamespace", service = "CustomService") public String handleRequest(Map input, Context context) { MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); 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 5bd547238..45db8b5e9 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 @@ -31,7 +31,7 @@ import org.slf4j.LoggerFactory; import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; import software.amazon.lambda.powertools.metrics.model.DimensionSet; @@ -49,7 +49,7 @@ public class App implements RequestHandler headers = new HashMap<>(); 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 a4204a867..297088479 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 @@ -32,7 +32,7 @@ import org.slf4j.LoggerFactory; import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; import software.amazon.lambda.powertools.metrics.model.DimensionSet; @@ -50,7 +50,7 @@ public class App implements RequestHandler headers = new HashMap<>(); 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 8e3e349c6..5052420cb 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 @@ -44,7 +44,7 @@ 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 866b7e620..2fae0e1e8 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 @@ -32,7 +32,7 @@ import org.slf4j.LoggerFactory; import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; import software.amazon.lambda.powertools.metrics.model.DimensionSet; @@ -51,7 +51,7 @@ public class App implements RequestHandler headers = new HashMap<>(); 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 53442f058..16449ff3b 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 @@ -35,7 +35,7 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; import software.amazon.lambda.powertools.metrics.model.DimensionSet; @@ -54,7 +54,7 @@ public class App implements RequestHandler headers = new HashMap<>(); 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 e5db5d326..f11cafc98 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 @@ -32,7 +32,7 @@ import org.apache.logging.log4j.Logger; import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; import software.amazon.lambda.powertools.metrics.model.DimensionSet; @@ -50,7 +50,7 @@ public class App implements RequestHandler headers = new HashMap<>(); 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 e5db5d326..f11cafc98 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 @@ -32,7 +32,7 @@ import org.apache.logging.log4j.Logger; import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; import software.amazon.lambda.powertools.metrics.model.DimensionSet; @@ -50,7 +50,7 @@ public class App implements RequestHandler headers = new HashMap<>(); 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/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..03eb8979e 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 @@ -20,7 +20,7 @@ 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.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsUtils; import java.time.LocalDateTime; import java.time.ZoneOffset; @@ -32,7 +32,7 @@ public class Function implements RequestHandler { MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); - @Metrics(captureColdStart = true) + @FlushMetrics(captureColdStart = true) public String handleRequest(Input input, Context context) { Instant currentTimeTruncatedPlusThirty = @@ -49,4 +49,4 @@ public String handleRequest(Input input, Context context) { return "OK"; } -} \ No newline at end of file +} 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/FlushMetrics.java similarity index 71% rename from powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java rename to powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/FlushMetrics.java index 1d827a7a2..952625f5b 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/FlushMetrics.java @@ -20,45 +20,45 @@ import java.lang.annotation.Target; /** - * {@code Metrics} is used to signal that the annotated Lambda handler method should be - * extended with Metrics functionality. Will have no effect when used on a method that is not a Lambda handler. + * {@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 Metrics} allows users to asynchronously create Amazon + *

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

{@code Metrics} should be used with the handleRequest method of a class + *

{@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 Metrics} creates Amazon CloudWatch custom metrics. You can find + *

{@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 @Metrics(captureColdStart = true)}. + *

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 @Metrics(raiseOnEmptyMetrics = true)}. + *

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 @Metrics(service = "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 @Metrics(namespace = "Namespace")}. If not specified, an IllegalStateException will be thrown. + * 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 @Metrics(functionName = "MyFunction")}. + *

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 Metrics { +public @interface FlushMetrics { String namespace() default ""; String service() default ""; 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 f067433a7..f765f9f83 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 @@ -27,7 +27,7 @@ import software.amazon.lambda.powertools.common.internal.LambdaConstants; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; import software.amazon.lambda.powertools.metrics.model.DimensionSet; @@ -39,7 +39,7 @@ public class LambdaMetricsAspect { private static final String SERVICE_DIMENSION = "Service"; private static final String FUNCTION_NAME_ENV_VAR = "POWERTOOLS_METRICS_FUNCTION_NAME"; - private String functionName(Metrics metrics, Context context) { + private String functionName(FlushMetrics metrics, Context context) { if (!"".equals(metrics.functionName())) { return metrics.functionName(); } @@ -52,7 +52,7 @@ private String functionName(Metrics metrics, Context context) { return context != null ? context.getFunctionName() : null; } - private String serviceNameWithFallback(Metrics metrics) { + private String serviceNameWithFallback(FlushMetrics metrics) { if (!"".equals(metrics.service())) { return metrics.service(); } @@ -61,19 +61,20 @@ private String serviceNameWithFallback(Metrics metrics) { @SuppressWarnings({ "EmptyMethod" }) @Pointcut("@annotation(metrics)") - public void callAt(Metrics metrics) { + public void callAt(FlushMetrics metrics) { } - @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)) { MetricsLogger logger = MetricsLoggerFactory.getMetricsLogger(); // The MetricsLoggerFactory applies default settings from the environment or can be configured by the - // MetricsLoggerBuilder. We only overwrite settings if they are explicitly set in the @Metrics annotation. + // MetricsLoggerBuilder. We only overwrite settings if they are explicitly set in the @FlushMetrics + // annotation. if (!"".equals(metrics.namespace())) { logger.setNamespace(metrics.namespace()); } 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 index 68562d974..acd94260d 100644 --- 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 @@ -185,7 +185,7 @@ void shouldUseDefaultsWhenNoConfiguration() throws Exception { private static class HandlerWithMetricsAnnotation implements RequestHandler, String> { @Override - @Metrics(namespace = "AnnotationNamespace", service = "AnnotationService") + @FlushMetrics(namespace = "AnnotationNamespace", service = "AnnotationService") public String handleRequest(Map input, Context context) { MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -195,7 +195,7 @@ public String handleRequest(Map input, Context context) { private static class HandlerWithDefaultMetricsAnnotation implements RequestHandler, String> { @Override - @Metrics + @FlushMetrics public String handleRequest(Map input, Context context) { MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); 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 0376c9ddf..2a6f752a5 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 @@ -33,7 +33,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; -import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.FlushMetrics; import software.amazon.lambda.powertools.metrics.MetricsLogger; import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; import software.amazon.lambda.powertools.metrics.model.MetricUnit; @@ -262,7 +262,7 @@ void shouldHaveNoEffectOnNonHandlerMethod() { static class HandlerWithMetricsAnnotation implements RequestHandler, String> { @Override - @Metrics(namespace = "CustomNamespace", service = "CustomService") + @FlushMetrics(namespace = "CustomNamespace", service = "CustomService") public String handleRequest(Map input, Context context) { MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -272,7 +272,7 @@ public String handleRequest(Map input, Context context) { static class HandlerWithDefaultMetricsAnnotation implements RequestHandler, String> { @Override - @Metrics + @FlushMetrics public String handleRequest(Map input, Context context) { MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -282,7 +282,7 @@ public String handleRequest(Map input, Context context) { static class HandlerWithColdStartMetricsAnnotation implements RequestHandler, String> { @Override - @Metrics(captureColdStart = true, namespace = "TestNamespace") + @FlushMetrics(captureColdStart = true, namespace = "TestNamespace") public String handleRequest(Map input, Context context) { MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -292,7 +292,7 @@ public String handleRequest(Map input, Context context) { static class HandlerWithCustomFunctionName implements RequestHandler, String> { @Override - @Metrics(captureColdStart = true, functionName = "CustomFunction", namespace = "TestNamespace") + @FlushMetrics(captureColdStart = true, functionName = "CustomFunction", namespace = "TestNamespace") public String handleRequest(Map input, Context context) { MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -302,7 +302,7 @@ public String handleRequest(Map input, Context context) { static class HandlerWithServiceNameAndColdStart implements RequestHandler, String> { @Override - @Metrics(service = "CustomService", captureColdStart = true, namespace = "TestNamespace") + @FlushMetrics(service = "CustomService", captureColdStart = true, namespace = "TestNamespace") public String handleRequest(Map input, Context context) { MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); @@ -319,7 +319,7 @@ public String handleRequest(Map input, Context context) { return "OK"; } - @Metrics + @FlushMetrics public void someOtherMethod() { MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); From a63561032bc3f37213e7e3e8b4b5ad31298d4d55 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 15:14:34 +0200 Subject: [PATCH 29/36] Rename MetricsLogger to Metrics, rename MetricsLoggerBuilder to MetricsBuilder, rename MetricsLoggerFactory to MetricsFactory. --- docs/core/metrics.md | 118 +++++++++--------- .../cdk/app/src/main/java/helloworld/App.java | 18 ++- .../gradle/src/main/java/helloworld/App.java | 10 +- .../kotlin/src/main/kotlin/helloworld/App.kt | 10 +- .../src/main/java/helloworld/App.java | 20 ++- .../sam/src/main/java/helloworld/App.java | 12 +- .../src/main/java/helloworld/App.java | 18 ++- .../src/main/java/helloworld/App.java | 18 ++- .../{MetricsLogger.java => Metrics.java} | 24 ++-- ...LoggerBuilder.java => MetricsBuilder.java} | 42 +++---- ...LoggerFactory.java => MetricsFactory.java} | 28 ++--- .../metrics/internal/EmfMetricsLogger.java | 6 +- .../metrics/internal/LambdaMetricsAspect.java | 32 ++--- .../metrics/provider/EmfMetricsProvider.java | 6 +- .../metrics/provider/MetricsProvider.java | 8 +- .../metrics/ConfigurationPrecedenceTest.java | 22 ++-- ...ilderTest.java => MetricsBuilderTest.java} | 48 +++---- ...ctoryTest.java => MetricsFactoryTest.java} | 48 +++---- .../internal/EmfMetricsLoggerTest.java | 100 +++++++-------- .../internal/LambdaMetricsAspectTest.java | 34 ++--- .../provider/EmfMetricsProviderTest.java | 6 +- ...estMetricsLogger.java => TestMetrics.java} | 4 +- .../testutils/TestMetricsProvider.java | 6 +- spotbugs-exclude.xml | 4 +- 24 files changed, 318 insertions(+), 324 deletions(-) rename powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/{MetricsLogger.java => Metrics.java} (90%) rename powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/{MetricsLoggerBuilder.java => MetricsBuilder.java} (70%) rename powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/{MetricsLoggerFactory.java => MetricsFactory.java} (72%) rename powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/{MetricsLoggerBuilderTest.java => MetricsBuilderTest.java} (79%) rename powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/{MetricsLoggerFactoryTest.java => MetricsFactoryTest.java} (76%) rename powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/{TestMetricsLogger.java => TestMetrics.java} (94%) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index d3386cb5e..fbf558a4b 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -122,15 +122,15 @@ Metrics has three global settings that will be used across all metrics emitted. !!! info "`POWERTOOLS_METRICS_DISABLED` will not disable default metrics created by AWS services." -### Order of Precedence of `MetricsLogger` configuration +### Order of Precedence of `Metrics` configuration -The `MetricsLogger` Singleton can be configured by three different interfaces. The following order of precedence applies: +The `Metrics` Singleton can be configured by three different interfaces. The following order of precedence applies: 1. `@FlushMetrics` annotation -2. `MetricsLoggerBuilder` using Builder pattern (see [Advanced section](#usage-without-metrics-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 `MetricsLoggerBuilder` if the annotation cannot be used. +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" @@ -151,11 +151,11 @@ For most use-cases, we recommend using Environment variables and only overwrite ```java hl_lines="9" import software.amazon.lambda.powertools.metrics.FlushMetrics; - import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; + import software.amazon.lambda.powertools.metrics.MetricsFactory; public class MetricsEnabledHandler implements RequestHandler { - private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Override @FlushMetrics(namespace = "ServerlessAirline", service = "payment") @@ -165,31 +165,31 @@ For most use-cases, we recommend using Environment variables and only overwrite } ``` -`MetricsLogger` 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. +`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 `addMetric`, and manually create dimensions for all your aggregate metrics using `addDimension`. Anywhere in your code, you can access the current `MetricsLogger` Singleton using the `MetricsLoggerFactory`. +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="13" import software.amazon.lambda.powertools.metrics.FlushMetrics; - import software.amazon.lambda.powertools.metrics.MetricsLogger; - import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.MetricsFactory; import software.amazon.lambda.powertools.metrics.model.MetricUnit; public class MetricsEnabledHandler implements RequestHandler { - private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Override @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - metricsLogger.addDimension("environment", "prod"); - metricsLogger.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); + metrics.addDimension("environment", "prod"); + metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); // ... } } @@ -211,18 +211,18 @@ passing a `#!java MetricResolution.HIGH` to the `addMetric` method. If nothing i ```java hl_lines="3 13" import software.amazon.lambda.powertools.metrics.FlushMetrics; - import software.amazon.lambda.powertools.metrics.MetricsLogger; + import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.metrics.model.MetricResolution; public class MetricsEnabledHandler implements RequestHandler { - private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Override @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { // ... - metricsLogger.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT, MetricResolution.HIGH); + metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT, MetricResolution.HIGH); } } ``` @@ -240,18 +240,18 @@ You can add dimensions to your metrics using the `addDimension` method. You can ```java hl_lines="3 13" import software.amazon.lambda.powertools.metrics.FlushMetrics; - import software.amazon.lambda.powertools.metrics.MetricsLogger; + import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.metrics.model.MetricResolution; public class MetricsEnabledHandler implements RequestHandler { - private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Override @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - metricsLogger.addDimension("Dimension", "Value"); - metricsLogger.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); + metrics.addDimension("Dimension", "Value"); + metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); } } ``` @@ -260,20 +260,20 @@ You can add dimensions to your metrics using the `addDimension` method. You can ```java hl_lines="4 13-14" import software.amazon.lambda.powertools.metrics.FlushMetrics; - import software.amazon.lambda.powertools.metrics.MetricsLogger; + 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 MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + 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 - metricsLogger.addDimension(DimensionSet.of("Dimension1", "Value1", "Dimension2", "Value2")); - metricsLogger.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); + metrics.addDimension(DimensionSet.of("Dimension1", "Value1", "Dimension2", "Value2")); + metrics.addMetric("SuccessfulBooking", 1, MetricUnit.COUNT); } } ``` @@ -358,12 +358,12 @@ You can also specify a custom function name to be used in the cold start metric: ```java hl_lines="6 8" public class MetricsColdStartCustomFunction implements RequestHandler { - private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Override @FlushMetrics(captureColdStart = false) public Object handleRequest(Object input, Context context) { - metricsLogger.captureColdStartMetric(context, DimensionSet.of("CustomDimension", "CustomValue")); + metrics.captureColdStartMetric(context, DimensionSet.of("CustomDimension", "CustomValue")); ... } } @@ -388,18 +388,18 @@ You can use `addMetadata` for advanced use cases, where you want to add metadata ```java hl_lines="13" import software.amazon.lambda.powertools.metrics.FlushMetrics; - import software.amazon.lambda.powertools.metrics.MetricsLogger; - import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; + import software.amazon.lambda.powertools.metrics.Metrics; + import software.amazon.lambda.powertools.metrics.MetricsFactory; public class App implements RequestHandler { - private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Override @FlushMetrics(namespace = "ServerlessAirline", service = "booking-service") public Object handleRequest(Object input, Context context) { - metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); - metricsLogger.addMetadata("booking_id", "1234567890"); // Needs to be added BEFORE flushing + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + metrics.addMetadata("booking_id", "1234567890"); // Needs to be added BEFORE flushing ... } } @@ -411,47 +411,47 @@ This will be available in CloudWatch Logs to ease operations on high cardinal da 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. -If you wish to set custom default dimensions, it can be done via `#!java metricsLogger.setDefaultDimensions()`. You can also use the `MetricsLoggerBuilder` instead of the `MetricsLoggerFactory` to configure **and** retrieve the `MetricsLogger` Singleton at the same time (see `MetricsLoggerBuilder.java` tab). +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="13" import software.amazon.lambda.powertools.metrics.FlushMetrics; - import software.amazon.lambda.powertools.metrics.MetricsLogger; - import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; + 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 MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Override @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - metricsLogger.setDefaultDimensions(DimensionSet.of("CustomDimension", "booking", "Environment", "prod")); + metrics.setDefaultDimensions(DimensionSet.of("CustomDimension", "booking", "Environment", "prod")); ... } } ``` -=== "MetricsLoggerBuilder.java" +=== "MetricsBuilder.java" ```java hl_lines="8-10" import software.amazon.lambda.powertools.metrics.FlushMetrics; - import software.amazon.lambda.powertools.metrics.MetricsLogger; - import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; + 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 MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + private static final Metrics metrics = MetricsBuilder.builder() .withDefaultDimensions(DimensionSet.of("CustomDimension", "booking", "Environment", "prod")) .build(); @Override @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); ... } } @@ -469,18 +469,18 @@ You can create a single metric with its own namespace and dimensions using `flus === "App.java" ```java hl_lines="12-18" - import software.amazon.lambda.powertools.metrics.MetricsLogger; - import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; + 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 MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Override @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - metricsLogger.flushSingleMetric( + metrics.flushSingleMetric( "CustomMetric", 1, MetricUnit.COUNT, @@ -500,23 +500,23 @@ You can create a single metric with its own namespace and dimensions using `flus ### Usage without `@FlushMetrics` annotation -The `MetricsLogger` provides all configuration options via `MetricsLoggerBuilder` 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. +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 `MetricsLoggerBuilder` if needed." +!!!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 `MetricsLogger` using the Builder pattern. Note that it is necessary to manually flush metrics now. +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-12 19 23" - import software.amazon.lambda.powertools.metrics.MetricsLogger; - import software.amazon.lambda.powertools.metrics.MetricsLoggerBuilder; + 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 MetricsLogger singleton without annotation - private static final MetricsLogger customLogger = MetricsLoggerBuilder.builder() + // Create and configure a Metrics singleton without annotation + private static final Metrics customMetrics = MetricsBuilder.builder() .withNamespace("ServerlessAirline") .withRaiseOnEmptyMetrics(true) .withService("payment") @@ -527,11 +527,11 @@ The following example shows how to configure a custom `MetricsLogger` using the // You can manually capture the cold start metric // Lambda context is an optional argument if not available in your environment // Dimensions are also optional. - customLogger.captureColdStartMetric(context, DimensionSet.of("FunctionName", "MyFunction", "Service", "payment")); + customMetrics.captureColdStartMetric(context, DimensionSet.of("FunctionName", "MyFunction", "Service", "payment")); - // Add metrics to the custom logger - customLogger.addMetric("CustomMetric", 1, MetricUnit.COUNT); - customLogger.flush(); + // Add metrics to the custom metrics singleton + customMetrics.addMetric("CustomMetric", 1, MetricUnit.COUNT); + customMetrics.flush(); } } ``` @@ -556,7 +556,7 @@ If you would like to suppress metrics output during your unit tests, you can use ### Asserting EMF output -When unit testing your code, you can run assertions against the output generated by the `MetricsLogger`. 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. +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. @@ -622,8 +622,8 @@ class MetricsTestExample { @Override @FlushMetrics(namespace = "CustomNamespace", service = "CustomService") public String handleRequest(Map input, Context context) { - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); return "OK"; } } 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 45db8b5e9..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 @@ -32,8 +32,8 @@ import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.metrics.FlushMetrics; -import software.amazon.lambda.powertools.metrics.MetricsLogger; -import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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; @@ -45,7 +45,7 @@ */ public class App implements RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); - private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @@ -56,13 +56,12 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); DimensionSet dimensionSet = DimensionSet.of( - "AnotherService", "CustomService", - "AnotherService1", "CustomService1" - ); - metricsLogger.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); + "AnotherService", "CustomService", + "AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); @@ -74,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/gradle/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/gradle/src/main/java/helloworld/App.java index 297088479..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 @@ -33,8 +33,8 @@ import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.metrics.FlushMetrics; -import software.amazon.lambda.powertools.metrics.MetricsLogger; -import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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; @@ -46,7 +46,7 @@ */ public class App implements RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); - private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @@ -57,13 +57,13 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); DimensionSet dimensionSet = DimensionSet.of( "AnotherService", "CustomService", "AnotherService1", "CustomService1" ); - metricsLogger.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); 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 5052420cb..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 @@ -23,8 +23,8 @@ import org.slf4j.MDC 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.MetricsLogger -import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory +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 @@ -40,7 +40,7 @@ import java.net.URL class App : RequestHandler { private val log = LoggerFactory.getLogger(this::class.java) - private val metricsLogger: MetricsLogger = MetricsLoggerFactory.getMetricsLogger() + private val metrics: Metrics = MetricsFactory.getMetricsInstance() @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @@ -48,13 +48,13 @@ class App : RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); - private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @@ -58,15 +58,14 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); DimensionSet dimensionSet = DimensionSet.of( - "AnotherService", "CustomService", - "AnotherService1", "CustomService1" - ); - metricsLogger.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); + "AnotherService", "CustomService", + "AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); - metricsLogger.addMetric("CustomMetric3", 1, MetricUnit.COUNT, MetricResolution.HIGH); + metrics.addMetric("CustomMetric3", 1, MetricUnit.COUNT, MetricResolution.HIGH); MDC.put("test", "willBeLogged"); @@ -78,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 16449ff3b..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 @@ -36,8 +36,8 @@ import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.metrics.FlushMetrics; -import software.amazon.lambda.powertools.metrics.MetricsLogger; -import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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; @@ -50,7 +50,7 @@ */ public class App implements RequestHandler { private static final Logger log = LoggerFactory.getLogger(App.class); - private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @@ -61,14 +61,14 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); DimensionSet dimensionSet = new DimensionSet(); dimensionSet.addDimension("AnotherService", "CustomService"); dimensionSet.addDimension("AnotherService1", "CustomService1"); - metricsLogger.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); - metricsLogger.addMetric("CustomMetric3", 1, MetricUnit.COUNT, MetricResolution.HIGH); + metrics.addMetric("CustomMetric3", 1, MetricUnit.COUNT, MetricResolution.HIGH); MDC.put("test", "willBeLogged"); 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 f11cafc98..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 @@ -33,8 +33,8 @@ import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.metrics.FlushMetrics; -import software.amazon.lambda.powertools.metrics.MetricsLogger; -import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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; @@ -46,7 +46,7 @@ */ public class App implements RequestHandler { private static final Logger log = LogManager.getLogger(App.class); - private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @@ -57,13 +57,12 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); DimensionSet dimensionSet = DimensionSet.of( - "AnotherService", "CustomService", - "AnotherService1", "CustomService1" - ); - metricsLogger.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); + "AnotherService", "CustomService", + "AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); @@ -75,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/App.java b/examples/powertools-examples-core-utilities/terraform/src/main/java/helloworld/App.java index f11cafc98..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 @@ -33,8 +33,8 @@ import org.slf4j.MDC; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.metrics.FlushMetrics; -import software.amazon.lambda.powertools.metrics.MetricsLogger; -import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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; @@ -46,7 +46,7 @@ */ public class App implements RequestHandler { private static final Logger log = LogManager.getLogger(App.class); - private static final MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + private static final Metrics metrics = MetricsFactory.getMetricsInstance(); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @@ -57,13 +57,12 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); - metricsLogger.addMetric("CustomMetric1", 1, MetricUnit.COUNT); + metrics.addMetric("CustomMetric1", 1, MetricUnit.COUNT); DimensionSet dimensionSet = DimensionSet.of( - "AnotherService", "CustomService", - "AnotherService1", "CustomService1" - ); - metricsLogger.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); + "AnotherService", "CustomService", + "AnotherService1", "CustomService1"); + metrics.flushSingleMetric("CustomMetric2", 1, MetricUnit.COUNT, "Another", dimensionSet); MDC.put("test", "willBeLogged"); @@ -75,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/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java similarity index 90% rename from powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java rename to powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java index b05b0458e..be83fd7c1 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLogger.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/Metrics.java @@ -21,12 +21,14 @@ import software.amazon.lambda.powertools.metrics.model.MetricUnit; /** - * Interface for metrics logging + * Interface for metrics implementations. + * This interface is used to collect metrics in the Lambda function. + * It provides methods to add metrics, dimensions, and metadata. */ -public interface MetricsLogger { +public interface Metrics { /** - * Add a metric to the metrics logger + * Add a metric * * @param key the name of the metric * @param value the value of the metric @@ -36,7 +38,7 @@ public interface MetricsLogger { void addMetric(String key, double value, MetricUnit unit, MetricResolution resolution); /** - * Add a metric to the metrics logger with default resolution + * Add a metric with default resolution * * @param key the name of the metric * @param value the value of the metric @@ -47,7 +49,7 @@ default void addMetric(String key, double value, MetricUnit unit) { } /** - * Add a metric to the metrics logger with default unit and resolution + * Add a metric with default unit and resolution * * @param key the name of the metric * @param value the value of the metric @@ -57,7 +59,7 @@ default void addMetric(String key, double value) { } /** - * Add a dimension to the metrics logger. + * Add a dimension * This is equivalent to calling {@code addDimension(DimensionSet.of(key, value))} * * @param key the name of the dimension @@ -68,14 +70,14 @@ default void addDimension(String key, String value) { } /** - * Add a dimension set to the metrics logger + * Add a dimension set * * @param dimensionSet the dimension set to add */ void addDimension(DimensionSet dimensionSet); /** - * Add metadata to the metrics logger + * Add metadata * * @param key the name of the metadata * @param value the value of the metadata @@ -83,21 +85,21 @@ default void addDimension(String key, String value) { void addMetadata(String key, Object value); /** - * Set default dimensions for the metrics logger + * Set default dimensions * * @param dimensionSet the dimension set to use as default dimensions */ void setDefaultDimensions(DimensionSet dimensionSet); /** - * Get the default dimensions for the metrics logger + * Get the default dimensions * * @return the default dimensions as a DimensionSet */ DimensionSet getDefaultDimensions(); /** - * Set the namespace for the metrics logger + * Set the namespace * * @param namespace the namespace */ diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsBuilder.java similarity index 70% rename from powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java rename to powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsBuilder.java index f13c8b2bb..2eb127b00 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilder.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsBuilder.java @@ -21,16 +21,16 @@ import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; /** - * Builder for configuring the singleton MetricsLogger instance + * Builder for configuring the singleton Metrics instance */ -public class MetricsLoggerBuilder { +public class MetricsBuilder { private MetricsProvider provider; private String namespace; private String service; private boolean raiseOnEmptyMetrics = false; private final Map defaultDimensions = new LinkedHashMap<>(); - private MetricsLoggerBuilder() { + private MetricsBuilder() { } /** @@ -38,8 +38,8 @@ private MetricsLoggerBuilder() { * * @return a new builder instance */ - public static MetricsLoggerBuilder builder() { - return new MetricsLoggerBuilder(); + public static MetricsBuilder builder() { + return new MetricsBuilder(); } /** @@ -48,7 +48,7 @@ public static MetricsLoggerBuilder builder() { * @param provider the metrics provider * @return this builder */ - public MetricsLoggerBuilder withMetricsProvider(MetricsProvider provider) { + public MetricsBuilder withMetricsProvider(MetricsProvider provider) { this.provider = provider; return this; } @@ -59,7 +59,7 @@ public MetricsLoggerBuilder withMetricsProvider(MetricsProvider provider) { * @param namespace the namespace * @return this builder */ - public MetricsLoggerBuilder withNamespace(String namespace) { + public MetricsBuilder withNamespace(String namespace) { this.namespace = namespace; return this; } @@ -71,7 +71,7 @@ public MetricsLoggerBuilder withNamespace(String namespace) { * @param service the service name * @return this builder */ - public MetricsLoggerBuilder withService(String service) { + public MetricsBuilder withService(String service) { this.service = service; return this; } @@ -82,7 +82,7 @@ public MetricsLoggerBuilder withService(String service) { * @param raiseOnEmptyMetrics true to raise an exception, false otherwise * @return this builder */ - public MetricsLoggerBuilder withRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics) { + public MetricsBuilder withRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics) { this.raiseOnEmptyMetrics = raiseOnEmptyMetrics; return this; } @@ -94,7 +94,7 @@ public MetricsLoggerBuilder withRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics) * @param value the dimension value * @return this builder */ - public MetricsLoggerBuilder withDefaultDimension(String key, String value) { + public MetricsBuilder withDefaultDimension(String key, String value) { this.defaultDimensions.put(key, value); return this; } @@ -105,7 +105,7 @@ public MetricsLoggerBuilder withDefaultDimension(String key, String value) { * @param dimensionSet the dimension set to add * @return this builder */ - public MetricsLoggerBuilder withDefaultDimensions(DimensionSet dimensionSet) { + public MetricsBuilder withDefaultDimensions(DimensionSet dimensionSet) { if (dimensionSet != null) { this.defaultDimensions.putAll(dimensionSet.getDimensions()); } @@ -113,32 +113,32 @@ public MetricsLoggerBuilder withDefaultDimensions(DimensionSet dimensionSet) { } /** - * Configure and return the singleton MetricsLogger instance + * Configure and return the singleton Metrics instance * - * @return the configured singleton MetricsLogger instance + * @return the configured singleton Metrics instance */ - public MetricsLogger build() { + public Metrics build() { if (provider != null) { - MetricsLoggerFactory.setMetricsProvider(provider); + MetricsFactory.setMetricsProvider(provider); } - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + Metrics metrics = MetricsFactory.getMetricsInstance(); if (namespace != null) { - metricsLogger.setNamespace(namespace); + metrics.setNamespace(namespace); } - metricsLogger.setRaiseOnEmptyMetrics(raiseOnEmptyMetrics); + metrics.setRaiseOnEmptyMetrics(raiseOnEmptyMetrics); if (service != null) { - metricsLogger.setDefaultDimensions(DimensionSet.of("Service", service)); + metrics.setDefaultDimensions(DimensionSet.of("Service", service)); } // If the user provided default dimension, we overwrite the default Service dimension again if (!defaultDimensions.isEmpty()) { - metricsLogger.setDefaultDimensions(DimensionSet.of(defaultDimensions)); + metrics.setDefaultDimensions(DimensionSet.of(defaultDimensions)); } - return metricsLogger; + return metrics; } } diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java similarity index 72% rename from powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java rename to powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java index 2c8c9922f..1fd5f88ca 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactory.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java @@ -21,38 +21,38 @@ import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; /** - * Factory for accessing the singleton MetricsLogger instance + * Factory for accessing the singleton Metrics instance */ -public final class MetricsLoggerFactory { +public final class MetricsFactory { private static MetricsProvider provider = new EmfMetricsProvider(); - private static MetricsLogger metricsLogger; + private static Metrics metrics; - private MetricsLoggerFactory() { + private MetricsFactory() { } /** - * Get the singleton instance of the MetricsLogger + * Get the singleton instance of the Metrics * - * @return the singleton MetricsLogger instance + * @return the singleton Metrics instance */ - public static synchronized MetricsLogger getMetricsLogger() { - if (metricsLogger == null) { - metricsLogger = provider.getMetricsLogger(); + 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) { - metricsLogger.setNamespace(envNamespace); + 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)) { - metricsLogger.setDefaultDimensions(DimensionSet.of("Service", serviceName)); + metrics.setDefaultDimensions(DimensionSet.of("Service", serviceName)); } } - return metricsLogger; + return metrics; } /** @@ -65,7 +65,7 @@ public static synchronized void setMetricsProvider(MetricsProvider metricsProvid throw new IllegalArgumentException("Metrics provider cannot be null"); } provider = metricsProvider; - // Reset the logger so it will be recreated with the new provider - metricsLogger = null; + // 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/internal/EmfMetricsLogger.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/EmfMetricsLogger.java index 76337cbc4..06503c871 100644 --- 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 @@ -32,15 +32,15 @@ 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.MetricsLogger; +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 MetricsLogger that uses the EMF library. Proxies MetricsLogger interface calls to underlying + * 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 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"; 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 f765f9f83..456fd01b7 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 @@ -28,8 +28,8 @@ 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.MetricsLogger; -import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; import software.amazon.lambda.powertools.metrics.model.DimensionSet; @Aspect @@ -70,32 +70,32 @@ public Object around(ProceedingJoinPoint pjp, Object[] proceedArgs = pjp.getArgs(); if (isHandlerMethod(pjp)) { - MetricsLogger logger = MetricsLoggerFactory.getMetricsLogger(); + Metrics metricsInstance = MetricsFactory.getMetricsInstance(); - // The MetricsLoggerFactory applies default settings from the environment or can be configured by the - // MetricsLoggerBuilder. We only overwrite settings if they are explicitly set in the @FlushMetrics + // 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())) { - logger.setNamespace(metrics.namespace()); + metricsInstance.setNamespace(metrics.namespace()); } // 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()) && (logger.getDefaultDimensions().getDimensionKeys().size() <= 1 - || logger.getDefaultDimensions().getDimensionKeys().contains(SERVICE_DIMENSION))) { - logger.setDefaultDimensions(DimensionSet.of(SERVICE_DIMENSION, metrics.service())); + if (!"".equals(metrics.service().trim()) && (metricsInstance.getDefaultDimensions().getDimensionKeys().size() <= 1 + || metricsInstance.getDefaultDimensions().getDimensionKeys().contains(SERVICE_DIMENSION))) { + metricsInstance.setDefaultDimensions(DimensionSet.of(SERVICE_DIMENSION, metrics.service())); } - logger.setRaiseOnEmptyMetrics(metrics.raiseOnEmptyMetrics()); + metricsInstance.setRaiseOnEmptyMetrics(metrics.raiseOnEmptyMetrics()); // Add trace ID metadata if available LambdaHandlerProcessor.getXrayTraceId() - .ifPresent(traceId -> logger.addMetadata(TRACE_ID_PROPERTY, traceId)); + .ifPresent(traceId -> metricsInstance.addMetadata(TRACE_ID_PROPERTY, traceId)); Context extractedContext = extractContext(pjp); if (null != extractedContext) { - logger.addMetadata(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); + metricsInstance.addMetadata(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); // Only capture cold start metrics if configured if (metrics.captureColdStart()) { @@ -104,8 +104,8 @@ public Object around(ProceedingJoinPoint pjp, DimensionSet coldStartDimensions = new DimensionSet(); - // Get service name from logger default dimensions or fallback - String serviceName = logger.getDefaultDimensions().getDimensions().getOrDefault(SERVICE_DIMENSION, + // 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 @@ -117,7 +117,7 @@ public Object around(ProceedingJoinPoint pjp, coldStartDimensions.addDimension("FunctionName", funcName != null ? funcName : extractedContext.getFunctionName()); - logger.captureColdStartMetric(extractedContext, coldStartDimensions); + metricsInstance.captureColdStartMetric(extractedContext, coldStartDimensions); } } @@ -125,7 +125,7 @@ public Object around(ProceedingJoinPoint pjp, return pjp.proceed(proceedArgs); } finally { coldStartDone(); - logger.flush(); + metricsInstance.flush(); } } 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 index e9a6f6b85..12c99b18f 100644 --- 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 @@ -16,7 +16,7 @@ import software.amazon.cloudwatchlogs.emf.environment.EnvironmentProvider; import software.amazon.cloudwatchlogs.emf.model.MetricsContext; -import software.amazon.lambda.powertools.metrics.MetricsLogger; +import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.metrics.internal.EmfMetricsLogger; /** @@ -25,7 +25,7 @@ public class EmfMetricsProvider implements MetricsProvider { @Override - public MetricsLogger getMetricsLogger() { + public Metrics getMetricsInstance() { return new EmfMetricsLogger(new EnvironmentProvider(), new MetricsContext()); } -} \ No newline at end of file +} 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 index 7eb5a94c7..e6c79e000 100644 --- 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 @@ -14,7 +14,7 @@ package software.amazon.lambda.powertools.metrics.provider; -import software.amazon.lambda.powertools.metrics.MetricsLogger; +import software.amazon.lambda.powertools.metrics.Metrics; /** * Interface for metrics provider implementations @@ -22,9 +22,9 @@ public interface MetricsProvider { /** - * Get a new instance of a metrics logger + * Get a new instance of a metrics implementation * - * @return a new metrics logger instance + * @return a new metrics instance */ - MetricsLogger getMetricsLogger(); + Metrics getMetricsInstance(); } 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 index acd94260d..1bf3b6a69 100644 --- 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 @@ -38,8 +38,8 @@ /** * Tests to verify the hierarchy of precedence for configuration: - * 1. Metrics annotation - * 2. MetricsLoggerBuilder + * 1. @FlushMetrics annotation + * 2. MetricsBuilder * 3. Environment variables */ class ConfigurationPrecedenceTest { @@ -68,11 +68,11 @@ void tearDown() throws Exception { System.setOut(standardOut); // Reset the singleton state between tests - java.lang.reflect.Field field = MetricsLoggerFactory.class.getDeclaredField("metricsLogger"); + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); field.setAccessible(true); field.set(null, null); - field = MetricsLoggerFactory.class.getDeclaredField("provider"); + field = MetricsFactory.class.getDeclaredField("provider"); field.setAccessible(true); field.set(null, new software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider()); } @@ -83,7 +83,7 @@ void tearDown() throws Exception { void annotationShouldOverrideBuilderAndEnvironment() throws Exception { // Given // Configure with builder first - MetricsLoggerBuilder.builder() + MetricsBuilder.builder() .withNamespace("BuilderNamespace") .withService("BuilderService") .build(); @@ -112,7 +112,7 @@ void annotationShouldOverrideBuilderAndEnvironment() throws Exception { void builderShouldOverrideEnvironment() throws Exception { // Given // Configure with builder - MetricsLoggerBuilder.builder() + MetricsBuilder.builder() .withNamespace("BuilderNamespace") .withService("BuilderService") .build(); @@ -161,7 +161,7 @@ void environmentVariablesShouldBeUsedWhenNoOverrides() throws Exception { @Test void shouldUseDefaultsWhenNoConfiguration() throws Exception { // Given - MetricsLoggerBuilder.builder() + MetricsBuilder.builder() .withNamespace("TestNamespace") .build(); @@ -187,8 +187,8 @@ private static class HandlerWithMetricsAnnotation implements RequestHandler input, Context context) { - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); return "OK"; } } @@ -197,8 +197,8 @@ private static class HandlerWithDefaultMetricsAnnotation implements RequestHandl @Override @FlushMetrics public String handleRequest(Map input, Context context) { - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + 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/MetricsLoggerBuilderTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsBuilderTest.java similarity index 79% rename from powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java rename to powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsBuilderTest.java index c13188171..bd300fb6b 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerBuilderTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsBuilderTest.java @@ -30,10 +30,10 @@ 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.TestMetricsLogger; +import software.amazon.lambda.powertools.metrics.testutils.TestMetrics; import software.amazon.lambda.powertools.metrics.testutils.TestMetricsProvider; -class MetricsLoggerBuilderTest { +class MetricsBuilderTest { private final PrintStream standardOut = System.out; private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); @@ -49,11 +49,11 @@ void tearDown() throws Exception { System.setOut(standardOut); // Reset the singleton state between tests - java.lang.reflect.Field field = MetricsLoggerFactory.class.getDeclaredField("metricsLogger"); + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); field.setAccessible(true); field.set(null, null); - field = MetricsLoggerFactory.class.getDeclaredField("provider"); + field = MetricsFactory.class.getDeclaredField("provider"); field.setAccessible(true); field.set(null, new software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider()); } @@ -61,12 +61,12 @@ void tearDown() throws Exception { @Test void shouldBuildWithCustomNamespace() throws Exception { // When - MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + Metrics metrics = MetricsBuilder.builder() .withNamespace("CustomNamespace") .build(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); - metricsLogger.flush(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -79,13 +79,13 @@ void shouldBuildWithCustomNamespace() throws Exception { @Test void shouldBuildWithCustomService() throws Exception { // When - MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + Metrics metrics = MetricsBuilder.builder() .withService("CustomService") .withNamespace("TestNamespace") .build(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); - metricsLogger.flush(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -98,14 +98,14 @@ void shouldBuildWithCustomService() throws Exception { @Test void shouldBuildWithRaiseOnEmptyMetrics() { // When - MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + Metrics metrics = MetricsBuilder.builder() .withRaiseOnEmptyMetrics(true) .withNamespace("TestNamespace") .build(); // Then - assertThat(metricsLogger).isNotNull(); - assertThatThrownBy(metricsLogger::flush) + assertThat(metrics).isNotNull(); + assertThatThrownBy(metrics::flush) .isInstanceOf(IllegalStateException.class) .hasMessage("No metrics were emitted"); } @@ -113,13 +113,13 @@ void shouldBuildWithRaiseOnEmptyMetrics() { @Test void shouldBuildWithDefaultDimension() throws Exception { // When - MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + Metrics metrics = MetricsBuilder.builder() .withDefaultDimension("Environment", "Test") .withNamespace("TestNamespace") .build(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); - metricsLogger.flush(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -132,13 +132,13 @@ void shouldBuildWithDefaultDimension() throws Exception { @Test void shouldBuildWithMultipleDefaultDimensions() throws Exception { // When - MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + Metrics metrics = MetricsBuilder.builder() .withDefaultDimensions(DimensionSet.of("Environment", "Test", "Region", "us-west-2")) .withNamespace("TestNamespace") .build(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); - metricsLogger.flush(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -156,25 +156,25 @@ void shouldBuildWithCustomMetricsProvider() { MetricsProvider testProvider = new TestMetricsProvider(); // When - MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + Metrics metrics = MetricsBuilder.builder() .withMetricsProvider(testProvider) .build(); // Then - assertThat(metricsLogger).isInstanceOf(TestMetricsLogger.class); + assertThat(metrics).isInstanceOf(TestMetrics.class); } @Test void shouldOverrideServiceWithDefaultDimensions() throws Exception { // When - MetricsLogger metricsLogger = MetricsLoggerBuilder.builder() + Metrics metrics = MetricsBuilder.builder() .withService("OriginalService") .withDefaultDimensions(DimensionSet.of("Service", "OverriddenService")) .withNamespace("TestNamespace") .build(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); - metricsLogger.flush(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsFactoryTest.java similarity index 76% rename from powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java rename to powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsFactoryTest.java index 6c150ab3e..962f2c2d7 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsLoggerFactoryTest.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/MetricsFactoryTest.java @@ -32,10 +32,10 @@ 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.TestMetricsLogger; +import software.amazon.lambda.powertools.metrics.testutils.TestMetrics; import software.amazon.lambda.powertools.metrics.testutils.TestMetricsProvider; -class MetricsLoggerFactoryTest { +class MetricsFactoryTest { private static final String TEST_NAMESPACE = "TestNamespace"; private static final String TEST_SERVICE = "TestService"; @@ -64,29 +64,29 @@ void tearDown() throws Exception { System.setOut(standardOut); // Reset the singleton state between tests - java.lang.reflect.Field field = MetricsLoggerFactory.class.getDeclaredField("metricsLogger"); + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); field.setAccessible(true); field.set(null, null); - field = MetricsLoggerFactory.class.getDeclaredField("provider"); + field = MetricsFactory.class.getDeclaredField("provider"); field.setAccessible(true); field.set(null, new software.amazon.lambda.powertools.metrics.provider.EmfMetricsProvider()); } @Test - void shouldGetMetricsLoggerInstance() { + void shouldGetMetricsInstance() { // When - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + Metrics metrics = MetricsFactory.getMetricsInstance(); // Then - assertThat(metricsLogger).isNotNull(); + assertThat(metrics).isNotNull(); } @Test void shouldReturnSameInstanceOnMultipleCalls() { // When - MetricsLogger firstInstance = MetricsLoggerFactory.getMetricsLogger(); - MetricsLogger secondInstance = MetricsLoggerFactory.getMetricsLogger(); + Metrics firstInstance = MetricsFactory.getMetricsInstance(); + Metrics secondInstance = MetricsFactory.getMetricsInstance(); // Then assertThat(firstInstance).isSameAs(secondInstance); @@ -96,9 +96,9 @@ void shouldReturnSameInstanceOnMultipleCalls() { @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_NAMESPACE", value = TEST_NAMESPACE) void shouldUseNamespaceFromEnvironmentVariable() throws Exception { // When - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); - metricsLogger.flush(); + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -112,10 +112,10 @@ void shouldUseNamespaceFromEnvironmentVariable() throws Exception { @SetEnvironmentVariable(key = "POWERTOOLS_SERVICE_NAME", value = TEST_SERVICE) void shouldUseServiceNameFromEnvironmentVariable() throws Exception { // When - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.setNamespace("TestNamespace"); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); - metricsLogger.flush(); + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.setNamespace("TestNamespace"); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -131,17 +131,17 @@ void shouldSetCustomMetricsProvider() { MetricsProvider testProvider = new TestMetricsProvider(); // When - MetricsLoggerFactory.setMetricsProvider(testProvider); - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); + MetricsFactory.setMetricsProvider(testProvider); + Metrics metrics = MetricsFactory.getMetricsInstance(); // Then - assertThat(metricsLogger).isInstanceOf(TestMetricsLogger.class); + assertThat(metrics).isInstanceOf(TestMetrics.class); } @Test void shouldThrowExceptionWhenSettingNullProvider() { // When/Then - assertThatThrownBy(() -> MetricsLoggerFactory.setMetricsProvider(null)) + assertThatThrownBy(() -> MetricsFactory.setMetricsProvider(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Metrics provider cannot be null"); } @@ -151,10 +151,10 @@ void shouldNotSetServiceDimensionWhenServiceUndefined() throws Exception { // Given - no POWERTOOLS_SERVICE_NAME set, so it will use the default undefined value // When - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.setNamespace("TestNamespace"); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); - metricsLogger.flush(); + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.setNamespace("TestNamespace"); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); 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 index 5e3bce4ac..c0cebcb60 100644 --- 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 @@ -39,8 +39,8 @@ import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; -import software.amazon.lambda.powertools.metrics.MetricsLogger; -import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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; @@ -48,7 +48,7 @@ class EmfMetricsLoggerTest { - private MetricsLogger metricsLogger; + private Metrics metrics; private final ObjectMapper objectMapper = new ObjectMapper(); private final PrintStream standardOut = System.out; private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); @@ -65,8 +65,8 @@ void setUp() throws Exception { coldStartField.setAccessible(true); coldStartField.set(null, null); - metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.setNamespace("TestNamespace"); + metrics = MetricsFactory.getMetricsInstance(); + metrics.setNamespace("TestNamespace"); System.setOut(new PrintStream(outputStreamCaptor)); } @@ -75,7 +75,7 @@ void tearDown() throws Exception { System.setOut(standardOut); // Reset the singleton state between tests - java.lang.reflect.Field field = MetricsLoggerFactory.class.getDeclaredField("metricsLogger"); + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); field.setAccessible(true); field.set(null, null); } @@ -89,7 +89,7 @@ void shouldConvertMetricUnits(MetricUnit inputUnit, Unit expectedUnit) throws Ex convertUnitMethod.setAccessible(true); // When - Unit actualUnit = (Unit) convertUnitMethod.invoke(metricsLogger, inputUnit); + Unit actualUnit = (Unit) convertUnitMethod.invoke(metrics, inputUnit); // Then assertThat(actualUnit).isEqualTo(expectedUnit); @@ -129,8 +129,8 @@ private static Stream unitConversionTestCases() { @Test void shouldCreateMetricWithDefaultResolution() throws Exception { // When - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); - metricsLogger.flush(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -145,8 +145,8 @@ void shouldCreateMetricWithDefaultResolution() throws Exception { @Test void shouldCreateMetricWithHighResolution() throws Exception { // When - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT, MetricResolution.HIGH); - metricsLogger.flush(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT, MetricResolution.HIGH); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -163,10 +163,10 @@ void shouldCreateMetricWithHighResolution() throws Exception { @Test void shouldAddDimension() throws Exception { // When - metricsLogger.clearDefaultDimensions(); // Clear default Service dimension first for easier assertions - metricsLogger.addDimension("CustomDimension", "CustomValue"); - metricsLogger.addMetric("test-metric", 100); - metricsLogger.flush(); + 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(); @@ -193,10 +193,10 @@ void shouldAddDimensionSet() throws Exception { DimensionSet dimensionSet = DimensionSet.of("Dim1", "Value1", "Dim2", "Value2"); // When - metricsLogger.clearDefaultDimensions(); // Clear default Service dimension first for easier assertions - metricsLogger.addDimension(dimensionSet); - metricsLogger.addMetric("test-metric", 100); - metricsLogger.flush(); + 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(); @@ -226,7 +226,7 @@ void shouldAddDimensionSet() throws Exception { @Test void shouldThrowExceptionWhenDimensionSetIsNull() { // When/Then - assertThatThrownBy(() -> metricsLogger.addDimension((DimensionSet) null)) + assertThatThrownBy(() -> metrics.addDimension((DimensionSet) null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("DimensionSet cannot be null"); } @@ -234,9 +234,9 @@ void shouldThrowExceptionWhenDimensionSetIsNull() { @Test void shouldAddMetadata() throws Exception { // When - metricsLogger.addMetadata("CustomMetadata", "MetadataValue"); - metricsLogger.addMetric("test-metric", 100); - metricsLogger.flush(); + metrics.addMetadata("CustomMetadata", "MetadataValue"); + metrics.addMetric("test-metric", 100); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -253,9 +253,9 @@ void shouldSetDefaultDimensions() throws Exception { DimensionSet dimensionSet = DimensionSet.of("Service", "TestService", "Environment", "Test"); // When - metricsLogger.setDefaultDimensions(dimensionSet); - metricsLogger.addMetric("test-metric", 100); - metricsLogger.flush(); + metrics.setDefaultDimensions(dimensionSet); + metrics.addMetric("test-metric", 100); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -273,8 +273,8 @@ void shouldGetDefaultDimensions() { DimensionSet dimensionSet = DimensionSet.of("Service", "TestService", "Environment", "Test"); // When - metricsLogger.setDefaultDimensions(dimensionSet); - DimensionSet dimensions = metricsLogger.getDefaultDimensions(); + metrics.setDefaultDimensions(dimensionSet); + DimensionSet dimensions = metrics.getDefaultDimensions(); // Then assertThat(dimensions.getDimensions()).containsEntry("Service", "TestService"); @@ -284,7 +284,7 @@ void shouldGetDefaultDimensions() { @Test void shouldThrowExceptionWhenDefaultDimensionSetIsNull() { // When/Then - assertThatThrownBy(() -> metricsLogger.setDefaultDimensions((DimensionSet) null)) + assertThatThrownBy(() -> metrics.setDefaultDimensions((DimensionSet) null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("DimensionSet cannot be null"); } @@ -292,9 +292,9 @@ void shouldThrowExceptionWhenDefaultDimensionSetIsNull() { @Test void shouldSetNamespace() throws Exception { // When - metricsLogger.setNamespace("CustomNamespace"); - metricsLogger.addMetric("test-metric", 100); - metricsLogger.flush(); + metrics.setNamespace("CustomNamespace"); + metrics.addMetric("test-metric", 100); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -307,10 +307,10 @@ void shouldSetNamespace() throws Exception { @Test void shouldRaiseExceptionOnEmptyMetrics() { // When - metricsLogger.setRaiseOnEmptyMetrics(true); + metrics.setRaiseOnEmptyMetrics(true); // Then - assertThatThrownBy(() -> metricsLogger.flush()) + assertThatThrownBy(() -> metrics.flush()) .isInstanceOf(IllegalStateException.class) .hasMessage("No metrics were emitted"); } @@ -322,7 +322,7 @@ void shouldLogWarningOnEmptyMetrics() throws Exception { // When // Flushing without adding metrics - metricsLogger.flush(); + metrics.flush(); // Then // Read the log file and check for the warning @@ -333,12 +333,12 @@ void shouldLogWarningOnEmptyMetrics() throws Exception { @Test void shouldClearDefaultDimensions() throws Exception { // Given - metricsLogger.setDefaultDimensions(DimensionSet.of("Service", "TestService", "Environment", "Test")); + metrics.setDefaultDimensions(DimensionSet.of("Service", "TestService", "Environment", "Test")); // When - metricsLogger.clearDefaultDimensions(); - metricsLogger.addMetric("test-metric", 100); - metricsLogger.flush(); + metrics.clearDefaultDimensions(); + metrics.addMetric("test-metric", 100); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -354,7 +354,7 @@ void shouldCaptureColdStartMetric() throws Exception { Context testContext = new TestContext(); // When - metricsLogger.captureColdStartMetric(testContext); + metrics.captureColdStartMetric(testContext); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -372,7 +372,7 @@ void shouldCaptureColdStartMetricWithDimensions() throws Exception { DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); // When - metricsLogger.captureColdStartMetric(dimensions); + metrics.captureColdStartMetric(dimensions); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -387,7 +387,7 @@ void shouldCaptureColdStartMetricWithDimensions() throws Exception { @Test void shouldCaptureColdStartMetricWithoutDimensions() throws Exception { // When - metricsLogger.captureColdStartMetric(); + metrics.captureColdStartMetric(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -401,14 +401,14 @@ void shouldCaptureColdStartMetricWithoutDimensions() throws Exception { void shouldReuseNamespaceForColdStartMetric() throws Exception { // Given String customNamespace = "CustomNamespace"; - metricsLogger.setNamespace(customNamespace); + metrics.setNamespace(customNamespace); Context testContext = new TestContext(); DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); // When - metricsLogger.captureColdStartMetric(testContext, dimensions); + metrics.captureColdStartMetric(testContext, dimensions); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -430,7 +430,7 @@ void shouldFlushSingleMetric() throws Exception { DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); // When - metricsLogger.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace", dimensions); + metrics.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace", dimensions); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -447,7 +447,7 @@ void shouldFlushSingleMetric() throws Exception { @Test void shouldFlushSingleMetricWithoutDimensions() throws Exception { // When - metricsLogger.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace"); + metrics.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace"); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -463,8 +463,8 @@ void shouldFlushSingleMetricWithoutDimensions() throws Exception { @SetEnvironmentVariable(key = "POWERTOOLS_METRICS_DISABLED", value = "true") void shouldNotFlushMetricsWhenDisabled() { // When - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); - metricsLogger.flush(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); + metrics.flush(); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -478,7 +478,7 @@ void shouldNotCaptureColdStartMetricWhenDisabled() { Context testContext = new TestContext(); // When - metricsLogger.captureColdStartMetric(testContext); + metrics.captureColdStartMetric(testContext); // Then String emfOutput = outputStreamCaptor.toString().trim(); @@ -492,7 +492,7 @@ void shouldNotFlushSingleMetricWhenDisabled() { DimensionSet dimensions = DimensionSet.of("CustomDim", "CustomValue"); // When - metricsLogger.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace", dimensions); + metrics.flushSingleMetric("single-metric", 200, MetricUnit.COUNT, "SingleNamespace", dimensions); // Then String emfOutput = outputStreamCaptor.toString().trim(); 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 2a6f752a5..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 @@ -34,8 +34,8 @@ import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.metrics.FlushMetrics; -import software.amazon.lambda.powertools.metrics.MetricsLogger; -import software.amazon.lambda.powertools.metrics.MetricsLoggerFactory; +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; @@ -65,7 +65,7 @@ void tearDown() throws Exception { System.setOut(standardOut); // Reset the singleton state between tests - java.lang.reflect.Field field = MetricsLoggerFactory.class.getDeclaredField("metricsLogger"); + java.lang.reflect.Field field = MetricsFactory.class.getDeclaredField("metrics"); field.setAccessible(true); field.set(null, null); } @@ -264,8 +264,8 @@ static class HandlerWithMetricsAnnotation implements RequestHandler input, Context context) { - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); return "OK"; } } @@ -274,8 +274,8 @@ static class HandlerWithDefaultMetricsAnnotation implements RequestHandler input, Context context) { - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); return "OK"; } } @@ -284,8 +284,8 @@ static class HandlerWithColdStartMetricsAnnotation implements RequestHandler input, Context context) { - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); return "OK"; } } @@ -294,8 +294,8 @@ static class HandlerWithCustomFunctionName implements RequestHandler input, Context context) { - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); return "OK"; } } @@ -304,8 +304,8 @@ static class HandlerWithServiceNameAndColdStart implements RequestHandler input, Context context) { - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); return "OK"; } } @@ -313,16 +313,16 @@ public String handleRequest(Map input, Context context) { static class HandlerWithAnnotationOnWrongMethod implements RequestHandler, String> { @Override public String handleRequest(Map input, Context context) { - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); someOtherMethod(); return "OK"; } @FlushMetrics public void someOtherMethod() { - MetricsLogger metricsLogger = MetricsLoggerFactory.getMetricsLogger(); - metricsLogger.addMetric("test-metric", 100, MetricUnit.COUNT); + Metrics metrics = MetricsFactory.getMetricsInstance(); + metrics.addMetric("test-metric", 100, MetricUnit.COUNT); } } } 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 index f961366e4..2b2268ea8 100644 --- 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 @@ -18,7 +18,7 @@ import org.junit.jupiter.api.Test; -import software.amazon.lambda.powertools.metrics.MetricsLogger; +import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.metrics.internal.EmfMetricsLogger; class EmfMetricsProviderTest { @@ -29,10 +29,10 @@ void shouldCreateEmfMetricsLogger() { EmfMetricsProvider provider = new EmfMetricsProvider(); // When - MetricsLogger logger = provider.getMetricsLogger(); + Metrics metrics = provider.getMetricsInstance(); // Then - assertThat(logger) + assertThat(metrics) .isNotNull() .isInstanceOf(EmfMetricsLogger.class); } diff --git a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetrics.java similarity index 94% rename from powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java rename to powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetrics.java index 1512566ee..740bee7ca 100644 --- a/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetricsLogger.java +++ b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetrics.java @@ -4,12 +4,12 @@ import com.amazonaws.services.lambda.runtime.Context; -import software.amazon.lambda.powertools.metrics.MetricsLogger; +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 TestMetricsLogger implements MetricsLogger { +public class TestMetrics implements Metrics { @Override public void addMetric(String name, double value, MetricUnit unit) { // 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 index d43a35b3c..4a5fc23dd 100644 --- 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 @@ -1,11 +1,11 @@ package software.amazon.lambda.powertools.metrics.testutils; -import software.amazon.lambda.powertools.metrics.MetricsLogger; +import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.metrics.provider.MetricsProvider; public class TestMetricsProvider implements MetricsProvider { @Override - public MetricsLogger getMetricsLogger() { - return new TestMetricsLogger(); + public Metrics getMetricsInstance() { + return new TestMetrics(); } } diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 0e69cf902..9a42ebf16 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -234,8 +234,8 @@ - - + + From ef3347a628dfbf4e289e74e4af6f16d863902347 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 15:46:29 +0200 Subject: [PATCH 30/36] Add support for setTimestamp. --- .../lambda/powertools/metrics/Metrics.java | 8 ++++ .../metrics/internal/EmfMetricsLogger.java | 8 ++++ .../metrics/internal/Validator.java | 36 +++++++++++++++ .../internal/EmfMetricsLoggerTest.java | 20 +++++++++ .../metrics/internal/ValidatorTest.java | 44 +++++++++++++++++++ .../metrics/testutils/TestMetrics.java | 6 +++ 6 files changed, 122 insertions(+) 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 be83fd7c1..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 @@ -15,6 +15,7 @@ package software.amazon.lambda.powertools.metrics; 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; @@ -76,6 +77,13 @@ default void addDimension(String key, String value) { */ 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 * 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 index 06503c871..a55e1da5a 100644 --- 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 @@ -17,6 +17,7 @@ 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; @@ -135,6 +136,13 @@ 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); 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 index 89639fbde..03a1766c5 100644 --- 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 @@ -14,6 +14,9 @@ package software.amazon.lambda.powertools.metrics.internal; +import java.time.Instant; +import java.util.concurrent.TimeUnit; + import org.apache.commons.lang3.StringUtils; /** @@ -24,6 +27,8 @@ public class Validator { 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 @@ -50,6 +55,37 @@ public static void validateNamespace(String 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. * 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 index c0cebcb60..1b7106ece 100644 --- 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 @@ -23,6 +23,7 @@ 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; @@ -187,6 +188,25 @@ void shouldAddDimension() throws Exception { 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 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 index 9e61abf2c..e5d780f9f 100644 --- 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 @@ -17,6 +17,8 @@ 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 { @@ -177,4 +179,46 @@ void shouldAcceptValidDimension() { 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/testutils/TestMetrics.java b/powertools-metrics/src/test/java/software/amazon/lambda/powertools/metrics/testutils/TestMetrics.java index 740bee7ca..949828a13 100644 --- 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 @@ -1,5 +1,6 @@ package software.amazon.lambda.powertools.metrics.testutils; +import java.time.Instant; import java.util.Collections; import com.amazonaws.services.lambda.runtime.Context; @@ -61,6 +62,11 @@ public void clearDefaultDimensions() { // Test placeholder } + @Override + public void setTimestamp(Instant timestamp) { + // Test placeholder + } + @Override public void captureColdStartMetric(Context context, DimensionSet dimensions) { // Test placeholder From 6b82ea5759bf97eb10f03064ac2ce6f3a7272d20 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 15:58:14 +0200 Subject: [PATCH 31/36] Update regex for namespace validation. --- .../amazon/lambda/powertools/metrics/internal/Validator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 03a1766c5..77a21ba95 100644 --- 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 @@ -26,7 +26,7 @@ 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._#/]+$"; + 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); From 120b8d0c958fe29e2b0e97fac5d63409fdb339fa Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 5 Jun 2025 16:06:07 +0200 Subject: [PATCH 32/36] Fix namespace regex and fix metrics e2e tests. --- .../lambda/powertools/e2e/Function.java | 27 +++++++++---------- .../metrics/internal/Validator.java | 2 +- 2 files changed, 14 insertions(+), 15 deletions(-) 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 03eb8979e..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,36 +16,35 @@ 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.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.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(); @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"; } 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 index 77a21ba95..eebb54739 100644 --- 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 @@ -26,7 +26,7 @@ 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.-_#/:]+$"; + 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); From 26a4b1dec83455305ccd0a5e154bc090c539c95e Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 6 Jun 2025 09:11:23 +0200 Subject: [PATCH 33/36] Add comment in empty callAt method to satisfy pmd_analyse. --- .../powertools/metrics/internal/LambdaMetricsAspect.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 456fd01b7..7189479bb 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 @@ -62,6 +62,7 @@ private String serviceNameWithFallback(FlushMetrics metrics) { @SuppressWarnings({ "EmptyMethod" }) @Pointcut("@annotation(metrics)") public void callAt(FlushMetrics metrics) { + // AspectJ point cut referenced in around() method } @Around(value = "callAt(metrics) && execution(@FlushMetrics * *.*(..))", argNames = "pjp,metrics") @@ -81,8 +82,9 @@ public Object around(ProceedingJoinPoint pjp, // 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))) { + if (!"".equals(metrics.service().trim()) + && (metricsInstance.getDefaultDimensions().getDimensionKeys().size() <= 1 + || metricsInstance.getDefaultDimensions().getDimensionKeys().contains(SERVICE_DIMENSION))) { metricsInstance.setDefaultDimensions(DimensionSet.of(SERVICE_DIMENSION, metrics.service())); } @@ -105,7 +107,8 @@ public Object around(ProceedingJoinPoint pjp, DimensionSet coldStartDimensions = new DimensionSet(); // Get service name from metrics instance default dimensions or fallback - String serviceName = metricsInstance.getDefaultDimensions().getDimensions().getOrDefault(SERVICE_DIMENSION, + String serviceName = metricsInstance.getDefaultDimensions().getDimensions().getOrDefault( + SERVICE_DIMENSION, serviceNameWithFallback(metrics)); // Only add service if it is not undefined From 0017e8b10ffcf38b1741cef1a115eb9dfd17bd10 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 6 Jun 2025 09:25:13 +0200 Subject: [PATCH 34/36] Replace usages of standalone Powertools wording with alternative wording. --- docs/FAQs.md | 43 +++++++++++++++++++++++-------------------- docs/core/logging.md | 10 +++++----- docs/core/metrics.md | 12 ++++++------ docs/core/tracing.md | 2 +- docs/upgrade.md | 2 +- 5 files changed, 36 insertions(+), 33 deletions(-) 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 08ce7ad27..2d9e57dda 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -220,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" @@ -278,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`. @@ -309,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** > @@ -739,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 @@ -974,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 fbf558a4b..35d08b140 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -111,12 +111,12 @@ Visit the AWS documentation for a complete explanation for [Amazon CloudWatch co 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 | 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 Powertools Metrics** | Optionally, disables all Powertools metrics | `POWERTOOLS_METRICS_DISABLED` | N/A | +| 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" diff --git a/docs/core/tracing.md b/docs/core/tracing.md index b6e142609..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) 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." From 3d0285381d9d249cac2d0d9cd1876beef5b18dc8 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 6 Jun 2025 09:34:07 +0200 Subject: [PATCH 35/36] Reduce cognitive complexity of around() method in LambdaMetricsAspect. --- .../metrics/internal/LambdaMetricsAspect.java | 63 ++++++++++--------- 1 file changed, 34 insertions(+), 29 deletions(-) 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 7189479bb..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 @@ -94,35 +94,7 @@ public Object around(ProceedingJoinPoint pjp, LambdaHandlerProcessor.getXrayTraceId() .ifPresent(traceId -> metricsInstance.addMetadata(TRACE_ID_PROPERTY, traceId)); - Context extractedContext = extractContext(pjp); - - if (null != extractedContext) { - metricsInstance.addMetadata(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); - - // Only capture cold start metrics if configured - if (metrics.captureColdStart()) { - // Get function name from annotation or context - String funcName = functionName(metrics, extractedContext); - - DimensionSet coldStartDimensions = new DimensionSet(); - - // 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()); - - metricsInstance.captureColdStartMetric(extractedContext, coldStartDimensions); - } - } + captureColdStartMetricIfEnabled(extractContext(pjp), metrics); try { return pjp.proceed(proceedArgs); @@ -134,4 +106,37 @@ public Object around(ProceedingJoinPoint pjp, return pjp.proceed(proceedArgs); } + + private void captureColdStartMetricIfEnabled(Context extractedContext, FlushMetrics metrics) { + if (extractedContext == null) { + return; + } + + Metrics metricsInstance = MetricsFactory.getMetricsInstance(); + metricsInstance.addMetadata(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); + + // Only capture cold start metrics if enabled on annotation + if (metrics.captureColdStart()) { + // Get function name from annotation or context + String funcName = functionName(metrics, extractedContext); + + DimensionSet coldStartDimensions = new DimensionSet(); + + // 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()); + + metricsInstance.captureColdStartMetric(extractedContext, coldStartDimensions); + } + } } From 53d054da22f629ac4011d1347f2913626adeaebd Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 6 Jun 2025 09:39:28 +0200 Subject: [PATCH 36/36] Re-generate GRM files after applying MetricsLogger renaming. --- .../powertools-metrics/jni-config.json | 4 ++++ .../powertools-metrics/reflect-config.json | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) 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 410a0d0cd..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,4 +1,8 @@ [ + { + "name": "java.lang.Boolean", + "methods": [{ "name": "getBoolean", "parameterTypes": ["java.lang.String"] }] + }, { "name": "java.lang.String", "methods": [ 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 c301750cf..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 @@ -130,13 +130,13 @@ "methods": [{ "name": "resetServiceName", "parameterTypes": [] }] }, { - "name": "software.amazon.lambda.powertools.metrics.MetricsLogger", + "name": "software.amazon.lambda.powertools.metrics.Metrics", "allDeclaredClasses": true, "queryAllPublicMethods": true }, { - "name": "software.amazon.lambda.powertools.metrics.MetricsLoggerFactory", - "fields": [{ "name": "metricsLogger" }, { "name": "provider" }] + "name": "software.amazon.lambda.powertools.metrics.MetricsFactory", + "fields": [{ "name": "metrics" }, { "name": "provider" }] }, { "name": "software.amazon.lambda.powertools.metrics.internal.EmfMetricsLogger",