From 13eb9d1bddf29a1b47ce4afed686a3c44540417e Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Mon, 29 Sep 2025 13:57:20 +0100 Subject: [PATCH 1/9] feat(metrics): introduce Metrics.flushMetrics - introduce `Metrics.flushMetrics` as a more powerful version of `flushSingleMetrics` to allow - using defaults by inheriting state e.g. namespace, dimensions and metadata - emitting multiple metrics in one metrics context - refactor `flushSingleMetrics` to use `flushMetrics` - move namespace/service setting from `MetricsFactory` to `EmfMetricsLogger` --- .../lambda/powertools/metrics/Metrics.java | 27 +++++++-- .../powertools/metrics/MetricsFactory.java | 15 ----- .../metrics/internal/EmfMetricsLogger.java | 60 +++++++++---------- .../internal/EmfMetricsLoggerTest.java | 35 ++++++++++- .../metrics/testutils/TestMetrics.java | 6 ++ spotbugs-exclude.xml | 4 ++ 6 files changed, 94 insertions(+), 53 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 d21fe163e..ba7b246c4 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 @@ -16,6 +16,7 @@ import com.amazonaws.services.lambda.runtime.Context; import java.time.Instant; +import java.util.function.Consumer; import software.amazon.lambda.powertools.metrics.model.DimensionSet; import software.amazon.lambda.powertools.metrics.model.MetricResolution; @@ -162,7 +163,15 @@ default void captureColdStartMetric() { } /** - * Flush a single metric with custom dimensions. This creates a separate metrics context + * Flush a separate metrics context that inherits the namespace, dimensions and metadata. This creates a separate metrics context + * that doesn't affect the default metrics context. + * + * @param metricsConsumer the consumer to use to edit the metrics instance (e.g. add metrics, override namespace) before flushing + */ + void flushMetrics(Consumer metricsConsumer); + + /** + * Flush a single metric with custom namespace and dimensions. This creates a separate metrics context * that doesn't affect the default metrics context. * * @param name the name of the metric @@ -171,10 +180,17 @@ default void captureColdStartMetric() { * @param namespace the namespace for the metric * @param dimensions custom dimensions for this metric (optional) */ - void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, DimensionSet dimensions); + default void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, DimensionSet dimensions) { + flushMetrics(metrics -> { + metrics.setNamespace(namespace); + metrics.setDefaultDimensions(dimensions); + metrics.addMetric(name, value, unit); + }); + + } /** - * Flush a single metric with custom dimensions. This creates a separate metrics context + * Flush a single metric with custom namespace. This creates a separate metrics context * that doesn't affect the default metrics context. * * @param name the name of the metric @@ -183,6 +199,9 @@ default void captureColdStartMetric() { * @param namespace the namespace for the metric */ default void flushSingleMetric(String name, double value, MetricUnit unit, String namespace) { - flushSingleMetric(name, value, unit, namespace, null); + flushMetrics(metrics -> { + metrics.setNamespace(namespace); + metrics.addMetric(name, value, unit); + }); } } diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java index 67ab17b7b..a0f5405c9 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java @@ -17,9 +17,6 @@ import org.crac.Core; import org.crac.Resource; import software.amazon.lambda.powertools.common.internal.ClassPreLoader; -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; @@ -46,18 +43,6 @@ public final class MetricsFactory implements Resource { public static synchronized Metrics getMetricsInstance() { if (metrics == null) { metrics = provider.getMetricsInstance(); - - // Apply default configuration from environment variables - String envNamespace = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); - if (envNamespace != null) { - metrics.setNamespace(envNamespace); - } - - // Only set Service dimension if it's not the default undefined value - String serviceName = LambdaHandlerProcessor.serviceName(); - if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) { - metrics.setDefaultDimensions(DimensionSet.of("Service", serviceName)); - } } return metrics; 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 c9651585f..1993f7318 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,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +34,8 @@ 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.common.internal.LambdaConstants; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.metrics.model.MetricResolution; import software.amazon.lambda.powertools.metrics.model.MetricUnit; @@ -50,15 +53,30 @@ public class EmfMetricsLogger implements Metrics { private final software.amazon.cloudwatchlogs.emf.logger.MetricsLogger emfLogger; private final EnvironmentProvider environmentProvider; - private AtomicBoolean raiseOnEmptyMetrics = new AtomicBoolean(false); + private final MetricsContext metricsContext; + private final AtomicBoolean raiseOnEmptyMetrics = new AtomicBoolean(false); private String namespace; private Map defaultDimensions = new HashMap<>(); + private final Map metadata = 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; + this.metricsContext = metricsContext; + + // Apply default configuration from environment variables + String envNamespace = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); + if (envNamespace != null) { + setNamespace(envNamespace); + } + + // Only set Service dimension if it's not the default undefined value + String serviceName = LambdaHandlerProcessor.serviceName(); + if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) { + setDefaultDimensions(software.amazon.lambda.powertools.metrics.model.DimensionSet.of("Service", serviceName)); + } } @Override @@ -92,6 +110,7 @@ public void addDimension(software.amazon.lambda.powertools.metrics.model.Dimensi @Override public void addMetadata(String key, Object value) { emfLogger.putMetadata(key, value); + metadata.put(key, value); } @Override @@ -221,43 +240,22 @@ 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) { + public void flushMetrics(Consumer metricsConsumer) { if (isMetricsDisabled()) { LOGGER.debug("Metrics are disabled, skipping single metric flush"); return; } - - Validator.validateNamespace(namespace); - - // Create a new logger for this single metric - software.amazon.cloudwatchlogs.emf.logger.MetricsLogger singleMetricLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger( - environmentProvider); - - try { - singleMetricLogger.setNamespace(namespace); - } catch (Exception e) { - LOGGER.error("Namespace cannot be set for single metric due to an error in EMF", e); + // Create a new instance, inheriting namespace/dimensions state + EmfMetricsLogger metrics = new EmfMetricsLogger(environmentProvider, metricsContext); + if (namespace != null) { + metrics.setNamespace(this.namespace); } + defaultDimensions.forEach(metrics::addDimension); + metadata.forEach(metrics::addMetadata); - // 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); - } + metricsConsumer.accept(metrics); - // Flush the metric - singleMetricLogger.flush(); + metrics.flush(); } private boolean isMetricsDisabled() { 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 6c324221c..f38e0e200 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 @@ -246,7 +246,7 @@ void shouldAddDimensionSet() throws Exception { @Test void shouldThrowExceptionWhenDimensionSetIsNull() { // When/Then - assertThatThrownBy(() -> metrics.addDimension((DimensionSet) null)) + assertThatThrownBy(() -> metrics.addDimension(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("DimensionSet cannot be null"); } @@ -304,7 +304,7 @@ void shouldGetDefaultDimensions() { @Test void shouldThrowExceptionWhenDefaultDimensionSetIsNull() { // When/Then - assertThatThrownBy(() -> metrics.setDefaultDimensions((DimensionSet) null)) + assertThatThrownBy(() -> metrics.setDefaultDimensions(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("DimensionSet cannot be null"); } @@ -346,7 +346,7 @@ void shouldLogWarningOnEmptyMetrics() throws Exception { // Then // Read the log file and check for the warning - String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8); + String logContent = Files.readString(logFile.toPath(), StandardCharsets.UTF_8); assertThat(logContent).contains("No metrics were emitted"); // No EMF output should be generated assertThat(outputStreamCaptor.toString().trim()).isEmpty(); @@ -446,6 +446,35 @@ void shouldReuseNamespaceForColdStartMetric() throws Exception { .isEqualTo(customNamespace); } + @Test + void shouldFlushMetrics() throws Exception { + // Given + metrics.setNamespace("MainNamespace"); + metrics.setDefaultDimensions(DimensionSet.of("CustomDim", "CustomValue")); + metrics.addMetadata("CustomMetadata", "MetadataValue"); + + // When + metrics.flushMetrics(m -> { + m.addMetric("metric-one", 200, MetricUnit.COUNT); + m.addMetric("metric-two", 100, MetricUnit.COUNT); + }); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("metric-one")).isTrue(); + assertThat(rootNode.get("metric-one").asDouble()).isEqualTo(200.0); + assertThat(rootNode.has("metric-two")).isTrue(); + assertThat(rootNode.get("metric-two").asDouble()).isEqualTo(100); + assertThat(rootNode.has("CustomDim")).isTrue(); + assertThat(rootNode.get("CustomDim").asText()).isEqualTo("CustomValue"); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("MainNamespace"); + assertThat(rootNode.get("_aws").has("CustomMetadata")).isTrue(); + assertThat(rootNode.get("_aws").get("CustomMetadata").asText()).isEqualTo("MetadataValue"); + } + @Test void shouldFlushSingleMetric() throws Exception { // Given 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 949828a13..4a2e33a78 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 @@ -2,6 +2,7 @@ import java.time.Instant; import java.util.Collections; +import java.util.function.Consumer; import com.amazonaws.services.lambda.runtime.Context; @@ -77,6 +78,11 @@ public void captureColdStartMetric(DimensionSet dimensions) { // Test placeholder } + @Override + public void flushMetrics(Consumer metricsConsumer) { + // Test placeholder + } + @Override public void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, DimensionSet dimensions) { diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 35aed5e26..9075e86b7 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -90,6 +90,10 @@ + + + + From 537dc90fc5814cb6112a6f9f2f6cb8bf642457b6 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Mon, 29 Sep 2025 13:57:20 +0100 Subject: [PATCH 2/9] feat(metrics): introduce Metrics.flushMetrics - introduce `Metrics.flushMetrics` as a more powerful version of `flushSingleMetrics` to allow - using defaults by inheriting state e.g. namespace, dimensions and metadata - emitting multiple metrics in one metrics context - refactor `flushSingleMetrics` to use `flushMetrics` - move namespace/service setting from `MetricsFactory` to `EmfMetricsLogger` --- .../lambda/powertools/metrics/Metrics.java | 27 +++++++-- .../powertools/metrics/MetricsFactory.java | 15 ----- .../metrics/internal/EmfMetricsLogger.java | 60 +++++++++---------- .../internal/EmfMetricsLoggerTest.java | 35 ++++++++++- .../metrics/testutils/TestMetrics.java | 6 ++ spotbugs-exclude.xml | 4 ++ 6 files changed, 94 insertions(+), 53 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 d21fe163e..ba7b246c4 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 @@ -16,6 +16,7 @@ import com.amazonaws.services.lambda.runtime.Context; import java.time.Instant; +import java.util.function.Consumer; import software.amazon.lambda.powertools.metrics.model.DimensionSet; import software.amazon.lambda.powertools.metrics.model.MetricResolution; @@ -162,7 +163,15 @@ default void captureColdStartMetric() { } /** - * Flush a single metric with custom dimensions. This creates a separate metrics context + * Flush a separate metrics context that inherits the namespace, dimensions and metadata. This creates a separate metrics context + * that doesn't affect the default metrics context. + * + * @param metricsConsumer the consumer to use to edit the metrics instance (e.g. add metrics, override namespace) before flushing + */ + void flushMetrics(Consumer metricsConsumer); + + /** + * Flush a single metric with custom namespace and dimensions. This creates a separate metrics context * that doesn't affect the default metrics context. * * @param name the name of the metric @@ -171,10 +180,17 @@ default void captureColdStartMetric() { * @param namespace the namespace for the metric * @param dimensions custom dimensions for this metric (optional) */ - void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, DimensionSet dimensions); + default void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, DimensionSet dimensions) { + flushMetrics(metrics -> { + metrics.setNamespace(namespace); + metrics.setDefaultDimensions(dimensions); + metrics.addMetric(name, value, unit); + }); + + } /** - * Flush a single metric with custom dimensions. This creates a separate metrics context + * Flush a single metric with custom namespace. This creates a separate metrics context * that doesn't affect the default metrics context. * * @param name the name of the metric @@ -183,6 +199,9 @@ default void captureColdStartMetric() { * @param namespace the namespace for the metric */ default void flushSingleMetric(String name, double value, MetricUnit unit, String namespace) { - flushSingleMetric(name, value, unit, namespace, null); + flushMetrics(metrics -> { + metrics.setNamespace(namespace); + metrics.addMetric(name, value, unit); + }); } } diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java index 67ab17b7b..a0f5405c9 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java @@ -17,9 +17,6 @@ import org.crac.Core; import org.crac.Resource; import software.amazon.lambda.powertools.common.internal.ClassPreLoader; -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; @@ -46,18 +43,6 @@ public final class MetricsFactory implements Resource { public static synchronized Metrics getMetricsInstance() { if (metrics == null) { metrics = provider.getMetricsInstance(); - - // Apply default configuration from environment variables - String envNamespace = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); - if (envNamespace != null) { - metrics.setNamespace(envNamespace); - } - - // Only set Service dimension if it's not the default undefined value - String serviceName = LambdaHandlerProcessor.serviceName(); - if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) { - metrics.setDefaultDimensions(DimensionSet.of("Service", serviceName)); - } } return metrics; 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 c9651585f..1993f7318 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,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +34,8 @@ 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.common.internal.LambdaConstants; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.metrics.model.MetricResolution; import software.amazon.lambda.powertools.metrics.model.MetricUnit; @@ -50,15 +53,30 @@ public class EmfMetricsLogger implements Metrics { private final software.amazon.cloudwatchlogs.emf.logger.MetricsLogger emfLogger; private final EnvironmentProvider environmentProvider; - private AtomicBoolean raiseOnEmptyMetrics = new AtomicBoolean(false); + private final MetricsContext metricsContext; + private final AtomicBoolean raiseOnEmptyMetrics = new AtomicBoolean(false); private String namespace; private Map defaultDimensions = new HashMap<>(); + private final Map metadata = 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; + this.metricsContext = metricsContext; + + // Apply default configuration from environment variables + String envNamespace = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); + if (envNamespace != null) { + setNamespace(envNamespace); + } + + // Only set Service dimension if it's not the default undefined value + String serviceName = LambdaHandlerProcessor.serviceName(); + if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) { + setDefaultDimensions(software.amazon.lambda.powertools.metrics.model.DimensionSet.of("Service", serviceName)); + } } @Override @@ -92,6 +110,7 @@ public void addDimension(software.amazon.lambda.powertools.metrics.model.Dimensi @Override public void addMetadata(String key, Object value) { emfLogger.putMetadata(key, value); + metadata.put(key, value); } @Override @@ -221,43 +240,22 @@ 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) { + public void flushMetrics(Consumer metricsConsumer) { if (isMetricsDisabled()) { LOGGER.debug("Metrics are disabled, skipping single metric flush"); return; } - - Validator.validateNamespace(namespace); - - // Create a new logger for this single metric - software.amazon.cloudwatchlogs.emf.logger.MetricsLogger singleMetricLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger( - environmentProvider); - - try { - singleMetricLogger.setNamespace(namespace); - } catch (Exception e) { - LOGGER.error("Namespace cannot be set for single metric due to an error in EMF", e); + // Create a new instance, inheriting namespace/dimensions state + EmfMetricsLogger metrics = new EmfMetricsLogger(environmentProvider, metricsContext); + if (namespace != null) { + metrics.setNamespace(this.namespace); } + defaultDimensions.forEach(metrics::addDimension); + metadata.forEach(metrics::addMetadata); - // 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); - } + metricsConsumer.accept(metrics); - // Flush the metric - singleMetricLogger.flush(); + metrics.flush(); } private boolean isMetricsDisabled() { 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 6c324221c..f38e0e200 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 @@ -246,7 +246,7 @@ void shouldAddDimensionSet() throws Exception { @Test void shouldThrowExceptionWhenDimensionSetIsNull() { // When/Then - assertThatThrownBy(() -> metrics.addDimension((DimensionSet) null)) + assertThatThrownBy(() -> metrics.addDimension(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("DimensionSet cannot be null"); } @@ -304,7 +304,7 @@ void shouldGetDefaultDimensions() { @Test void shouldThrowExceptionWhenDefaultDimensionSetIsNull() { // When/Then - assertThatThrownBy(() -> metrics.setDefaultDimensions((DimensionSet) null)) + assertThatThrownBy(() -> metrics.setDefaultDimensions(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("DimensionSet cannot be null"); } @@ -346,7 +346,7 @@ void shouldLogWarningOnEmptyMetrics() throws Exception { // Then // Read the log file and check for the warning - String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8); + String logContent = Files.readString(logFile.toPath(), StandardCharsets.UTF_8); assertThat(logContent).contains("No metrics were emitted"); // No EMF output should be generated assertThat(outputStreamCaptor.toString().trim()).isEmpty(); @@ -446,6 +446,35 @@ void shouldReuseNamespaceForColdStartMetric() throws Exception { .isEqualTo(customNamespace); } + @Test + void shouldFlushMetrics() throws Exception { + // Given + metrics.setNamespace("MainNamespace"); + metrics.setDefaultDimensions(DimensionSet.of("CustomDim", "CustomValue")); + metrics.addMetadata("CustomMetadata", "MetadataValue"); + + // When + metrics.flushMetrics(m -> { + m.addMetric("metric-one", 200, MetricUnit.COUNT); + m.addMetric("metric-two", 100, MetricUnit.COUNT); + }); + + // Then + String emfOutput = outputStreamCaptor.toString().trim(); + JsonNode rootNode = objectMapper.readTree(emfOutput); + + assertThat(rootNode.has("metric-one")).isTrue(); + assertThat(rootNode.get("metric-one").asDouble()).isEqualTo(200.0); + assertThat(rootNode.has("metric-two")).isTrue(); + assertThat(rootNode.get("metric-two").asDouble()).isEqualTo(100); + assertThat(rootNode.has("CustomDim")).isTrue(); + assertThat(rootNode.get("CustomDim").asText()).isEqualTo("CustomValue"); + assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) + .isEqualTo("MainNamespace"); + assertThat(rootNode.get("_aws").has("CustomMetadata")).isTrue(); + assertThat(rootNode.get("_aws").get("CustomMetadata").asText()).isEqualTo("MetadataValue"); + } + @Test void shouldFlushSingleMetric() throws Exception { // Given 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 949828a13..4a2e33a78 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 @@ -2,6 +2,7 @@ import java.time.Instant; import java.util.Collections; +import java.util.function.Consumer; import com.amazonaws.services.lambda.runtime.Context; @@ -77,6 +78,11 @@ public void captureColdStartMetric(DimensionSet dimensions) { // Test placeholder } + @Override + public void flushMetrics(Consumer metricsConsumer) { + // Test placeholder + } + @Override public void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, DimensionSet dimensions) { diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 35aed5e26..9075e86b7 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -90,6 +90,10 @@ + + + + From 4730be43a224a86621b69be3d6ce33f9c47a4190 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Tue, 30 Sep 2025 11:06:51 +0100 Subject: [PATCH 3/9] address metrics context issue --- .../powertools/metrics/MetricsFactory.java | 15 +++++++++++++++ .../metrics/internal/EmfMetricsLogger.java | 18 +----------------- spotbugs-exclude.xml | 4 ---- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java index a0f5405c9..67ab17b7b 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/MetricsFactory.java @@ -17,6 +17,9 @@ import org.crac.Core; import org.crac.Resource; import software.amazon.lambda.powertools.common.internal.ClassPreLoader; +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; @@ -43,6 +46,18 @@ public final class MetricsFactory implements Resource { public static synchronized Metrics getMetricsInstance() { if (metrics == null) { metrics = provider.getMetricsInstance(); + + // Apply default configuration from environment variables + String envNamespace = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); + if (envNamespace != null) { + metrics.setNamespace(envNamespace); + } + + // Only set Service dimension if it's not the default undefined value + String serviceName = LambdaHandlerProcessor.serviceName(); + if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) { + metrics.setDefaultDimensions(DimensionSet.of("Service", serviceName)); + } } return metrics; 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 1993f7318..862b7e20d 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 @@ -34,8 +34,6 @@ 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.common.internal.LambdaConstants; -import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.metrics.model.MetricResolution; import software.amazon.lambda.powertools.metrics.model.MetricUnit; @@ -53,7 +51,6 @@ public class EmfMetricsLogger implements Metrics { private final software.amazon.cloudwatchlogs.emf.logger.MetricsLogger emfLogger; private final EnvironmentProvider environmentProvider; - private final MetricsContext metricsContext; private final AtomicBoolean raiseOnEmptyMetrics = new AtomicBoolean(false); private String namespace; private Map defaultDimensions = new HashMap<>(); @@ -64,19 +61,6 @@ public EmfMetricsLogger(EnvironmentProvider environmentProvider, MetricsContext this.emfLogger = new software.amazon.cloudwatchlogs.emf.logger.MetricsLogger(environmentProvider, metricsContext); this.environmentProvider = environmentProvider; - this.metricsContext = metricsContext; - - // Apply default configuration from environment variables - String envNamespace = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); - if (envNamespace != null) { - setNamespace(envNamespace); - } - - // Only set Service dimension if it's not the default undefined value - String serviceName = LambdaHandlerProcessor.serviceName(); - if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) { - setDefaultDimensions(software.amazon.lambda.powertools.metrics.model.DimensionSet.of("Service", serviceName)); - } } @Override @@ -246,7 +230,7 @@ public void flushMetrics(Consumer metricsConsumer) { return; } // Create a new instance, inheriting namespace/dimensions state - EmfMetricsLogger metrics = new EmfMetricsLogger(environmentProvider, metricsContext); + EmfMetricsLogger metrics = new EmfMetricsLogger(environmentProvider, new MetricsContext()); if (namespace != null) { metrics.setNamespace(this.namespace); } diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 9075e86b7..35aed5e26 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -90,10 +90,6 @@ - - - - From a97906c4c3f7e3f1176205169e379ea178adaa67 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Tue, 30 Sep 2025 13:16:51 +0100 Subject: [PATCH 4/9] fix addDimension wrongly adding to defaultDimensions --- .../powertools/metrics/internal/EmfMetricsLogger.java | 6 +++--- .../powertools/metrics/internal/EmfMetricsLoggerTest.java | 2 ++ 2 files changed, 5 insertions(+), 3 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 862b7e20d..db2205136 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 @@ -81,8 +81,6 @@ public void addDimension(software.amazon.lambda.powertools.metrics.model.Dimensi 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 } @@ -234,7 +232,9 @@ public void flushMetrics(Consumer metricsConsumer) { if (namespace != null) { metrics.setNamespace(this.namespace); } - defaultDimensions.forEach(metrics::addDimension); + if (!defaultDimensions.isEmpty()) { + metrics.setDefaultDimensions(software.amazon.lambda.powertools.metrics.model.DimensionSet.of(defaultDimensions)); + } metadata.forEach(metrics::addMetadata); metricsConsumer.accept(metrics); 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 f38e0e200..3059897c2 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 @@ -451,6 +451,7 @@ void shouldFlushMetrics() throws Exception { // Given metrics.setNamespace("MainNamespace"); metrics.setDefaultDimensions(DimensionSet.of("CustomDim", "CustomValue")); + metrics.addDimension(DimensionSet.of("CustomDim2", "CustomValue2")); metrics.addMetadata("CustomMetadata", "MetadataValue"); // When @@ -469,6 +470,7 @@ void shouldFlushMetrics() throws Exception { assertThat(rootNode.get("metric-two").asDouble()).isEqualTo(100); assertThat(rootNode.has("CustomDim")).isTrue(); assertThat(rootNode.get("CustomDim").asText()).isEqualTo("CustomValue"); + assertThat(rootNode.get("CustomDim2")).isNull(); assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) .isEqualTo("MainNamespace"); assertThat(rootNode.get("_aws").has("CustomMetadata")).isTrue(); From 3738d6277e1883bec7bbb8e1f655ba31c3e1103b Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Tue, 30 Sep 2025 15:36:07 +0100 Subject: [PATCH 5/9] introduce Metrics.addProperty and use it instead of addMetadata --- .../amazon/lambda/powertools/metrics/Metrics.java | 8 ++++++++ .../powertools/metrics/internal/EmfMetricsLogger.java | 11 ++++++++--- .../metrics/internal/LambdaMetricsAspect.java | 4 ++-- .../metrics/internal/EmfMetricsLoggerTest.java | 6 ++++-- .../powertools/metrics/testutils/TestMetrics.java | 5 +++++ 5 files changed, 27 insertions(+), 7 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 ba7b246c4..be942b785 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 @@ -93,6 +93,14 @@ default void addDimension(String key, String value) { */ void addMetadata(String key, Object value); + /** + * Add property + * + * @param key the name of the property + * @param value the value of the property + */ + void addProperty(String key, Object value); + /** * Set default dimensions * 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 db2205136..1c2578ca5 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 @@ -54,7 +54,7 @@ public class EmfMetricsLogger implements Metrics { private final AtomicBoolean raiseOnEmptyMetrics = new AtomicBoolean(false); private String namespace; private Map defaultDimensions = new HashMap<>(); - private final Map metadata = new HashMap<>(); + private final Map properties = new HashMap<>(); private final AtomicBoolean hasMetrics = new AtomicBoolean(false); public EmfMetricsLogger(EnvironmentProvider environmentProvider, MetricsContext metricsContext) { @@ -92,7 +92,12 @@ public void addDimension(software.amazon.lambda.powertools.metrics.model.Dimensi @Override public void addMetadata(String key, Object value) { emfLogger.putMetadata(key, value); - metadata.put(key, value); + } + + @Override + public void addProperty(String key, Object value) { + emfLogger.putProperty(key, value); + properties.put(key, value); } @Override @@ -235,7 +240,7 @@ public void flushMetrics(Consumer metricsConsumer) { if (!defaultDimensions.isEmpty()) { metrics.setDefaultDimensions(software.amazon.lambda.powertools.metrics.model.DimensionSet.of(defaultDimensions)); } - metadata.forEach(metrics::addMetadata); + properties.forEach(metrics::addProperty); metricsConsumer.accept(metrics); 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 32824e24f..5ca80612e 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 @@ -92,7 +92,7 @@ public Object around(ProceedingJoinPoint pjp, // Add trace ID metadata if available LambdaHandlerProcessor.getXrayTraceId() - .ifPresent(traceId -> metricsInstance.addMetadata(TRACE_ID_PROPERTY, traceId)); + .ifPresent(traceId -> metricsInstance.addProperty(TRACE_ID_PROPERTY, traceId)); captureColdStartMetricIfEnabled(extractContext(pjp), metrics); @@ -115,7 +115,7 @@ private void captureColdStartMetricIfEnabled(Context extractedContext, FlushMetr Metrics metricsInstance = MetricsFactory.getMetricsInstance(); // This can be null e.g. during unit tests when mocking the Lambda context if (extractedContext.getAwsRequestId() != null) { - metricsInstance.addMetadata(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); + metricsInstance.addProperty(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); } // Only capture cold start metrics if enabled on annotation 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 3059897c2..8fdad71de 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 @@ -453,6 +453,7 @@ void shouldFlushMetrics() throws Exception { metrics.setDefaultDimensions(DimensionSet.of("CustomDim", "CustomValue")); metrics.addDimension(DimensionSet.of("CustomDim2", "CustomValue2")); metrics.addMetadata("CustomMetadata", "MetadataValue"); + metrics.addProperty("CustomProperty", "PropertyValue"); // When metrics.flushMetrics(m -> { @@ -473,8 +474,9 @@ void shouldFlushMetrics() throws Exception { assertThat(rootNode.get("CustomDim2")).isNull(); assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) .isEqualTo("MainNamespace"); - assertThat(rootNode.get("_aws").has("CustomMetadata")).isTrue(); - assertThat(rootNode.get("_aws").get("CustomMetadata").asText()).isEqualTo("MetadataValue"); + assertThat(rootNode.get("_aws").has("CustomMetadata")).isFalse(); + assertThat(rootNode.has("CustomProperty")).isTrue(); + assertThat(rootNode.get("CustomProperty").asText()).isEqualTo("PropertyValue"); } @Test 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 4a2e33a78..b96e3ce38 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 @@ -37,6 +37,11 @@ public void addMetadata(String key, Object value) { // Test placeholder } + @Override + public void addProperty(String key, Object value) { + // Test placeholder + } + @Override public void setDefaultDimensions(DimensionSet dimensionSet) { // Test placeholder From f1f3e7042c7fa5d9a80c6f792f26747c28019855 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Tue, 30 Sep 2025 16:09:49 +0100 Subject: [PATCH 6/9] use flushMetrics in captureColdStartMetric --- .../metrics/internal/EmfMetricsLogger.java | 49 ++++--------------- .../metrics/internal/LambdaMetricsAspect.java | 22 ++++----- 2 files changed, 21 insertions(+), 50 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 1c2578ca5..33583d1e9 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 @@ -179,45 +179,16 @@ 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(); - - try { - coldStartLogger.setNamespace(namespace); - } catch (Exception e) { - LOGGER.error("Namespace cannot be set for cold start metrics due to an error in EMF", e); - } - - coldStartLogger.putMetric(COLD_START_METRIC, 1, Unit.COUNT); - - // Set dimensions if provided - if (dimensions != null) { - DimensionSet emfDimensionSet = new DimensionSet(); - dimensions.getDimensions().forEach((key, val) -> { - try { - emfDimensionSet.addDimension(key, val); - } catch (Exception e) { - // Ignore dimension errors - } - }); - coldStartLogger.setDimensions(emfDimensionSet); - } - - // Add request ID from context if available - if (context != null && context.getAwsRequestId() != 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(); + flushMetrics(metrics -> { + if (context != null && context.getAwsRequestId() != null) { + metrics.addProperty(REQUEST_ID_PROPERTY, context.getAwsRequestId()); + } + getXrayTraceId().ifPresent(traceId -> metrics.addProperty(TRACE_ID_PROPERTY, traceId)); + if (dimensions != null) { + metrics.setDefaultDimensions(dimensions); + } + metrics.addMetric(COLD_START_METRIC, 1, MetricUnit.COUNT); + }); } } 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 5ca80612e..3ecfef80b 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 @@ -107,40 +107,40 @@ public Object around(ProceedingJoinPoint pjp, return pjp.proceed(proceedArgs); } - private void captureColdStartMetricIfEnabled(Context extractedContext, FlushMetrics metrics) { + private void captureColdStartMetricIfEnabled(Context extractedContext, FlushMetrics flushMetrics) { if (extractedContext == null) { return; } - Metrics metricsInstance = MetricsFactory.getMetricsInstance(); + Metrics metrics = MetricsFactory.getMetricsInstance(); // This can be null e.g. during unit tests when mocking the Lambda context if (extractedContext.getAwsRequestId() != null) { - metricsInstance.addProperty(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); + metrics.addProperty(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); } // Only capture cold start metrics if enabled on annotation - if (metrics.captureColdStart()) { + if (flushMetrics.captureColdStart()) { // Get function name from annotation or context - String funcName = functionName(metrics, extractedContext); + String funcName = functionName(flushMetrics, extractedContext); - DimensionSet coldStartDimensions = new DimensionSet(); + DimensionSet dimensionSet = new DimensionSet(); // Get service name from metrics instance default dimensions or fallback - String serviceName = metricsInstance.getDefaultDimensions().getDimensions().getOrDefault( + String serviceName = metrics.getDefaultDimensions().getDimensions().getOrDefault( SERVICE_DIMENSION, - serviceNameWithFallback(metrics)); + serviceNameWithFallback(flushMetrics)); // Only add service if it is not undefined if (!LambdaConstants.SERVICE_UNDEFINED.equals(serviceName)) { - coldStartDimensions.addDimension(SERVICE_DIMENSION, serviceName); + dimensionSet.addDimension(SERVICE_DIMENSION, serviceName); } // Add function name if (funcName != null) { - coldStartDimensions.addDimension("FunctionName", funcName); + dimensionSet.addDimension("FunctionName", funcName); } - metricsInstance.captureColdStartMetric(extractedContext, coldStartDimensions); + metrics.captureColdStartMetric(extractedContext, dimensionSet); } } } From 13eff8f50b2e9732b30919348909e57344d20f46 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Tue, 30 Sep 2025 23:54:11 +0100 Subject: [PATCH 7/9] use putProperty for addMetadata and consolidate duplicate logic --- .../lambda/powertools/metrics/Metrics.java | 8 -------- .../metrics/internal/EmfMetricsLogger.java | 15 ++------------- .../metrics/internal/LambdaMetricsAspect.java | 13 +++---------- .../metrics/internal/MetricsUtils.java | 18 ++++++++++++++++++ .../metrics/internal/EmfMetricsLoggerTest.java | 10 ++++------ .../metrics/testutils/TestMetrics.java | 5 ----- 6 files changed, 27 insertions(+), 42 deletions(-) create mode 100644 powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/MetricsUtils.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 be942b785..ba7b246c4 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 @@ -93,14 +93,6 @@ default void addDimension(String key, String value) { */ void addMetadata(String key, Object value); - /** - * Add property - * - * @param key the name of the property - * @param value the value of the property - */ - void addProperty(String key, Object value); - /** * Set default dimensions * 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 33583d1e9..2f2505af4 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,6 @@ package software.amazon.lambda.powertools.metrics.internal; -import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.getXrayTraceId; import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.isColdStart; import java.time.Instant; @@ -44,8 +43,6 @@ */ public class EmfMetricsLogger implements Metrics { private static final Logger LOGGER = LoggerFactory.getLogger(EmfMetricsLogger.class); - private static final String TRACE_ID_PROPERTY = "xray_trace_id"; - private static final String REQUEST_ID_PROPERTY = "function_request_id"; private static final String COLD_START_METRIC = "ColdStart"; private static final String METRICS_DISABLED_ENV_VAR = "POWERTOOLS_METRICS_DISABLED"; @@ -91,11 +88,6 @@ public void addDimension(software.amazon.lambda.powertools.metrics.model.Dimensi @Override public void addMetadata(String key, Object value) { - emfLogger.putMetadata(key, value); - } - - @Override - public void addProperty(String key, Object value) { emfLogger.putProperty(key, value); properties.put(key, value); } @@ -180,10 +172,7 @@ public void captureColdStartMetric(Context context, software.amazon.lambda.powertools.metrics.model.DimensionSet dimensions) { if (isColdStart()) { flushMetrics(metrics -> { - if (context != null && context.getAwsRequestId() != null) { - metrics.addProperty(REQUEST_ID_PROPERTY, context.getAwsRequestId()); - } - getXrayTraceId().ifPresent(traceId -> metrics.addProperty(TRACE_ID_PROPERTY, traceId)); + MetricsUtils.addRequestIdAndXrayTraceIdIfAvailable(context, metrics); if (dimensions != null) { metrics.setDefaultDimensions(dimensions); } @@ -211,7 +200,7 @@ public void flushMetrics(Consumer metricsConsumer) { if (!defaultDimensions.isEmpty()) { metrics.setDefaultDimensions(software.amazon.lambda.powertools.metrics.model.DimensionSet.of(defaultDimensions)); } - properties.forEach(metrics::addProperty); + properties.forEach(metrics::addMetadata); metricsConsumer.accept(metrics); 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 3ecfef80b..1f0e3ec8c 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 @@ -34,8 +34,6 @@ @Aspect public class LambdaMetricsAspect { - public static final String TRACE_ID_PROPERTY = "xray_trace_id"; - public static final String REQUEST_ID_PROPERTY = "function_request_id"; private static final String SERVICE_DIMENSION = "Service"; private static final String FUNCTION_NAME_ENV_VAR = "POWERTOOLS_METRICS_FUNCTION_NAME"; @@ -90,11 +88,10 @@ public Object around(ProceedingJoinPoint pjp, metricsInstance.setRaiseOnEmptyMetrics(metrics.raiseOnEmptyMetrics()); - // Add trace ID metadata if available - LambdaHandlerProcessor.getXrayTraceId() - .ifPresent(traceId -> metricsInstance.addProperty(TRACE_ID_PROPERTY, traceId)); + Context extractedContext = extractContext(pjp); + MetricsUtils.addRequestIdAndXrayTraceIdIfAvailable(extractedContext, metricsInstance); - captureColdStartMetricIfEnabled(extractContext(pjp), metrics); + captureColdStartMetricIfEnabled(extractedContext, metrics); try { return pjp.proceed(proceedArgs); @@ -113,10 +110,6 @@ private void captureColdStartMetricIfEnabled(Context extractedContext, FlushMetr } Metrics metrics = MetricsFactory.getMetricsInstance(); - // This can be null e.g. during unit tests when mocking the Lambda context - if (extractedContext.getAwsRequestId() != null) { - metrics.addProperty(REQUEST_ID_PROPERTY, extractedContext.getAwsRequestId()); - } // Only capture cold start metrics if enabled on annotation if (flushMetrics.captureColdStart()) { diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/MetricsUtils.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/MetricsUtils.java new file mode 100644 index 000000000..c1b22e48f --- /dev/null +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/MetricsUtils.java @@ -0,0 +1,18 @@ +package software.amazon.lambda.powertools.metrics.internal; + +import com.amazonaws.services.lambda.runtime.Context; +import software.amazon.lambda.powertools.metrics.Metrics; + +import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.getXrayTraceId; + +class MetricsUtils { + private static final String TRACE_ID_PROPERTY = "xray_trace_id"; + private static final String REQUEST_ID_PROPERTY = "function_request_id"; + + static void addRequestIdAndXrayTraceIdIfAvailable(Context context, Metrics metrics) { + if (context != null && context.getAwsRequestId() != null) { + metrics.addMetadata(REQUEST_ID_PROPERTY, context.getAwsRequestId()); + } + getXrayTraceId().ifPresent(traceId -> metrics.addMetadata(TRACE_ID_PROPERTY, traceId)); + } +} 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 8fdad71de..9f793f977 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 @@ -263,8 +263,8 @@ void shouldAddMetadata() throws Exception { 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"); + assertThat(rootNode.has("CustomMetadata")).isTrue(); + assertThat(rootNode.get("CustomMetadata").asText()).isEqualTo("MetadataValue"); } @Test @@ -453,7 +453,6 @@ void shouldFlushMetrics() throws Exception { metrics.setDefaultDimensions(DimensionSet.of("CustomDim", "CustomValue")); metrics.addDimension(DimensionSet.of("CustomDim2", "CustomValue2")); metrics.addMetadata("CustomMetadata", "MetadataValue"); - metrics.addProperty("CustomProperty", "PropertyValue"); // When metrics.flushMetrics(m -> { @@ -474,9 +473,8 @@ void shouldFlushMetrics() throws Exception { assertThat(rootNode.get("CustomDim2")).isNull(); assertThat(rootNode.get("_aws").get("CloudWatchMetrics").get(0).get("Namespace").asText()) .isEqualTo("MainNamespace"); - assertThat(rootNode.get("_aws").has("CustomMetadata")).isFalse(); - assertThat(rootNode.has("CustomProperty")).isTrue(); - assertThat(rootNode.get("CustomProperty").asText()).isEqualTo("PropertyValue"); + assertThat(rootNode.has("CustomMetadata")).isTrue(); + assertThat(rootNode.get("CustomMetadata").asText()).isEqualTo("MetadataValue"); } @Test 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 b96e3ce38..4a2e33a78 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 @@ -37,11 +37,6 @@ public void addMetadata(String key, Object value) { // Test placeholder } - @Override - public void addProperty(String key, Object value) { - // Test placeholder - } - @Override public void setDefaultDimensions(DimensionSet dimensionSet) { // Test placeholder From 84a194fa71136eb7e6a15950bad53361006e2ab9 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Wed, 1 Oct 2025 11:21:37 +0100 Subject: [PATCH 8/9] update docs and javadoc --- docs/core/metrics.md | 28 ++++++++++--------- .../lambda/powertools/metrics/Metrics.java | 4 +-- .../metrics/internal/EmfMetricsLogger.java | 2 +- .../metrics/internal/MetricsUtils.java | 6 +++- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 61d4c38f0..65f0cc72d 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -462,9 +462,9 @@ If you wish to set custom default dimensions, it can be done via `#!java metrics 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. -### Creating a single metric with different configuration +### Creating metrics with different configuration -You can create a single metric with its own namespace and dimensions using `flushSingleMetric`: +You can create metrics with different configurations e.g. different namespace and/or dimensions using `flushMetrics()`: === "App.java" @@ -480,13 +480,15 @@ You can create a single metric with its own namespace and dimensions using `flus @Override @FlushMetrics(namespace = "ServerlessAirline", service = "payment") public Object handleRequest(Object input, Context context) { - metrics.flushSingleMetric( - "CustomMetric", - 1, - MetricUnit.COUNT, - "CustomNamespace", - DimensionSet.of("CustomDimension", "value") // Dimensions are optional - ); + metrics.flushMetrics((customMetrics) -> { + customMetrics.addMetric("CustomMetric", 1, MetricUnit.COUNT); + // To optionally set a different namespace + customMetrics.setNamespace("CustomNamespace"); + // To optionally set different dimensions + customMetrics.setDefaultDimensions(DimensionSet.of("CustomDimension", "value")); + // To optionally add metadata + customMetrics.addMetadata("CustomMetadata", "value")); + }); } } ``` @@ -516,7 +518,7 @@ The following example shows how to configure a custom `Metrics` Singleton using public class App implements RequestHandler { // Create and configure a Metrics singleton without annotation - private static final Metrics customMetrics = MetricsBuilder.builder() + private static final Metrics metrics = MetricsBuilder.builder() .withNamespace("ServerlessAirline") .withRaiseOnEmptyMetrics(true) .withService("payment") @@ -527,11 +529,11 @@ The following example shows how to configure a custom `Metrics` Singleton using // You can manually capture the cold start metric // Lambda context is an optional argument if not available in your environment // Dimensions are also optional. - customMetrics.captureColdStartMetric(context, DimensionSet.of("FunctionName", "MyFunction", "Service", "payment")); + metrics.captureColdStartMetric(context, DimensionSet.of("FunctionName", "MyFunction", "Service", "payment")); // Add metrics to the custom metrics singleton - customMetrics.addMetric("CustomMetric", 1, MetricUnit.COUNT); - customMetrics.flush(); + metrics.addMetric("CustomMetric", 1, MetricUnit.COUNT); + metrics.flush(); } } ``` 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 ba7b246c4..77db2aba0 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 @@ -163,10 +163,10 @@ default void captureColdStartMetric() { } /** - * Flush a separate metrics context that inherits the namespace, dimensions and metadata. This creates a separate metrics context + * Flush a separate metrics context that inherits the namespace, default dimensions, and metadata. This creates a separate metrics context * that doesn't affect the default metrics context. * - * @param metricsConsumer the consumer to use to edit the metrics instance (e.g. add metrics, override namespace) before flushing + * @param metricsConsumer the consumer to use to edit the metrics instance (e.g. add metrics, override namespace, set or add custom dimensions) before flushing */ void flushMetrics(Consumer metricsConsumer); 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 2f2505af4..37f2d193a 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 @@ -192,7 +192,7 @@ public void flushMetrics(Consumer metricsConsumer) { LOGGER.debug("Metrics are disabled, skipping single metric flush"); return; } - // Create a new instance, inheriting namespace/dimensions state + // Create a new instance, inheriting namespace, default dimensions, and metadata EmfMetricsLogger metrics = new EmfMetricsLogger(environmentProvider, new MetricsContext()); if (namespace != null) { metrics.setNamespace(this.namespace); diff --git a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/MetricsUtils.java b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/MetricsUtils.java index c1b22e48f..246f6effc 100644 --- a/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/MetricsUtils.java +++ b/powertools-metrics/src/main/java/software/amazon/lambda/powertools/metrics/internal/MetricsUtils.java @@ -5,10 +5,14 @@ import static software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor.getXrayTraceId; -class MetricsUtils { +final class MetricsUtils { private static final String TRACE_ID_PROPERTY = "xray_trace_id"; private static final String REQUEST_ID_PROPERTY = "function_request_id"; + private MetricsUtils() { + // Utility class + } + static void addRequestIdAndXrayTraceIdIfAvailable(Context context, Metrics metrics) { if (context != null && context.getAwsRequestId() != null) { metrics.addMetadata(REQUEST_ID_PROPERTY, context.getAwsRequestId()); From c99a69a2e7fd99f6a4b55786956c2ce4a0af78b7 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Wed, 1 Oct 2025 12:57:27 +0100 Subject: [PATCH 9/9] Update docs/core/metrics.md Co-authored-by: Philipp Page --- docs/core/metrics.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 65f0cc72d..71c56bb8b 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -484,8 +484,10 @@ You can create metrics with different configurations e.g. different namespace an customMetrics.addMetric("CustomMetric", 1, MetricUnit.COUNT); // To optionally set a different namespace customMetrics.setNamespace("CustomNamespace"); - // To optionally set different dimensions - customMetrics.setDefaultDimensions(DimensionSet.of("CustomDimension", "value")); + // To optionally set different default dimensions + customMetrics.setDefaultDimensions(DimensionSet.of("CustomDefaultDimension", "value")); + // To optionally append additional dimensions to default dimensions + customMetrics.addDimension(DimensionSet.of("CustomDimension", "value")); // To optionally add metadata customMetrics.addMetadata("CustomMetadata", "value")); });