metricCategories) {
+ this.metricCategories = new ArrayList<>(metricCategories);
+ return this;
+ }
+
+ /**
+ * @see #metricCategories(Collection)
+ */
+ public Builder metricCategories(MetricCategory... metricCategories) {
+ return metricCategories(Arrays.asList(metricCategories));
+ }
+
+ /**
+ * Configure the LogGroupName key that will be put into the emf log to this publisher. This is required when using
+ * the CloudWatch agent to send embedded metric format logs that tells the agent which log
+ * group to use.
+ *
+ * If this is not specified, for AWS lambda environments, {@code AWS_LAMBDA_LOG_GROUP_NAME}
+ * is used.
+ * This field is required and must not be null or empty for non-lambda environments.
+ * @throws NullPointerException if non-lambda environment and logGroupName is null
+ */
+ public Builder logGroupName(String logGroupName) {
+ this.logGroupName = logGroupName;
+ return this;
+ }
+
+ /**
+ * Configure the {@link MetricLevel} that should be uploaded to CloudWatch.
+ *
+ *
If this is not specified, {@link MetricLevel#INFO} is used.
+ *
+ *
All {@link SdkMetric}s are associated with one {@code MetricLevel}. This setting determines which level of metrics
+ * uploaded to CloudWatch. Any metrics {@link #publish(MetricCollection)}ed that do not fall under these configured
+ * categories are ignored.
+ *
+ *
Note: If there are {@link #dimensions(Collection)} configured that do not fall under this {@code MetricLevel}
+ * values, the dimensions will NOT be ignored. In other words, the metric category configuration only affects which
+ * metrics are uploaded to CloudWatch, not which values can be used for {@code dimensions}.
+ */
+ public Builder metricLevel(MetricLevel metricLevel) {
+ this.metricLevel = metricLevel;
+ return this;
+ }
+
+
+ /**
+ * Build a {@link EmfMetricLoggingPublisher} using the configuration currently configured on this publisher.
+ */
+ public EmfMetricLoggingPublisher build() {
+ return new EmfMetricLoggingPublisher(this);
+ }
+
+ }
+}
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
new file mode 100644
index 000000000000..739cf0b0e4ca
--- /dev/null
+++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java
@@ -0,0 +1,119 @@
+/*
+ * 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.internal;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.core.metrics.CoreMetric;
+import software.amazon.awssdk.metrics.MetricCategory;
+import software.amazon.awssdk.metrics.MetricLevel;
+import software.amazon.awssdk.metrics.SdkMetric;
+import software.amazon.awssdk.utils.Validate;
+import software.amazon.awssdk.utils.internal.SystemSettingUtils;
+
+@SdkInternalApi
+public final class EmfMetricConfiguration {
+ private static final String DEFAULT_NAMESPACE = "AwsSdk/JavaSdk2";
+ private static final Set> DEFAULT_DIMENSIONS = Stream.of(CoreMetric.SERVICE_ID,
+ CoreMetric.OPERATION_NAME)
+ .collect(Collectors.toSet());
+ private static final Set DEFAULT_CATEGORIES = Collections.singleton(MetricCategory.ALL);
+ private static final MetricLevel DEFAULT_METRIC_LEVEL = MetricLevel.INFO;
+
+ private final String namespace;
+ private final String logGroupName;
+ private final Set> dimensions;
+ private final Collection metricCategories;
+ private final MetricLevel metricLevel;
+
+ private EmfMetricConfiguration(Builder builder) {
+ this.namespace = builder.namespace == null ? DEFAULT_NAMESPACE : builder.namespace;
+ this.logGroupName = Validate.paramNotNull(resolveLogGroupName(builder), "logGroupName");
+ 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;
+ }
+
+
+ public static class Builder {
+ private String namespace;
+ private String logGroupName;
+ private Collection> dimensions;
+ private Collection metricCategories;
+ private MetricLevel metricLevel;
+
+ public Builder namespace(String namespace) {
+ this.namespace = namespace;
+ return this;
+ }
+
+ public Builder logGroupName(String logGroupName) {
+ this.logGroupName = logGroupName;
+ return this;
+ }
+
+ public Builder dimensions(Collection> dimensions) {
+ this.dimensions = dimensions;
+ return this;
+ }
+
+ public Builder metricCategories(Collection metricCategories) {
+ this.metricCategories = metricCategories;
+ return this;
+ }
+
+ public Builder metricLevel(MetricLevel metricLevel) {
+ this.metricLevel = metricLevel;
+ return this;
+ }
+
+ public EmfMetricConfiguration build() {
+ return new EmfMetricConfiguration(this);
+ }
+ }
+
+ public String namespace() {
+ return namespace;
+ }
+
+ public String logGroupName() {
+ return logGroupName;
+ }
+
+ public Collection> dimensions() {
+ return dimensions;
+ }
+
+ public Collection metricCategories() {
+ return metricCategories;
+ }
+
+ public MetricLevel metricLevel() {
+ return metricLevel;
+ }
+
+ 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
new file mode 100644
index 000000000000..7973fa1fa0de
--- /dev/null
+++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java
@@ -0,0 +1,379 @@
+/*
+ * 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.internal;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.ArrayList;
+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 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.protocols.jsoncore.JsonWriter;
+import software.amazon.awssdk.utils.Logger;
+import software.amazon.awssdk.utils.MetricValueNormalizer;
+import software.amazon.awssdk.utils.Validate;
+
+/**
+ * Converts {@link MetricCollection} into List of Amazon CloudWatch Embedded Metric Format (EMF) Strings.
+ *
+ * This class is responsible for transforming {@link MetricCollection} into the EMF format required by CloudWatch.
+ * It handles the conversion of different metric types and ensures the output conforms to EMF specifications.
+ *
+ *
+ *
+ * @see
+ * CloudWatch Embedded Metric Format Specification
+ * @see EmfMetricConfiguration
+ */
+@SdkInternalApi
+public class MetricEmfConverter {
+ /**
+ * EMF allows up to 100 elements in an array
+ * ...
+ */
+ private static final int MAX_RECORD_SIZE = 100;
+
+ /**
+ * EMF allows up to 100 MetricDefinition objects in one emf string
+ */
+ private static final int MAX_METRIC_NUM = 100;
+
+ private static final Logger logger = Logger.loggerFor(MetricEmfConverter.class);
+ private final List dimensions = new ArrayList<>();
+ private final EmfMetricConfiguration config;
+ private final boolean metricCategoriesContainsAll;
+ private final Clock clock;
+
+ @SdkTestInternalApi
+ public MetricEmfConverter(EmfMetricConfiguration config, Clock clock) {
+ this.config = config;
+ this.clock = clock;
+ this.metricCategoriesContainsAll = config.metricCategories().contains(MetricCategory.ALL);
+ }
+
+ public MetricEmfConverter(EmfMetricConfiguration config) {
+ this(config, Clock.systemUTC());
+ }
+
+ /**
+ *
+ * Convert SDK Metrics to EMF Format.
+ * Transforms a collection of SDK metrics into CloudWatch's Embedded Metric Format (EMF).
+ * The method processes standard SDK measurements and structures them according to
+ * CloudWatch's EMF specification.
+ *
+ * Example Output
+ * @snippet
+ * {
+ * "_aws": {
+ * "Timestamp": 1672963200,
+ * "CloudWatchMetrics": [{
+ * "Namespace": "AwsSdk/JavaSdk2",
+ * "Dimensions": [["ServiceId"]],
+ * "Metrics": [{
+ * "Name": "AvailableConcurrency",
+ * }]
+ * }]
+ * },
+ * "ServiceId": "DynamoDB",
+ * "AvailableConcurrency": 5
+ * }
+ *
+ * @param metricCollection Collection of SDK metrics to be converted
+ * @return List of EMF-formatted metrics ready for CloudWatch
+ */
+ public List convertMetricCollectionToEmf(MetricCollection metricCollection) {
+ Map, List>> aggregatedMetrics = new HashMap<>();
+
+ // Process metrics using level-order traversal
+ Queue queue = new LinkedList<>();
+ if (!queue.offer(metricCollection)) {
+ logger.warn(() -> "failed to add metricCollection to the queue");
+ }
+
+ while (!queue.isEmpty()) {
+ MetricCollection current = queue.poll();
+
+ current.stream().forEach(metricRecord -> {
+ SdkMetric> metric = metricRecord.metric();
+ String metricName = metric.name();
+ if (isDimension(metric) && !dimensions.contains(metricName)) {
+ dimensions.add(metricName);
+ }
+
+ if (shouldReport(metricRecord) || isDimension(metric)) {
+ aggregatedMetrics.computeIfAbsent(metric, k -> new ArrayList<>())
+ .add(metricRecord);
+ }
+
+ });
+
+ if (current.children() != null) {
+ queue.addAll(current.children());
+ }
+ }
+
+ return createEmfStrings(aggregatedMetrics);
+ }
+
+ /**
+ * Processes and normalizes metric values for EMF formatting.
+ * The method handles various input types and normalizes them according to EMF requirements:
+ * Value Conversion Rules:
+ *
+ * - Numbers (Integer, Long, Double, etc.) are converted to their native numeric format
+ * - Duration values are converted to milliseconds
+ * - Date/Time values are converted to epoch milliseconds
+ * - Null values are omitted from the output
+ * - Boolean values are converted to 1 (true) or 0 (false)
+ * - Non-Dimension metrics with non-numeric values are omitted from the output
+ *
+ *
+ * @param mRecord The metric record to process
+ */
+ private void processAndWriteValue(JsonWriter jsonWriter, MetricRecord> mRecord) {
+ Object value = mRecord.value();
+ Class> valueClass = mRecord.metric().valueClass();
+
+ if (value == null) {
+ return;
+ }
+
+ if (Boolean.class.isAssignableFrom(valueClass)) {
+ jsonWriter.writeValue(value.equals(true) ? 1.0 : 0.0);
+ return;
+ }
+
+ if (Duration.class.isAssignableFrom(valueClass)) {
+ Duration duration = (Duration) value;
+ double millisValue = duration.toMillis();
+ jsonWriter.writeValue(millisValue);
+ return;
+ }
+
+ if (Double.class.isAssignableFrom(valueClass)) {
+ jsonWriter.writeValue(MetricValueNormalizer.normalize((Double) value));
+ return;
+ }
+
+ if (Integer.class.isAssignableFrom(valueClass) || Long.class.isAssignableFrom(valueClass)) {
+ jsonWriter.writeValue((Integer) value);
+ }
+ }
+
+ private List createEmfStrings(Map, List>> aggregatedMetrics) {
+ List emfStrings = new ArrayList<>();
+ Map, List>> currentMetricBatch = new HashMap<>();
+
+ for (Map.Entry, List>> entry : aggregatedMetrics.entrySet()) {
+ SdkMetric> metric = entry.getKey();
+ List> records = entry.getValue();
+ int size = records.size();
+ if (records.size() > MAX_RECORD_SIZE) {
+ records = records.subList(0, MAX_RECORD_SIZE);
+ logger.warn(() -> String.format("Some AWS SDK client-side metric data have been dropped because it exceeds the "
+ + "cloudwatch requirements. "
+ + "There are %d values for metric %s", size, metric.name()));
+ }
+
+ if (currentMetricBatch.size() == MAX_METRIC_NUM) {
+ emfStrings.add(createEmfString(currentMetricBatch));
+ currentMetricBatch = new HashMap<>();
+ }
+
+ currentMetricBatch.put(metric, records);
+ }
+
+ emfStrings.add(createEmfString(currentMetricBatch));
+
+ return emfStrings;
+ }
+
+
+ private String createEmfString(Map, List>> metrics) {
+
+ JsonWriter jsonWriter = JsonWriter.create();
+ jsonWriter.writeStartObject();
+
+ writeAwsObject(jsonWriter, metrics.keySet());
+ writeMetricValues(jsonWriter, metrics);
+
+ jsonWriter.writeEndObject();
+
+ return new String(jsonWriter.getBytes(), StandardCharsets.UTF_8);
+
+ }
+
+ private void writeAwsObject(JsonWriter jsonWriter, Set> metricNames) {
+ jsonWriter.writeFieldName("_aws");
+ jsonWriter.writeStartObject();
+
+ jsonWriter.writeFieldName("Timestamp");
+ jsonWriter.writeValue(clock.instant().toEpochMilli());
+
+ jsonWriter.writeFieldName("LogGroupName");
+ jsonWriter.writeValue(config.logGroupName());
+
+ writeCloudWatchMetricsArray(jsonWriter, metricNames);
+ jsonWriter.writeEndObject();
+ }
+
+ private void writeCloudWatchMetricsArray(JsonWriter jsonWriter, Set> metricNames) {
+ jsonWriter.writeFieldName("CloudWatchMetrics");
+ jsonWriter.writeStartArray();
+
+ writeCloudWatchMetricsObjects(jsonWriter, metricNames);
+ jsonWriter.writeEndArray();
+ }
+
+ private void writeCloudWatchMetricsObjects(JsonWriter jsonWriter, Set> metricNames) {
+ jsonWriter.writeStartObject();
+ jsonWriter.writeFieldName("Namespace");
+ jsonWriter.writeValue(config.namespace());
+
+ writeDimensionSetArray(jsonWriter);
+
+ writeMetricDefinitionArray(jsonWriter, metricNames);
+ jsonWriter.writeEndObject();
+ }
+
+ private void writeDimensionSetArray(JsonWriter jsonWriter) {
+ jsonWriter.writeFieldName("Dimensions");
+ jsonWriter.writeStartArray();
+ jsonWriter.writeStartArray();
+ for (String dimension : dimensions) {
+ jsonWriter.writeValue(dimension);
+ }
+ jsonWriter.writeEndArray();
+ jsonWriter.writeEndArray();
+ }
+
+ private void writeMetricDefinitionArray(JsonWriter jsonWriter, Set> metricNames) {
+ jsonWriter.writeFieldName("Metrics");
+ jsonWriter.writeStartArray();
+
+ metricNames.forEach(sdkMetric -> writeMetricDefinition(jsonWriter, sdkMetric));
+
+ jsonWriter.writeEndArray();
+ }
+
+ private void writeMetricDefinition(JsonWriter jsonWriter, SdkMetric> sdkMetric) {
+ if (!isNumericMetric(sdkMetric)) {
+ return;
+ }
+
+ jsonWriter.writeStartObject();
+ jsonWriter.writeFieldName("Name");
+ jsonWriter.writeValue(sdkMetric.name());
+
+ String unit = getMetricUnit(sdkMetric.valueClass());
+ if (unit != null) {
+ jsonWriter.writeFieldName("Unit");
+ jsonWriter.writeValue(unit);
+ }
+
+ jsonWriter.writeEndObject();
+ }
+
+ private void writeMetricValues(JsonWriter jsonWriter, Map, List>> metrics) {
+ metrics.forEach((metric, records) -> {
+ if (isDimension(metric)) {
+ writeDimensionValue(jsonWriter, metric, records);
+ } else {
+ writeMetricRecord(jsonWriter, metric, records);
+ }
+ });
+ }
+
+ private void writeDimensionValue(JsonWriter jsonWriter, SdkMetric> metric, List> records) {
+ Validate.validState(records.size() == 1 && String.class.isAssignableFrom(metric.valueClass()),
+ "Metric (%s) is configured as a dimension, and the value must be a single string",
+ metric.name());
+
+ jsonWriter.writeFieldName(metric.name());
+ jsonWriter.writeValue((String) records.get(0).value());
+ }
+
+ private void writeMetricRecord(JsonWriter jsonWriter, SdkMetric> metric, List> records) {
+ if (!isNumericMetric(metric)) {
+ return;
+ }
+
+ jsonWriter.writeFieldName(metric.name());
+
+ if (records.size() == 1) {
+ processAndWriteValue(jsonWriter, records.get(0));
+ } else {
+ writeMetricArray(jsonWriter, records);
+ }
+ }
+
+ private boolean isNumericMetric(SdkMetric> metric) {
+ return Integer.class.isAssignableFrom(metric.valueClass())
+ || Boolean.class.isAssignableFrom(metric.valueClass())
+ || Long.class.isAssignableFrom(metric.valueClass())
+ || Duration.class.isAssignableFrom(metric.valueClass())
+ || Double.class.isAssignableFrom(metric.valueClass());
+ }
+
+
+ private void writeMetricArray(JsonWriter jsonWriter, List> records) {
+ jsonWriter.writeStartArray();
+ for (MetricRecord> mRecord : records) {
+ processAndWriteValue(jsonWriter, mRecord);
+ }
+ jsonWriter.writeEndArray();
+ }
+
+
+ private boolean isDimension(SdkMetric> metric) {
+ return config.dimensions().contains(metric);
+ }
+
+
+ private String getMetricUnit(Class> type) {
+ if (Duration.class.isAssignableFrom(type)) {
+ return "Milliseconds";
+ }
+ return null;
+ }
+
+ private boolean shouldReport(MetricRecord> metricRecord) {
+ return isSupportedCategory(metricRecord) && isSupportedLevel(metricRecord);
+ }
+
+ private boolean isSupportedCategory(MetricRecord> metricRecord) {
+ return metricCategoriesContainsAll ||
+ metricRecord.metric()
+ .categories()
+ .stream()
+ .anyMatch(config.metricCategories()::contains);
+ }
+
+ private boolean isSupportedLevel(MetricRecord> metricRecord) {
+ return config.metricLevel().includesLevel(metricRecord.metric().level());
+ }
+}
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
new file mode 100644
index 000000000000..f3da12abfb4c
--- /dev/null
+++ b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.http.HttpMetric;
+import software.amazon.awssdk.metrics.MetricCategory;
+import software.amazon.awssdk.metrics.MetricCollector;
+import software.amazon.awssdk.metrics.MetricLevel;
+import software.amazon.awssdk.testutils.LogCaptor;
+import software.amazon.awssdk.utils.internal.SystemSettingUtilsTestBackdoor;
+
+
+public class EmfMetricLoggingPublisherTest extends LogCaptor.LogCaptorTestBase{
+
+ private EmfMetricLoggingPublisher.Builder publisherBuilder;
+
+
+ @BeforeEach
+ void setUp() {
+ publisherBuilder = EmfMetricLoggingPublisher.builder();
+ }
+
+ @Test
+ void Publish_noLogGroupName_throwException() {
+ assertThatThrownBy(() -> {
+ EmfMetricLoggingPublisher publisher = publisherBuilder.build();
+ publisher.publish(null);
+ })
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("logGroupName must not be null.");
+ }
+
+ @Test
+ void Publish_noLogGroupNameInLambda_defaultLogGroupName() {
+ SystemSettingUtilsTestBackdoor.addEnvironmentVariableOverride("AWS_LAMBDA_LOG_GROUP_NAME", "/aws/lambda/testFunction");
+ EmfMetricLoggingPublisher publisher = publisherBuilder.build();
+ MetricCollector metricCollector = MetricCollector.create("test");
+ publisher.publish(metricCollector.collect());
+ assertThat(loggedEvents()).hasSize(2);
+ assertThat(loggedEvents().get(1).toString()).contains("/aws/lambda/testFunction");
+ SystemSettingUtilsTestBackdoor.clearEnvironmentVariableOverrides();
+ }
+
+ @Test
+ void Publish_nullMetrics() {
+ EmfMetricLoggingPublisher publisher = publisherBuilder.logGroupName("/aws/lambda/emfMetricTest").build();
+ publisher.publish(null);
+ assertThat(loggedEvents()).hasSize(1);
+ }
+
+ @Test
+ void Publish_metricCollectionWithChild() {
+ EmfMetricLoggingPublisher publisher = publisherBuilder.dimensions(HttpMetric.HTTP_CLIENT_NAME)
+ .logGroupName("/aws/lambda/emfMetricTest")
+ .namespace("ExampleSDKV2MetricsEmf")
+ .metricLevel(MetricLevel.INFO)
+ .metricCategories(MetricCategory.HTTP_CLIENT)
+ .build();
+
+ MetricCollector metricCollector = MetricCollector.create("test");
+ metricCollector.reportMetric(HttpMetric.HTTP_CLIENT_NAME, "apache-http-client");
+ metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5);
+
+ MetricCollector childMetricCollector1 = metricCollector.createChild("child1");
+ childMetricCollector1.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(100));
+ MetricCollector childMetricCollector2 = metricCollector.createChild("child2");
+ childMetricCollector2.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(200));
+ publisher.publish(metricCollector.collect());
+ assertThat(loggedEvents()).hasSize(4);
+ }
+
+ @Test
+ void Publish_multipleMetrics() {
+ EmfMetricLoggingPublisher publisher = publisherBuilder.logGroupName("/aws/lambda/emfMetricTest").build();
+ MetricCollector metricCollector = MetricCollector.create("test");
+ metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5);
+ metricCollector.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(100));
+ publisher.publish(metricCollector.collect());
+ assertThat(loggedEvents()).hasSize(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
new file mode 100644
index 000000000000..07473f5afc02
--- /dev/null
+++ b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java
@@ -0,0 +1,236 @@
+/*
+ * 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.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.networknt.schema.JsonSchema;
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.SpecVersion;
+import com.networknt.schema.ValidationMessage;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.core.metrics.CoreMetric;
+import software.amazon.awssdk.http.HttpMetric;
+import software.amazon.awssdk.metrics.MetricCategory;
+import software.amazon.awssdk.metrics.MetricCollector;
+import software.amazon.awssdk.metrics.MetricLevel;
+import software.amazon.awssdk.metrics.SdkMetric;
+import software.amazon.awssdk.utils.IoUtils;
+
+public class MetricEmfConverterTest {
+
+ private MetricEmfConverter metricEmfConverterDefault;
+ private MetricEmfConverter metricEmfConverterCustom;
+ private final EmfMetricConfiguration testConfigDefault = new EmfMetricConfiguration.Builder()
+ .logGroupName("my_log_group_name")
+ .build();
+ private final EmfMetricConfiguration testConfigCustom = new EmfMetricConfiguration.Builder()
+ .namespace("AwsSdk/Test/JavaSdk2")
+ .logGroupName("my_log_group_name")
+ .dimensions(Stream.of(HttpMetric.HTTP_CLIENT_NAME).collect(Collectors.toSet()))
+ .metricCategories(Stream.of(MetricCategory.HTTP_CLIENT).collect(Collectors.toSet()))
+ .metricLevel(MetricLevel.TRACE)
+ .build();
+ private final Clock fixedClock = Clock.fixed(
+ Instant.ofEpochMilli(12345678),
+ ZoneOffset.UTC
+ );
+
+ @BeforeEach
+ void setUp() {
+ metricEmfConverterDefault = new MetricEmfConverter(testConfigDefault,fixedClock);
+ metricEmfConverterCustom = new MetricEmfConverter(testConfigCustom, fixedClock);
+ }
+
+ @Test
+ void ConvertMetricCollectionToEMF_emptyCollection() {
+ List emfLogs = metricEmfConverterDefault.convertMetricCollectionToEmf(MetricCollector.create("test").collect());
+
+ assertThat(emfLogs).containsOnly("{\"_aws\":{\"Timestamp\":12345678,\"LogGroupName\":\"my_log_group_name\","
+ + "\"CloudWatchMetrics\":[{\"Namespace\":\"AwsSdk/JavaSdk2\",\"Dimensions\":[[]],"
+ + "\"Metrics\":[]}]}}");
+ }
+
+ @Test
+ void ConvertMetricCollectionToEMF_multipleMetrics() {
+
+ MetricCollector metricCollector = MetricCollector.create("test");
+ metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5);
+ metricCollector.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(100));
+ 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\"},"
+ + "{\"Name\":\"ConcurrencyAcquireDuration\",\"Unit\":\"Milliseconds\"}]}]},\"AvailableConcurrency\":5,\"ConcurrencyAcquireDuration\":100.0}");
+ }
+
+
+ @Test
+ void ConvertMetricCollectionToEMF_oneDimension() {
+
+ MetricCollector metricCollector = MetricCollector.create("test");
+ metricCollector.reportMetric(CoreMetric.OPERATION_NAME, "operationName");
+ metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5);
+ metricCollector.reportMetric(HttpMetric.HTTP_STATUS_CODE, 404);
+ List emfLogs = metricEmfConverterDefault.convertMetricCollectionToEmf(metricCollector.collect());
+
+ assertThat(emfLogs).containsOnly("{\"_aws\":{\"Timestamp\":12345678,\"LogGroupName\":\"my_log_group_name\","
+ + "\"CloudWatchMetrics\":[{\"Namespace\":\"AwsSdk/JavaSdk2\","
+ + "\"Dimensions\":[[\"OperationName\"]],"
+ + "\"Metrics\":[{\"Name\":\"AvailableConcurrency\"}]}]},"
+ + "\"AvailableConcurrency\":5,"
+ + "\"OperationName\":\"operationName\"}");
+ }
+
+ @Test
+ void ConvertMetricCollectionToEMF_metricCategoryConfigured_onlyHttpMetric() {
+ MetricCollector metricCollector = MetricCollector.create("test");
+ metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5);
+ metricCollector.reportMetric(CoreMetric.SERVICE_ID, "serviceId");
+ List emfLogs = metricEmfConverterCustom.convertMetricCollectionToEmf(metricCollector.collect());
+
+ assertThat(emfLogs).containsOnly("{\"_aws\":{\"Timestamp\":12345678,\"LogGroupName\":\"my_log_group_name\",\"CloudWatchMetrics\":[{\"Namespace\":"
+ + "\"AwsSdk/Test/JavaSdk2\",\"Dimensions\":[[]],"
+ + "\"Metrics\":[{\"Name\":\"AvailableConcurrency\"}]}]},\"AvailableConcurrency\":5}");
+ }
+
+ @Test
+ void convertMetricCollectionToEmf_traceEnabled_shouldIncludeTrace() {
+ MetricCollector metricCollector = MetricCollector.create("test");
+ metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5);
+ metricCollector.reportMetric(HttpMetric.HTTP_STATUS_CODE, 404);
+ List emfLogs = metricEmfConverterCustom.convertMetricCollectionToEmf(metricCollector.collect());
+
+ assertThat(emfLogs).containsOnly("{\"_aws\":{\"Timestamp\":12345678,\"LogGroupName\":\"my_log_group_name\","
+ + "\"CloudWatchMetrics\":[{\"Namespace\":\"AwsSdk/Test/JavaSdk2\""
+ + ",\"Dimensions\":[[]],\"Metrics\":[{\"Name\":\"AvailableConcurrency\"},{\"Name\":\"HttpStatusCode\"}]}]},\"AvailableConcurrency\":5,\"HttpStatusCode\":404}");
+ }
+
+
+
+ @Test
+ void ConvertMetricCollectionToEMF_multiChildCollections_recordList() {
+
+ MetricCollector metricCollector = MetricCollector.create("test");
+ metricCollector.reportMetric(HttpMetric.HTTP_CLIENT_NAME, "apache-http-client");
+ metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5);
+
+ MetricCollector childMetricCollector1 = metricCollector.createChild("child1");
+ childMetricCollector1.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(100));
+ MetricCollector childMetricCollector2 = metricCollector.createChild("child2");
+ childMetricCollector2.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(200));
+ 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\"},"
+ + "{\"Name\":\"ConcurrencyAcquireDuration\",\"Unit\":\"Milliseconds\"}]}]},\"AvailableConcurrency\":5,\"ConcurrencyAcquireDuration\":"
+ + "[100.0,200.0]}");
+ }
+
+ @Test
+ void ConvertMetricCollectionToEMF_recordsExceedsMaxSize_shouldDrop() {
+
+ MetricCollector metricCollector = MetricCollector.create("test");
+ metricCollector.reportMetric(HttpMetric.HTTP_CLIENT_NAME, "apache-http-client");
+ metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5);
+
+ for (int i = 0; i < 120; i++) {
+ MetricCollector childMetricCollector = metricCollector.createChild("child" + i);
+ childMetricCollector.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(100+i));
+ }
+
+ 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\"},{\"Name\":\"ConcurrencyAcquireDuration\",\"Unit\":\"Milliseconds\"}]}]},\"AvailableConcurrency\":5,"
+ + "\"ConcurrencyAcquireDuration\":[100.0,101.0,102.0,103.0,104.0,105.0,106.0,107.0,108.0,109.0,110.0,111.0,112.0,113.0,114.0,115.0,116.0,117.0,118.0,119.0,120.0,121.0,122.0,"
+ + "123.0,124.0,125.0,126.0,127.0,128.0,129.0,130.0,131.0,132.0,133.0,134.0,135.0,136.0,137.0,138.0,139.0,140.0,141.0,142.0,143.0,144.0,145.0,146.0,147.0,148.0,149.0,150.0,151.0,"
+ + "152.0,153.0,154.0,155.0,156.0,157.0,158.0,159.0,160.0,161.0,162.0,163.0,164.0,165.0,166.0,167.0,168.0,169.0,170.0,171.0,172.0,173.0,174.0,175.0,176.0,177.0,178.0,179.0,180.0,181.0,"
+ + "182.0,183.0,184.0,185.0,186.0,187.0,188.0,189.0,190.0,191.0,192.0,193.0,194.0,195.0,196.0,197.0,198.0,199.0]}");
+
+ }
+
+ @Test
+ void ConvertMetricCollectionToEMF_metricCollectionExceedsMaxSize_shouldSplit() {
+
+ MetricCollector metricCollector = MetricCollector.create("test");
+ for (int i = 0; i < 220; i++) {
+ metricCollector.reportMetric(SdkMetric.create("metric_" + i, Integer.class, MetricLevel.INFO, MetricCategory.CORE), i);
+ }
+
+ List emfLogs = metricEmfConverterDefault.convertMetricCollectionToEmf(metricCollector.collect());
+ assertThat(emfLogs).hasSize(3);
+ }
+
+ @Test
+ void ConvertMetricCollectionToEMF_dropNonNumericMetrics() {
+
+ MetricCollector metricCollector = MetricCollector.create("test");
+ metricCollector.reportMetric(CoreMetric.SERVICE_ID, "serviceId");
+ metricCollector.reportMetric(CoreMetric.OPERATION_NAME, "operationName");
+ metricCollector.reportMetric(SdkMetric.create("stringMetric", String.class, MetricLevel.INFO, MetricCategory.CORE),
+ "stringMetricValue");
+ 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\":[[\"OperationName\",\"ServiceId\"]],\"Metrics\":[{\"Name\":\"AvailableConcurrency\"}]}]},"
+ + "\"AvailableConcurrency\":5,\"OperationName\":\"operationName\",\"ServiceId\":\"serviceId\"}");
+ }
+
+ @Test
+ void ConvertMetricCollectionToEMF_shouldConformToSchema() throws Exception {
+
+ MetricCollector metricCollector = MetricCollector.create("test");
+ metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5);
+ metricCollector.reportMetric(CoreMetric.API_CALL_SUCCESSFUL, true);
+ metricCollector.reportMetric(CoreMetric.API_CALL_DURATION, java.time.Duration.ofMillis(100));
+ metricCollector.reportMetric(CoreMetric.SERVICE_ID, "serviceId");
+ metricCollector.reportMetric(CoreMetric.OPERATION_NAME, "operationName");
+ metricCollector.reportMetric(HttpMetric.HTTP_CLIENT_NAME, "apache-http-client");
+ MetricCollector childMetricCollector1 = metricCollector.createChild("child1");
+ childMetricCollector1.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(100));
+ MetricCollector childMetricCollector2 = metricCollector.createChild("child2");
+ childMetricCollector2.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(200));
+ MetricCollector childMetricCollector3 = metricCollector.createChild("child3");
+ childMetricCollector3.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(200));
+
+ List emfLogs = metricEmfConverterDefault.convertMetricCollectionToEmf(metricCollector.collect());
+
+ String jsonSchema = IoUtils.toUtf8String(Objects.requireNonNull(getClass().getResourceAsStream("/emfSchema.json")));
+ JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4);
+ JsonSchema schema = factory.getSchema(jsonSchema);
+
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode jsonNode = mapper.readTree(emfLogs.get(0));
+ Set errors = schema.validate(jsonNode);
+
+ assertThat(errors).isEmpty();
+ }
+}
\ No newline at end of file
diff --git a/metric-publishers/emf-metric-logging-publisher/src/test/resources/emfSchema.json b/metric-publishers/emf-metric-logging-publisher/src/test/resources/emfSchema.json
new file mode 100644
index 000000000000..1ed7be988024
--- /dev/null
+++ b/metric-publishers/emf-metric-logging-publisher/src/test/resources/emfSchema.json
@@ -0,0 +1,123 @@
+{
+ "type": "object",
+ "title": "Root Node",
+ "required": [
+ "_aws"
+ ],
+ "properties": {
+ "_aws": {
+ "$id": "#/properties/_aws",
+ "type": "object",
+ "title": "Metadata",
+ "required": [
+ "Timestamp",
+ "CloudWatchMetrics"
+ ],
+ "properties": {
+ "Timestamp": {
+ "$id": "#/properties/_aws/properties/Timestamp",
+ "type": "integer",
+ "title": "The Timestamp Schema",
+ "examples": [
+ 1565375354953
+ ]
+ },
+ "CloudWatchMetrics": {
+ "$id": "#/properties/_aws/properties/CloudWatchMetrics",
+ "type": "array",
+ "title": "MetricDirectives",
+ "items": {
+ "$id": "#/properties/_aws/properties/CloudWatchMetrics/items",
+ "type": "object",
+ "title": "MetricDirective",
+ "required": [
+ "Namespace",
+ "Dimensions",
+ "Metrics"
+ ],
+ "properties": {
+ "Namespace": {
+ "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Namespace",
+ "type": "string",
+ "title": "CloudWatch Metrics Namespace",
+ "examples": [
+ "MyApp"
+ ],
+ "pattern": "^(.*)$",
+ "minLength": 1,
+ "maxLength": 1024
+ },
+ "Dimensions": {
+ "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions",
+ "type": "array",
+ "title": "The Dimensions Schema",
+ "minItems": 1,
+ "items": {
+ "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions/items",
+ "type": "array",
+ "title": "DimensionSet",
+ "minItems": 0,
+ "maxItems": 30,
+ "items": {
+ "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions/items/items",
+ "type": "string",
+ "title": "DimensionReference",
+ "examples": [
+ "Operation"
+ ],
+ "pattern": "^(.*)$",
+ "minLength": 1,
+ "maxLength": 250
+ }
+ }
+ },
+ "Metrics": {
+ "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics",
+ "type": "array",
+ "title": "MetricDefinitions",
+ "items": {
+ "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items",
+ "type": "object",
+ "title": "MetricDefinition",
+ "required": [
+ "Name"
+ ],
+ "properties": {
+ "Name": {
+ "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/Name",
+ "type": "string",
+ "title": "MetricName",
+ "examples": [
+ "ProcessingLatency"
+ ],
+ "pattern": "^(.*)$",
+ "minLength": 1,
+ "maxLength": 1024
+ },
+ "Unit": {
+ "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/Unit",
+ "type": "string",
+ "title": "MetricUnit",
+ "examples": [
+ "Milliseconds"
+ ],
+ "pattern": "^(Seconds|Microseconds|Milliseconds|Bytes|Kilobytes|Megabytes|Gigabytes|Terabytes|Bits|Kilobits|Megabits|Gigabits|Terabits|Percent|Count|Bytes\\/Second|Kilobytes\\/Second|Megabytes\\/Second|Gigabytes\\/Second|Terabytes\\/Second|Bits\\/Second|Kilobits\\/Second|Megabits\\/Second|Gigabits\\/Second|Terabits\\/Second|Count\\/Second|None)$"
+ },
+ "StorageResolution": {
+ "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/StorageResolution",
+ "type": "integer",
+ "title": "StorageResolution",
+ "examples": [
+ 60
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/metric-publishers/pom.xml b/metric-publishers/pom.xml
index 5800a26658a0..1878d660e9fc 100644
--- a/metric-publishers/pom.xml
+++ b/metric-publishers/pom.xml
@@ -26,6 +26,7 @@
cloudwatch-metric-publisher
+ emf-metric-logging-publisher
diff --git a/pom.xml b/pom.xml
index 56dd62d53d86..aa9c00d648c9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -144,6 +144,7 @@
9.4.45.v20220203
1.14.15
1.3.0
+ 1.5.4
3.1.2
@@ -655,6 +656,7 @@
netty-nio-client
url-connection-client
cloudwatch-metric-publisher
+ emf-metric-logging-publisher
utils
imds
retries
diff --git a/test/architecture-tests/archunit_store/18fc8858-1308-4d5d-b92d-87817d2fab53 b/test/architecture-tests/archunit_store/18fc8858-1308-4d5d-b92d-87817d2fab53
index 00043c4de219..0e7fc4de658f 100644
--- a/test/architecture-tests/archunit_store/18fc8858-1308-4d5d-b92d-87817d2fab53
+++ b/test/architecture-tests/archunit_store/18fc8858-1308-4d5d-b92d-87817d2fab53
@@ -14,14 +14,12 @@ Class de
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
-Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
-Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
@@ -50,7 +48,6 @@ Class depends on an int
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
-Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
Class depends on an internal API from a different module (Class )
diff --git a/test/architecture-tests/pom.xml b/test/architecture-tests/pom.xml
index 562668933601..8e01fe730df0 100644
--- a/test/architecture-tests/pom.xml
+++ b/test/architecture-tests/pom.xml
@@ -51,6 +51,11 @@
software.amazon.awssdk
${awsjavasdk.version}