From c98f9ab48eb043330cbb535f89997204190d2ad5 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Fri, 13 Feb 2026 18:59:58 +0000 Subject: [PATCH 1/6] Add propertiesSupplier support to EmfMetricLoggingPublisher Enable `EmfMetricLoggingPublisher` to accept an optional `Supplier>` for enriching EMF records with custom properties at publish time - Add `propertiesSupplier` field and builder method to `EmfMetricLoggingPublisher.Builder` - Add `propertiesSupplier` field, accessor, and builder setter to `EmfMetricConfiguration`, defaulting to empty map when null - Add `resolveProperties()` to `MetricEmfConverter` which invokes the supplier from config once per convert call, handling null returns and exceptions gracefully - Add `writeCustomProperties()` to `MetricEmfConverter` which writes properties first in the EMF JSON so `_aws`, dimensions, and metrics overwrite any key collisions Closes #6595 --- ...nCloudWatchEMFMetricPublisher-33792c9.json | 6 + .../emf/EmfMetricLoggingPublisher.java | 25 ++++ .../emf/internal/EmfMetricConfiguration.java | 16 ++ .../emf/internal/MetricEmfConverter.java | 38 ++++- .../emf/EmfMetricLoggingPublisherTest.java | 98 ++++++++++++ .../emf/internal/MetricEmfConverterTest.java | 140 +++++++++++++++++- 6 files changed, 317 insertions(+), 6 deletions(-) create mode 100644 .changes/next-release/feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json diff --git a/.changes/next-release/feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json b/.changes/next-release/feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json new file mode 100644 index 000000000000..d6aa0fee0b52 --- /dev/null +++ b/.changes/next-release/feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Amazon CloudWatch EMF Metric Publisher", + "contributor": "humanzz", + "description": "Add `propertiesSupplier` to `EmfMetricLoggingPublisher.Builder`, enabling users to enrich EMF records with custom key-value properties that are searchable in CloudWatch Logs Insights. See [#6595](https://github.com/aws/aws-sdk-java-v2/issues/6595)." +} diff --git a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java index 07bba8a97442..4bd12cd68135 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java @@ -20,6 +20,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.function.Supplier; import software.amazon.awssdk.annotations.Immutable; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; @@ -82,6 +84,7 @@ private EmfMetricLoggingPublisher(Builder builder) { .dimensions(builder.dimensions) .metricLevel(builder.metricLevel) .metricCategories(builder.metricCategories) + .propertiesSupplier(builder.propertiesSupplier) .build(); this.metricConverter = new MetricEmfConverter(config); @@ -123,6 +126,7 @@ public static final class Builder { private Collection> dimensions; private Collection metricCategories; private MetricLevel metricLevel; + private Supplier> propertiesSupplier; private Builder() { } @@ -217,6 +221,27 @@ public Builder metricLevel(MetricLevel metricLevel) { } + /** + * Configure a supplier of custom properties to include in each EMF record. + * The supplier is invoked on each {@link #publish(MetricCollection)} call, + * and the returned map entries are written as top-level key-value pairs + * in the EMF JSON output. These appear as searchable fields in + * CloudWatch Logs Insights. + * + *

Keys that collide with reserved EMF fields ({@code _aws}), configured + * dimension names, or reported metric names are silently skipped. + * + *

If this is not specified, no custom properties are added. + * + * @param propertiesSupplier a supplier returning a map of property names to values, + * or {@code null} to disable custom properties + * @return this builder + */ + public Builder propertiesSupplier(Supplier> propertiesSupplier) { + this.propertiesSupplier = propertiesSupplier; + return this; + } + /** * Build a {@link EmfMetricLoggingPublisher} using the configuration currently configured on this publisher. */ diff --git a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java index 739cf0b0e4ca..a28417d53763 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java @@ -18,7 +18,9 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -43,6 +45,7 @@ public final class EmfMetricConfiguration { private final Set> dimensions; private final Collection metricCategories; private final MetricLevel metricLevel; + private final Supplier> propertiesSupplier; private EmfMetricConfiguration(Builder builder) { this.namespace = builder.namespace == null ? DEFAULT_NAMESPACE : builder.namespace; @@ -50,6 +53,9 @@ private EmfMetricConfiguration(Builder builder) { this.dimensions = builder.dimensions == null ? DEFAULT_DIMENSIONS : new HashSet<>(builder.dimensions); this.metricCategories = builder.metricCategories == null ? DEFAULT_CATEGORIES : new HashSet<>(builder.metricCategories); this.metricLevel = builder.metricLevel == null ? DEFAULT_METRIC_LEVEL : builder.metricLevel; + this.propertiesSupplier = builder.propertiesSupplier == null + ? Collections::emptyMap + : builder.propertiesSupplier; } @@ -59,6 +65,7 @@ public static class Builder { private Collection> dimensions; private Collection metricCategories; private MetricLevel metricLevel; + private Supplier> propertiesSupplier; public Builder namespace(String namespace) { this.namespace = namespace; @@ -85,6 +92,11 @@ public Builder metricLevel(MetricLevel metricLevel) { return this; } + public Builder propertiesSupplier(Supplier> propertiesSupplier) { + this.propertiesSupplier = propertiesSupplier; + return this; + } + public EmfMetricConfiguration build() { return new EmfMetricConfiguration(this); } @@ -110,6 +122,10 @@ public MetricLevel metricLevel() { return metricLevel; } + public Supplier> propertiesSupplier() { + return propertiesSupplier; + } + private String resolveLogGroupName(Builder builder) { return builder.logGroupName != null ? builder.logGroupName : SystemSettingUtils.resolveEnvironmentVariable("AWS_LAMBDA_LOG_GROUP_NAME").orElse(null); diff --git a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java index 3ab16d3b0878..34515c27f1bb 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java @@ -19,12 +19,14 @@ import java.time.Clock; import java.time.Duration; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; +import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.metrics.MetricCategory; @@ -66,12 +68,14 @@ public class MetricEmfConverter { private final EmfMetricConfiguration config; private final boolean metricCategoriesContainsAll; private final Clock clock; + private final Supplier> propertiesSupplier; @SdkTestInternalApi public MetricEmfConverter(EmfMetricConfiguration config, Clock clock) { this.config = config; this.clock = clock; this.metricCategoriesContainsAll = config.metricCategories().contains(MetricCategory.ALL); + this.propertiesSupplier = config.propertiesSupplier(); } public MetricEmfConverter(EmfMetricConfiguration config) { @@ -136,7 +140,18 @@ public List convertMetricCollectionToEmf(MetricCollection metricCollecti } } - return createEmfStrings(aggregatedMetrics); + Map properties = resolveProperties(); + return createEmfStrings(aggregatedMetrics, properties); + } + + private Map resolveProperties() { + try { + Map result = propertiesSupplier.get(); + return result == null ? Collections.emptyMap() : result; + } catch (Exception e) { + logger.warn(() -> "Properties supplier threw an exception, publishing without custom properties", e); + return Collections.emptyMap(); + } } /** @@ -188,7 +203,8 @@ private void processAndWriteValue(JsonWriter jsonWriter, MetricRecord mRecord } } - private List createEmfStrings(Map, List>> aggregatedMetrics) { + private List createEmfStrings(Map, List>> aggregatedMetrics, + Map properties) { List emfStrings = new ArrayList<>(); Map, List>> currentMetricBatch = new HashMap<>(); @@ -204,24 +220,26 @@ private List createEmfStrings(Map, List>> a } if (currentMetricBatch.size() == MAX_METRIC_NUM) { - emfStrings.add(createEmfString(currentMetricBatch)); + emfStrings.add(createEmfString(currentMetricBatch, properties)); currentMetricBatch = new HashMap<>(); } currentMetricBatch.put(metric, records); } - emfStrings.add(createEmfString(currentMetricBatch)); + emfStrings.add(createEmfString(currentMetricBatch, properties)); return emfStrings; } - private String createEmfString(Map, List>> metrics) { + private String createEmfString(Map, List>> metrics, + Map properties) { JsonWriter jsonWriter = JsonWriter.create(); jsonWriter.writeStartObject(); + writeCustomProperties(jsonWriter, properties); writeAwsObject(jsonWriter, metrics.keySet()); writeMetricValues(jsonWriter, metrics); @@ -231,6 +249,16 @@ private String createEmfString(Map, List>> metrics) } + private void writeCustomProperties(JsonWriter jsonWriter, Map properties) { + for (Map.Entry entry : properties.entrySet()) { + jsonWriter.writeFieldName(entry.getKey()); + jsonWriter.writeValue(entry.getValue()); + } + } + + + + private void writeAwsObject(JsonWriter jsonWriter, Set> metricNames) { jsonWriter.writeFieldName("_aws"); jsonWriter.writeStartObject(); diff --git a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java index f3da12abfb4c..fb713e1d406b 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java +++ b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java @@ -18,6 +18,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.logging.log4j.Level; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import software.amazon.awssdk.http.HttpMetric; @@ -97,4 +103,96 @@ void Publish_multipleMetrics() { assertThat(loggedEvents()).hasSize(2); } + @Test + void publish_propertiesSupplierThrowsException_publishesWithoutCustomProperties() { + EmfMetricLoggingPublisher publisher = publisherBuilder + .logGroupName("/aws/lambda/emfMetricTest") + .propertiesSupplier(() -> { throw new RuntimeException("supplier failed"); }) + .build(); + + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + publisher.publish(metricCollector.collect()); + + // Should have: 1 warning about supplier + 1 EMF info log + boolean hasWarning = loggedEvents().stream() + .anyMatch(e -> e.getLevel() == Level.WARN + && e.getMessage().getFormattedMessage().contains("Properties supplier threw an exception")); + assertThat(hasWarning).isTrue(); + + boolean hasEmfOutput = loggedEvents().stream() + .anyMatch(e -> e.getLevel() == Level.INFO + && e.getMessage().getFormattedMessage().contains("\"_aws\":{")); + assertThat(hasEmfOutput).isTrue(); + + // EMF output should not contain any custom properties + String emfLog = loggedEvents().stream() + .filter(e -> e.getLevel() == Level.INFO + && e.getMessage().getFormattedMessage().contains("\"_aws\":{")) + .findFirst().get().getMessage().getFormattedMessage(); + assertThat(emfLog).contains("\"AvailableConcurrency\":5"); + } + + @Test + void publish_propertiesSupplierReturnsNull_publishesWithoutCustomProperties() { + EmfMetricLoggingPublisher publisher = publisherBuilder + .logGroupName("/aws/lambda/emfMetricTest") + .propertiesSupplier(() -> null) + .build(); + + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + publisher.publish(metricCollector.collect()); + + // Should have EMF output without custom properties + boolean hasEmfOutput = loggedEvents().stream() + .anyMatch(e -> e.getLevel() == Level.INFO + && e.getMessage().getFormattedMessage().contains("\"_aws\":{")); + assertThat(hasEmfOutput).isTrue(); + + String emfLog = loggedEvents().stream() + .filter(e -> e.getLevel() == Level.INFO + && e.getMessage().getFormattedMessage().contains("\"_aws\":{")) + .findFirst().get().getMessage().getFormattedMessage(); + assertThat(emfLog).contains("\"AvailableConcurrency\":5"); + // No warning should be logged for null return + boolean hasWarning = loggedEvents().stream() + .anyMatch(e -> e.getLevel() == Level.WARN); + assertThat(hasWarning).isFalse(); + } + + @Test + void publish_statefulSupplier_eachPublishUsesCurrentMap() { + AtomicInteger counter = new AtomicInteger(0); + EmfMetricLoggingPublisher publisher = publisherBuilder + .logGroupName("/aws/lambda/emfMetricTest") + .propertiesSupplier(() -> { + int count = counter.incrementAndGet(); + Map map = new HashMap(); + map.put("InvocationCount", String.valueOf(count)); + return map; + }) + .build(); + + // First publish + MetricCollector mc1 = MetricCollector.create("test1"); + mc1.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + publisher.publish(mc1.collect()); + + // Second publish + MetricCollector mc2 = MetricCollector.create("test2"); + mc2.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 10); + publisher.publish(mc2.collect()); + + // Collect all EMF info logs + List emfLogs = loggedEvents().stream() + .filter(e -> e.getLevel() == Level.INFO + && e.getMessage().getFormattedMessage().contains("\"_aws\":{")) + .map(e -> e.getMessage().getFormattedMessage()) + .collect(java.util.stream.Collectors.toList()); + + assertThat(emfLogs).hasSize(2); + assertThat(emfLogs.get(0)).contains("\"InvocationCount\":\"1\""); + assertThat(emfLogs.get(1)).contains("\"InvocationCount\":\"2\""); + } } diff --git a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java index a01af0f96320..2c83317752b0 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java +++ b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java @@ -26,7 +26,9 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -250,4 +252,140 @@ void ConvertMetricCollectionToEMF_longValueShouldSucceed() { + "\"CloudWatchMetrics\":[{\"Namespace\":\"AwsSdk/JavaSdk2\",\"Dimensions\":[[]]," + "\"Metrics\":[{\"Name\":\"TestMetric\"}]}]},\"TestMetric\":42}"); } -} \ No newline at end of file + + @Test + void convertMetricCollectionToEmf_withCustomProperties_propertiesAppearInOutput() { + Map properties = new HashMap(); + properties.put("RequestId", "abc-123"); + properties.put("FunctionName", "myLambda"); + + MetricEmfConverter converter = converterWithProperties(properties); + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + List emfLogs = converter.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).hasSize(1); + String emfLog = emfLogs.get(0); + assertThat(emfLog).contains("\"RequestId\":\"abc-123\""); + assertThat(emfLog).contains("\"FunctionName\":\"myLambda\""); + assertThat(emfLog).contains("\"AvailableConcurrency\":5"); + assertThat(emfLog).contains("\"_aws\":{"); + } + + @Test + void convertMetricCollectionToEmf_noProperties_identicalToCurrentBehavior() { + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + List emfLogs = metricEmfConverterDefault.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).containsOnly( + "{\"_aws\":{\"Timestamp\":12345678,\"LogGroupName\":\"my_log_group_name\"," + + "\"CloudWatchMetrics\":[{\"Namespace\":\"AwsSdk/JavaSdk2\",\"Dimensions\":[[]]," + + "\"Metrics\":[{\"Name\":\"AvailableConcurrency\"}]}]},\"AvailableConcurrency\":5}"); + } + + @Test + void convertMetricCollectionToEmf_emptyProperties_noExtraKeys() { + MetricEmfConverter converter = converterWithProperties(new HashMap()); + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + List emfLogs = converter.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).hasSize(1); + String emfLog = emfLogs.get(0); + assertThat(emfLog).doesNotContain("\"RequestId\""); + assertThat(emfLog).doesNotContain("\"FunctionName\""); + } + + @Test + void convertMetricCollectionToEmf_propertyKeyMatchesAwsKey_awsObjectPreserved() { + Map properties = new HashMap(); + properties.put("_aws", "should-be-overwritten"); + + MetricEmfConverter converter = converterWithProperties(properties); + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + List emfLogs = converter.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).hasSize(1); + String emfLog = emfLogs.get(0); + assertThat(emfLog).contains("\"_aws\":{\"Timestamp\":"); + assertThat(emfLog).contains("\"CloudWatchMetrics\":"); + } + + @Test + void convertMetricCollectionToEmf_propertyKeyMatchesDimensionName_dimensionPreserved() { + Map properties = new HashMap(); + properties.put("OperationName", "should-be-overwritten"); + + MetricEmfConverter converter = converterWithProperties(properties); + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(CoreMetric.OPERATION_NAME, "GetObject"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + List emfLogs = converter.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).hasSize(1); + String emfLog = emfLogs.get(0); + assertThat(emfLog).contains("\"OperationName\":\"GetObject\""); + int customIndex = emfLog.indexOf("\"OperationName\":\"should-be-overwritten\""); + int realIndex = emfLog.indexOf("\"OperationName\":\"GetObject\""); + assertThat(customIndex).isGreaterThanOrEqualTo(0); + assertThat(realIndex).isGreaterThan(customIndex); + } + + @Test + void convertMetricCollectionToEmf_propertyKeyMatchesMetricName_metricPreserved() { + Map properties = new HashMap(); + properties.put("AvailableConcurrency", "should-be-overwritten"); + + MetricEmfConverter converter = converterWithProperties(properties); + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + List emfLogs = converter.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).hasSize(1); + String emfLog = emfLogs.get(0); + assertThat(emfLog).contains("\"AvailableConcurrency\":5"); + int customIndex = emfLog.indexOf("\"AvailableConcurrency\":\"should-be-overwritten\""); + int realIndex = emfLog.indexOf("\"AvailableConcurrency\":5"); + assertThat(customIndex).isGreaterThanOrEqualTo(0); + assertThat(realIndex).isGreaterThan(customIndex); + } + + @Test + void convertMetricCollectionToEmf_batchedRecords_allContainCustomProperties() { + Map properties = new HashMap(); + properties.put("RequestId", "batch-test-123"); + properties.put("TraceId", "trace-abc"); + + MetricEmfConverter converter = converterWithProperties(properties); + MetricCollector metricCollector = MetricCollector.create("test"); + for (int i = 0; i < 220; i++) { + metricCollector.reportMetric( + SdkMetric.create("cp_batch_metric_" + i, Integer.class, MetricLevel.INFO, MetricCategory.CORE), i); + } + + List emfLogs = converter.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).hasSize(3); + for (String emfLog : emfLogs) { + assertThat(emfLog).contains("\"RequestId\":\"batch-test-123\""); + assertThat(emfLog).contains("\"TraceId\":\"trace-abc\""); + assertThat(emfLog).contains("\"_aws\":{"); + } + } + + private MetricEmfConverter converterWithProperties(Map properties) { + EmfMetricConfiguration config = new EmfMetricConfiguration.Builder() + .logGroupName("my_log_group_name") + .propertiesSupplier(() -> properties) + .build(); + return new MetricEmfConverter(config, fixedClock); + } +} From e8d055c3e475445b91d8e723890e33dfd99f8d4e Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Wed, 11 Mar 2026 20:14:00 +0000 Subject: [PATCH 2/6] Filter out colliding keys when writing custom properties --- .../emf/internal/MetricEmfConverter.java | 24 +++++++++++++++---- .../emf/internal/MetricEmfConverterTest.java | 11 +++------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java index 34515c27f1bb..7ac63606102c 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -63,6 +64,8 @@ public class MetricEmfConverter { */ private static final int MAX_METRIC_NUM = 100; + private static final String AWS_METADATA_KEY = "_aws"; + private static final Logger logger = Logger.loggerFor(MetricEmfConverter.class); private final List dimensions = new ArrayList<>(); private final EmfMetricConfiguration config; @@ -239,9 +242,9 @@ private String createEmfString(Map, List>> metrics, JsonWriter jsonWriter = JsonWriter.create(); jsonWriter.writeStartObject(); - writeCustomProperties(jsonWriter, properties); writeAwsObject(jsonWriter, metrics.keySet()); writeMetricValues(jsonWriter, metrics); + writeCustomProperties(jsonWriter, properties, metrics.keySet()); jsonWriter.writeEndObject(); @@ -249,10 +252,21 @@ private String createEmfString(Map, List>> metrics, } - private void writeCustomProperties(JsonWriter jsonWriter, Map properties) { + private void writeCustomProperties(JsonWriter jsonWriter, Map properties, + Set> metrics) { + if (properties.isEmpty()) { + return; + } + Set reservedKeys = new HashSet<>(); + reservedKeys.add(AWS_METADATA_KEY); + for (SdkMetric metric : metrics) { + reservedKeys.add(metric.name()); + } for (Map.Entry entry : properties.entrySet()) { - jsonWriter.writeFieldName(entry.getKey()); - jsonWriter.writeValue(entry.getValue()); + if (!reservedKeys.contains(entry.getKey())) { + jsonWriter.writeFieldName(entry.getKey()); + jsonWriter.writeValue(entry.getValue()); + } } } @@ -260,7 +274,7 @@ private void writeCustomProperties(JsonWriter jsonWriter, Map pr private void writeAwsObject(JsonWriter jsonWriter, Set> metricNames) { - jsonWriter.writeFieldName("_aws"); + jsonWriter.writeFieldName(AWS_METADATA_KEY); jsonWriter.writeStartObject(); jsonWriter.writeFieldName("Timestamp"); diff --git a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java index 2c83317752b0..cf1383f5681b 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java +++ b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java @@ -313,6 +313,7 @@ void convertMetricCollectionToEmf_propertyKeyMatchesAwsKey_awsObjectPreserved() assertThat(emfLogs).hasSize(1); String emfLog = emfLogs.get(0); + assertThat(emfLog).doesNotContain("should-be-overwritten"); assertThat(emfLog).contains("\"_aws\":{\"Timestamp\":"); assertThat(emfLog).contains("\"CloudWatchMetrics\":"); } @@ -331,11 +332,8 @@ void convertMetricCollectionToEmf_propertyKeyMatchesDimensionName_dimensionPrese assertThat(emfLogs).hasSize(1); String emfLog = emfLogs.get(0); + assertThat(emfLog).doesNotContain("should-be-overwritten"); assertThat(emfLog).contains("\"OperationName\":\"GetObject\""); - int customIndex = emfLog.indexOf("\"OperationName\":\"should-be-overwritten\""); - int realIndex = emfLog.indexOf("\"OperationName\":\"GetObject\""); - assertThat(customIndex).isGreaterThanOrEqualTo(0); - assertThat(realIndex).isGreaterThan(customIndex); } @Test @@ -351,11 +349,8 @@ void convertMetricCollectionToEmf_propertyKeyMatchesMetricName_metricPreserved() assertThat(emfLogs).hasSize(1); String emfLog = emfLogs.get(0); + assertThat(emfLog).doesNotContain("should-be-overwritten"); assertThat(emfLog).contains("\"AvailableConcurrency\":5"); - int customIndex = emfLog.indexOf("\"AvailableConcurrency\":\"should-be-overwritten\""); - int realIndex = emfLog.indexOf("\"AvailableConcurrency\":5"); - assertThat(customIndex).isGreaterThanOrEqualTo(0); - assertThat(realIndex).isGreaterThan(customIndex); } @Test From 64f42ba69dc41bb670f83451a352a5bf38cdda7d Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Wed, 11 Mar 2026 20:24:18 +0000 Subject: [PATCH 3/6] Drop blank lines --- .../metrics/publishers/emf/internal/MetricEmfConverter.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java index 7ac63606102c..e940b1e2185e 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java @@ -270,9 +270,6 @@ private void writeCustomProperties(JsonWriter jsonWriter, Map pr } } - - - private void writeAwsObject(JsonWriter jsonWriter, Set> metricNames) { jsonWriter.writeFieldName(AWS_METADATA_KEY); jsonWriter.writeStartObject(); From cb950363b6927027c0b43336c1738f2285fa0c53 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Wed, 11 Mar 2026 20:27:47 +0000 Subject: [PATCH 4/6] Drop unused import --- .../metrics/publishers/emf/EmfMetricLoggingPublisherTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java index fb713e1d406b..ffebcbe06d3c 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java +++ b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java @@ -18,7 +18,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; From c0147735cbc442679d1446b985e62f0a16513161 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Sat, 14 Mar 2026 01:11:40 +0000 Subject: [PATCH 5/6] Change Supplier to PropertiesFactory functional interface --- .../emf/EmfMetricLoggingPublisher.java | 25 ++++--- .../publishers/emf/PropertiesFactory.java | 69 +++++++++++++++++++ .../emf/internal/EmfMetricConfiguration.java | 21 +++--- .../emf/internal/MetricEmfConverter.java | 14 ++-- .../emf/EmfMetricLoggingPublisherTest.java | 16 ++--- .../emf/internal/MetricEmfConverterTest.java | 2 +- 6 files changed, 107 insertions(+), 40 deletions(-) create mode 100644 metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/PropertiesFactory.java diff --git a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java index 4bd12cd68135..dbe858c23dac 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java @@ -20,8 +20,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Map; -import java.util.function.Supplier; import software.amazon.awssdk.annotations.Immutable; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; @@ -84,7 +82,7 @@ private EmfMetricLoggingPublisher(Builder builder) { .dimensions(builder.dimensions) .metricLevel(builder.metricLevel) .metricCategories(builder.metricCategories) - .propertiesSupplier(builder.propertiesSupplier) + .propertiesFactory(builder.propertiesFactory) .build(); this.metricConverter = new MetricEmfConverter(config); @@ -126,7 +124,7 @@ public static final class Builder { private Collection> dimensions; private Collection metricCategories; private MetricLevel metricLevel; - private Supplier> propertiesSupplier; + private PropertiesFactory propertiesFactory; private Builder() { } @@ -222,23 +220,24 @@ public Builder metricLevel(MetricLevel metricLevel) { /** - * Configure a supplier of custom properties to include in each EMF record. - * The supplier is invoked on each {@link #publish(MetricCollection)} call, - * and the returned map entries are written as top-level key-value pairs - * in the EMF JSON output. These appear as searchable fields in - * CloudWatch Logs Insights. + * Configure a factory for custom properties to include in each EMF record. + * The factory is invoked on each {@link #publish(MetricCollection)} call with the + * {@link MetricCollection} being published, and the returned map entries are written + * as top-level key-value pairs in the EMF JSON output. These appear as searchable + * fields in CloudWatch Logs Insights. * *

Keys that collide with reserved EMF fields ({@code _aws}), configured * dimension names, or reported metric names are silently skipped. * *

If this is not specified, no custom properties are added. * - * @param propertiesSupplier a supplier returning a map of property names to values, - * or {@code null} to disable custom properties + * @param propertiesFactory a factory returning a map of property names to values, + * or {@code null} to disable custom properties * @return this builder + * @see PropertiesFactory */ - public Builder propertiesSupplier(Supplier> propertiesSupplier) { - this.propertiesSupplier = propertiesSupplier; + public Builder propertiesFactory(PropertiesFactory propertiesFactory) { + this.propertiesFactory = propertiesFactory; return this; } diff --git a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/PropertiesFactory.java b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/PropertiesFactory.java new file mode 100644 index 000000000000..9755406f57b9 --- /dev/null +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/PropertiesFactory.java @@ -0,0 +1,69 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.metrics.publishers.emf; + +import java.util.Map; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.metrics.MetricCollection; + +/** + * A factory for producing custom properties to include in each EMF record. + * + *

Implementations receive the {@link MetricCollection} being published, allowing properties + * to be derived from the SDK metrics (e.g. service endpoint, request ID) or from ambient + * context (e.g. Lambda request ID, trace ID). + * + *

The returned map entries are written as top-level key-value pairs in the EMF JSON output, + * making them searchable in CloudWatch Logs Insights. Keys that collide with reserved EMF + * fields ({@code _aws}), dimension names, or metric names are silently skipped. + * + *

If the factory returns {@code null} or throws an exception, no custom properties are added + * and a warning is logged. + * + *

Example using ambient context: + *

{@code
+ * EmfMetricLoggingPublisher.builder()
+ *     .propertiesFactory(metrics -> Collections.singletonMap("RequestId", requestId))
+ *     .build();
+ * }
+ * + *

Example using metric collection values: + *

{@code
+ * EmfMetricLoggingPublisher.builder()
+ *     .propertiesFactory(metrics -> {
+ *         Map props = new HashMap<>();
+ *         metrics.metricValues(CoreMetric.SERVICE_ENDPOINT)
+ *                .stream().findFirst()
+ *                .ifPresent(uri -> props.put("ServiceEndpoint", uri.toString()));
+ *         return props;
+ *     })
+ *     .build();
+ * }
+ * + * @see EmfMetricLoggingPublisher.Builder#propertiesFactory(PropertiesFactory) + */ +@FunctionalInterface +@SdkPublicApi +public interface PropertiesFactory { + + /** + * Create a map of custom properties to include in the EMF record for the given metric collection. + * + * @param metricCollection the SDK metric collection being published + * @return a map of property names to string values, or {@code null} for no custom properties + */ + Map create(MetricCollection metricCollection); +} diff --git a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java index a28417d53763..249da1178985 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java @@ -18,9 +18,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; -import java.util.Map; import java.util.Set; -import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -28,6 +26,7 @@ import software.amazon.awssdk.metrics.MetricCategory; import software.amazon.awssdk.metrics.MetricLevel; import software.amazon.awssdk.metrics.SdkMetric; +import software.amazon.awssdk.metrics.publishers.emf.PropertiesFactory; import software.amazon.awssdk.utils.Validate; import software.amazon.awssdk.utils.internal.SystemSettingUtils; @@ -45,7 +44,7 @@ public final class EmfMetricConfiguration { private final Set> dimensions; private final Collection metricCategories; private final MetricLevel metricLevel; - private final Supplier> propertiesSupplier; + private final PropertiesFactory propertiesFactory; private EmfMetricConfiguration(Builder builder) { this.namespace = builder.namespace == null ? DEFAULT_NAMESPACE : builder.namespace; @@ -53,9 +52,9 @@ private EmfMetricConfiguration(Builder builder) { this.dimensions = builder.dimensions == null ? DEFAULT_DIMENSIONS : new HashSet<>(builder.dimensions); this.metricCategories = builder.metricCategories == null ? DEFAULT_CATEGORIES : new HashSet<>(builder.metricCategories); this.metricLevel = builder.metricLevel == null ? DEFAULT_METRIC_LEVEL : builder.metricLevel; - this.propertiesSupplier = builder.propertiesSupplier == null - ? Collections::emptyMap - : builder.propertiesSupplier; + this.propertiesFactory = builder.propertiesFactory == null + ? mc -> Collections.emptyMap() + : builder.propertiesFactory; } @@ -65,7 +64,7 @@ public static class Builder { private Collection> dimensions; private Collection metricCategories; private MetricLevel metricLevel; - private Supplier> propertiesSupplier; + private PropertiesFactory propertiesFactory; public Builder namespace(String namespace) { this.namespace = namespace; @@ -92,8 +91,8 @@ public Builder metricLevel(MetricLevel metricLevel) { return this; } - public Builder propertiesSupplier(Supplier> propertiesSupplier) { - this.propertiesSupplier = propertiesSupplier; + public Builder propertiesFactory(PropertiesFactory propertiesFactory) { + this.propertiesFactory = propertiesFactory; return this; } @@ -122,8 +121,8 @@ public MetricLevel metricLevel() { return metricLevel; } - public Supplier> propertiesSupplier() { - return propertiesSupplier; + public PropertiesFactory propertiesFactory() { + return propertiesFactory; } private String resolveLogGroupName(Builder builder) { diff --git a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java index e940b1e2185e..6f9050f04db2 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java @@ -27,13 +27,13 @@ import java.util.Map; import java.util.Queue; import java.util.Set; -import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.metrics.MetricCategory; import software.amazon.awssdk.metrics.MetricCollection; import software.amazon.awssdk.metrics.MetricRecord; import software.amazon.awssdk.metrics.SdkMetric; +import software.amazon.awssdk.metrics.publishers.emf.PropertiesFactory; import software.amazon.awssdk.protocols.jsoncore.JsonWriter; import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.MetricValueNormalizer; @@ -71,14 +71,14 @@ public class MetricEmfConverter { private final EmfMetricConfiguration config; private final boolean metricCategoriesContainsAll; private final Clock clock; - private final Supplier> propertiesSupplier; + private final PropertiesFactory propertiesFactory; @SdkTestInternalApi public MetricEmfConverter(EmfMetricConfiguration config, Clock clock) { this.config = config; this.clock = clock; this.metricCategoriesContainsAll = config.metricCategories().contains(MetricCategory.ALL); - this.propertiesSupplier = config.propertiesSupplier(); + this.propertiesFactory = config.propertiesFactory(); } public MetricEmfConverter(EmfMetricConfiguration config) { @@ -143,16 +143,16 @@ public List convertMetricCollectionToEmf(MetricCollection metricCollecti } } - Map properties = resolveProperties(); + Map properties = resolveProperties(metricCollection); return createEmfStrings(aggregatedMetrics, properties); } - private Map resolveProperties() { + private Map resolveProperties(MetricCollection metricCollection) { try { - Map result = propertiesSupplier.get(); + Map result = propertiesFactory.create(metricCollection); return result == null ? Collections.emptyMap() : result; } catch (Exception e) { - logger.warn(() -> "Properties supplier threw an exception, publishing without custom properties", e); + logger.warn(() -> "Properties factory threw an exception, publishing without custom properties", e); return Collections.emptyMap(); } } diff --git a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java index ffebcbe06d3c..659a2fddd969 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java +++ b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java @@ -103,20 +103,20 @@ void Publish_multipleMetrics() { } @Test - void publish_propertiesSupplierThrowsException_publishesWithoutCustomProperties() { + void publish_propertiesFactoryThrowsException_publishesWithoutCustomProperties() { EmfMetricLoggingPublisher publisher = publisherBuilder .logGroupName("/aws/lambda/emfMetricTest") - .propertiesSupplier(() -> { throw new RuntimeException("supplier failed"); }) + .propertiesFactory(mc -> { throw new RuntimeException("factory failed"); }) .build(); MetricCollector metricCollector = MetricCollector.create("test"); metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); publisher.publish(metricCollector.collect()); - // Should have: 1 warning about supplier + 1 EMF info log + // Should have: 1 warning about factory + 1 EMF info log boolean hasWarning = loggedEvents().stream() .anyMatch(e -> e.getLevel() == Level.WARN - && e.getMessage().getFormattedMessage().contains("Properties supplier threw an exception")); + && e.getMessage().getFormattedMessage().contains("Properties factory threw an exception")); assertThat(hasWarning).isTrue(); boolean hasEmfOutput = loggedEvents().stream() @@ -133,10 +133,10 @@ void publish_propertiesSupplierThrowsException_publishesWithoutCustomProperties( } @Test - void publish_propertiesSupplierReturnsNull_publishesWithoutCustomProperties() { + void publish_propertiesFactoryReturnsNull_publishesWithoutCustomProperties() { EmfMetricLoggingPublisher publisher = publisherBuilder .logGroupName("/aws/lambda/emfMetricTest") - .propertiesSupplier(() -> null) + .propertiesFactory(mc -> null) .build(); MetricCollector metricCollector = MetricCollector.create("test"); @@ -161,11 +161,11 @@ void publish_propertiesSupplierReturnsNull_publishesWithoutCustomProperties() { } @Test - void publish_statefulSupplier_eachPublishUsesCurrentMap() { + void publish_statefulFactory_eachPublishUsesCurrentMap() { AtomicInteger counter = new AtomicInteger(0); EmfMetricLoggingPublisher publisher = publisherBuilder .logGroupName("/aws/lambda/emfMetricTest") - .propertiesSupplier(() -> { + .propertiesFactory(mc -> { int count = counter.incrementAndGet(); Map map = new HashMap(); map.put("InvocationCount", String.valueOf(count)); diff --git a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java index cf1383f5681b..a33522af594e 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java +++ b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java @@ -379,7 +379,7 @@ void convertMetricCollectionToEmf_batchedRecords_allContainCustomProperties() { private MetricEmfConverter converterWithProperties(Map properties) { EmfMetricConfiguration config = new EmfMetricConfiguration.Builder() .logGroupName("my_log_group_name") - .propertiesSupplier(() -> properties) + .propertiesFactory(mc -> properties) .build(); return new MetricEmfConverter(config, fixedClock); } From 817bc8773cfaeee9e0564ed698d1a0a69af8ee54 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Sat, 14 Mar 2026 01:16:11 +0000 Subject: [PATCH 6/6] Update changelog entry --- .../feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changes/next-release/feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json b/.changes/next-release/feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json index d6aa0fee0b52..fb7ac8c8255f 100644 --- a/.changes/next-release/feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json +++ b/.changes/next-release/feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json @@ -2,5 +2,5 @@ "type": "feature", "category": "Amazon CloudWatch EMF Metric Publisher", "contributor": "humanzz", - "description": "Add `propertiesSupplier` to `EmfMetricLoggingPublisher.Builder`, enabling users to enrich EMF records with custom key-value properties that are searchable in CloudWatch Logs Insights. See [#6595](https://github.com/aws/aws-sdk-java-v2/issues/6595)." + "description": "Add `PropertiesFactory` and `propertiesFactory` to `EmfMetricLoggingPublisher.Builder`, enabling users to enrich EMF records with custom key-value properties derived from the metric collection or ambient context, searchable in CloudWatch Logs Insights. See [#6595](https://github.com/aws/aws-sdk-java-v2/issues/6595)." }