diff --git a/core/metrics-spi/pom.xml b/core/metrics-spi/pom.xml
new file mode 100644
index 000000000000..0a330a249ba9
--- /dev/null
+++ b/core/metrics-spi/pom.xml
@@ -0,0 +1,81 @@
+
+
+
+ core
+ software.amazon.awssdk
+ 2.13.23-SNAPSHOT
+
+ 4.0.0
+
+ metrics-spi
+ AWS Java SDK :: Metrics SPI
+ This is the base module for SDK metrics feature. It contains the interfaces used for metrics feature
+ that are used by other modules in the library.
+
+
+
+
+ software.amazon.awssdk
+ annotations
+ ${awsjavasdk.version}
+
+
+ software.amazon.awssdk
+ utils
+ ${awsjavasdk.version}
+
+
+ software.amazon.awssdk
+ test-utils
+ ${awsjavasdk.version}
+ test
+
+
+ junit
+ junit
+ test
+
+
+ com.github.tomakehurst
+ wiremock
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ software.amazon.awssdk.metrics
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 1.8
+ 1.8
+
+
+
+
+
diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricCategory.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricCategory.java
new file mode 100644
index 000000000000..867d73426556
--- /dev/null
+++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricCategory.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+import software.amazon.awssdk.annotations.SdkPublicApi;
+
+/**
+ * A enum class representing the different types of metric categories in the SDK.
+ *
+ * A metric can be tagged with multiple categories. Clients can enable/disable metric collection
+ * at a {@link MetricCategory} level.
+ */
+@SdkPublicApi
+public enum MetricCategory {
+
+ /**
+ * All metrics defined by the SDK are classified under this category at a minimum. If the metrics feature is enabled
+ * but the category to collect is not, only metrics that are classified under this category are collected by the SDK
+ */
+ DEFAULT("Default"),
+
+ /**
+ * Metrics collected at the http client level are classified under this category.
+ */
+ HTTP_CLIENT("HttpClient"),
+
+ /**
+ * Metrics specific to streaming, eventStream APIs are classified under this category.
+ */
+ STREAMING("Streaming"),
+
+ /**
+ * This is an umbrella category (provided for convenience) that records metrics belonging to every category
+ * defined in this enum. Clients who wish to collect lot of SDK metrics data should use this.
+ *
+ * Note: Enabling this option is verbose and can be expensive based on the platform the metrics are uploaded to.
+ * Please make sure you need all this data before using this category.
+ */
+ ALL("All")
+
+ ;
+
+ private final String value;
+
+ MetricCategory(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Create a {@link MetricCategory} from the given String value. This method is case insensitive.
+ *
+ * @param value the value to create the {@link MetricCategory} from
+ * @return A {@link MetricCategory} if the given {@link #value} matches one of the enum values.
+ * Otherwise throws {@link IllegalArgumentException}
+ */
+ public static MetricCategory fromString(String value) {
+ for (MetricCategory mc : MetricCategory.values()) {
+ if (mc.value.equalsIgnoreCase(value)) {
+ return mc;
+ }
+ }
+
+ throw new IllegalArgumentException("MetricCategory cannot be created from value: " + value);
+ }
+}
diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricCollection.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricCollection.java
new file mode 100644
index 000000000000..581985d70029
--- /dev/null
+++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricCollection.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+import java.util.Collection;
+import java.util.List;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+
+/**
+ * An immutable collection of metrics.
+ */
+@SdkPublicApi
+public interface MetricCollection extends Iterable> {
+ /**
+ * @return The name of this metric collection.
+ */
+ String name();
+
+ /**
+ * Return all the values of the given metric.
+ *
+ * @param metric The metric.
+ * @param The type of the value.
+ * @return All of the values of this metric.
+ */
+ List metricValues(SdkMetric metric);
+
+ /**
+ * @return The child metric collections.
+ */
+ Collection children();
+}
diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricCollector.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricCollector.java
new file mode 100644
index 000000000000..c4599ab37411
--- /dev/null
+++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricCollector.java
@@ -0,0 +1,57 @@
+/*
+ * 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;
+
+import software.amazon.awssdk.annotations.NotThreadSafe;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.metrics.internal.DefaultMetricCollector;
+
+/**
+ * Used to collect metrics reported by the SDK.
+ */
+@NotThreadSafe
+@SdkPublicApi
+public interface MetricCollector {
+ /**
+ * @return The name of this collector.
+ */
+ String name();
+
+ /**
+ * Report a metric.
+ */
+ void reportMetric(SdkMetric metric, T data);
+
+ /**
+ * Create a child of this metric collector.
+ *
+ * @param name The name of the child collector.
+ * @return The child collector.
+ */
+ MetricCollector createChild(String name);
+
+ /**
+ * Return the collected metrics.
+ *
+ * Calling {@code collect()} prevents further invocations of {@link #reportMetric(SdkMetric, Object)}.
+ * @return The collected metrics.
+ */
+ MetricCollection collect();
+
+ static MetricCollector create(String name) {
+ return DefaultMetricCollector.create(name);
+ }
+}
diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricPublisher.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricPublisher.java
new file mode 100644
index 000000000000..8451f63547b6
--- /dev/null
+++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricPublisher.java
@@ -0,0 +1,68 @@
+/*
+ * 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;
+
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.annotations.ThreadSafe;
+import software.amazon.awssdk.metrics.MetricCollection;
+import software.amazon.awssdk.utils.SdkAutoCloseable;
+
+/**
+ * Interface to report and publish the collected SDK metric events to external
+ * sources.
+ *
+ * Conceptually, a publisher receives a stream of {@link MetricCollection} objects
+ * overs its lifetime through its {@link #publish(MetricCollection)} )} method.
+ * Implementations are then free further aggregate these events into sets of
+ * metrics that are then published to some external system for further use.
+ * As long as a publisher is not closed, then it can receive {@code
+ * MetricCollection} objects at any time. In addition, as the SDK makes use of
+ * multithreading, it's possible that the publisher is shared concurrently by
+ * multiple threads, and necessitates that all implementations are threadsafe.
+ *
+ * The SDK may invoke methods on the interface from multiple threads
+ * concurrently so implementations must be threadsafe.
+ */
+@ThreadSafe
+@SdkPublicApi
+public interface MetricPublisher extends SdkAutoCloseable {
+ /**
+ * Notify the publisher of new metric data. After this call returns, the
+ * caller can safely discard the given {@code metricCollection} instance if it
+ * no longer needs it. Implementations are strongly encouraged to complete
+ * any further aggregation and publishing of metrics in an asynchronous manner to
+ * avoid blocking the calling thread.
+ *
+ * With the exception of a {@code null} {@code metricCollection}, all
+ * invocations of this method must return normally. This
+ * is to ensure that callers of the publisher can safely assume that even
+ * in situations where an error happens during publishing that it will not
+ * interrupt the calling thread.
+ *
+ * @param metricCollection The collection of metrics.
+ * @throws IllegalArgumentException If {@code metricCollection} is {@code null}.
+ */
+ void publish(MetricCollection metricCollection);
+
+ /**
+ * {@inheritDoc}
+ *
+ * Important: Implementations must block the calling thread until all
+ * pending metrics are published and any resources acquired have been freed.
+ */
+ @Override
+ void close();
+}
diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricRecord.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricRecord.java
new file mode 100644
index 000000000000..2ec0cbcb5db2
--- /dev/null
+++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricRecord.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+import software.amazon.awssdk.annotations.SdkPublicApi;
+
+/**
+ * A container associating a metric and its value.
+ */
+@SdkPublicApi
+public interface MetricRecord {
+ /**
+ * @return The metric.
+ */
+ SdkMetric metric();
+
+ /**
+ * @return The value of this metric.
+ */
+ T value();
+}
diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/SdkMetric.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/SdkMetric.java
new file mode 100644
index 000000000000..89b25e832fdf
--- /dev/null
+++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/SdkMetric.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+import java.util.Set;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.metrics.internal.DefaultSdkMetric;
+
+/**
+ * A specific SDK metric.
+ *
+ * @param The type for values of this metric.
+ */
+@SdkPublicApi
+public interface SdkMetric {
+
+ /**
+ * @return The name of this metric.
+ */
+ String name();
+
+ /**
+ * @return The categories of this metric.
+ */
+ Set categories();
+
+ /**
+ * @return The class of the value associated with this metric.
+ */
+ Class valueClass();
+
+ /**
+ * Cast the given object to the value class associated with this event.
+ *
+ * @param o The object.
+ * @return The cast object.
+ * @throws ClassCastException If {@code o} is not an instance of type {@code
+ * T}.
+ */
+ T convertValue(Object o);
+
+ /**
+ * Create a new metric.
+ *
+ * @param name The name of this metric.
+ * @param clzz The class of the object containing the associated value for this metric.
+ * @param c1 A category associated with this metric.
+ * @param cn Additional categories associated with this metric.
+ * @param The type of the object containing the associated value for this metric.
+ * @return The created metric.
+ *
+ * @throws IllegalArgumentException If a metric of the same name has already been created.
+ */
+ static SdkMetric create(String name, Class clzz, MetricCategory c1, MetricCategory... cn) {
+ return DefaultSdkMetric.create(name, clzz, c1, cn);
+ }
+
+ /**
+ * Create a new metric.
+ *
+ * @param name The name of this metric.
+ * @param clzz The class of the object containing the associated value for this metric.
+ * @param categories The categories associated with this metric.
+ * @param The type of the object containing the associated value for this metric.
+ * @return The created metric.
+ *
+ * @throws IllegalArgumentException If a metric of the same name has already been created.
+ */
+ static SdkMetric create(String name, Class clzz, Set categories) {
+ return DefaultSdkMetric.create(name, clzz, categories);
+ }
+}
diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/DefaultMetricCollection.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/DefaultMetricCollection.java
new file mode 100644
index 000000000000..e5cf6f9c953d
--- /dev/null
+++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/DefaultMetricCollection.java
@@ -0,0 +1,73 @@
+/*
+ * 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.internal;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.metrics.MetricCollection;
+import software.amazon.awssdk.metrics.MetricRecord;
+import software.amazon.awssdk.metrics.SdkMetric;
+
+@SdkInternalApi
+public final class DefaultMetricCollection implements MetricCollection {
+ private final String name;
+ private final Map, List>> metrics;
+ private final Collection children;
+
+
+ public DefaultMetricCollection(String name, Map,
+ List>> metrics,
+ Collection children) {
+ this.name = name;
+ this.metrics = metrics;
+ this.children = children != null ? Collections.unmodifiableCollection(children) : Collections.emptyList();
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public List metricValues(SdkMetric metric) {
+ if (metrics.containsKey(metric)) {
+ List> metricRecords = metrics.get(metric);
+ List> values = metricRecords.stream()
+ .map(MetricRecord::value)
+ .collect(Collectors.toList());
+ return (List) Collections.unmodifiableList(values);
+ }
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Collection children() {
+ return children;
+ }
+
+ @Override
+ public Iterator> iterator() {
+ return metrics.values().stream()
+ .flatMap(List::stream)
+ .iterator();
+ }
+}
diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/DefaultMetricCollector.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/DefaultMetricCollector.java
new file mode 100644
index 000000000000..85d557c61c7d
--- /dev/null
+++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/DefaultMetricCollector.java
@@ -0,0 +1,84 @@
+/*
+ * 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.internal;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.metrics.MetricCollection;
+import software.amazon.awssdk.metrics.MetricCollector;
+import software.amazon.awssdk.metrics.MetricRecord;
+import software.amazon.awssdk.metrics.SdkMetric;
+import software.amazon.awssdk.utils.Validate;
+
+@SdkInternalApi
+public final class DefaultMetricCollector implements MetricCollector {
+ private final String name;
+ private Map, List>> metrics = new LinkedHashMap<>();
+ private final List children = new ArrayList<>();
+
+ private MetricCollection collection;
+
+ public DefaultMetricCollector(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public void reportMetric(SdkMetric metric, T data) {
+ if (collected()) {
+ throw new IllegalStateException("This collector has already been closed");
+ }
+ metrics.computeIfAbsent(metric, (m) -> new ArrayList<>())
+ .add(new DefaultMetricRecord<>(metric, data));
+ }
+
+ @Override
+ public MetricCollector createChild(String name) {
+ MetricCollector child = new DefaultMetricCollector(name);
+ children.add(child);
+ return child;
+ }
+
+ @Override
+ public MetricCollection collect() {
+ if (!collected()) {
+ List collectedChildren = children.stream()
+ .map(MetricCollector::collect)
+ .collect(Collectors.toList());
+
+ collection = new DefaultMetricCollection(name, metrics, collectedChildren);
+ }
+
+ return collection;
+ }
+
+ public static MetricCollector create(String name) {
+ Validate.notEmpty(name, "name");
+ return new DefaultMetricCollector(name);
+ }
+
+ private boolean collected() {
+ return collection != null;
+ }
+}
diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/DefaultMetricRecord.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/DefaultMetricRecord.java
new file mode 100644
index 000000000000..24511d065a06
--- /dev/null
+++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/DefaultMetricRecord.java
@@ -0,0 +1,41 @@
+/*
+ * 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.internal;
+
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.metrics.MetricRecord;
+import software.amazon.awssdk.metrics.SdkMetric;
+
+@SdkInternalApi
+public final class DefaultMetricRecord implements MetricRecord {
+ private final SdkMetric metric;
+ private final T value;
+
+ public DefaultMetricRecord(SdkMetric metric, T value) {
+ this.metric = metric;
+ this.value = value;
+ }
+
+ @Override
+ public SdkMetric metric() {
+ return metric;
+ }
+
+ @Override
+ public T value() {
+ return value;
+ }
+}
diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/DefaultSdkMetric.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/DefaultSdkMetric.java
new file mode 100644
index 000000000000..106ed7045e0e
--- /dev/null
+++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/DefaultSdkMetric.java
@@ -0,0 +1,138 @@
+/*
+ * 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.internal;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.annotations.SdkTestInternalApi;
+import software.amazon.awssdk.metrics.MetricCategory;
+import software.amazon.awssdk.metrics.SdkMetric;
+import software.amazon.awssdk.utils.AttributeMap;
+import software.amazon.awssdk.utils.Validate;
+
+@SdkInternalApi
+public final class DefaultSdkMetric extends AttributeMap.Key implements SdkMetric {
+ private static final ConcurrentHashMap, Boolean> SDK_METRICS = new ConcurrentHashMap<>();
+
+ private final String name;
+ private final Class clzz;
+ private final Set categories;
+
+ private DefaultSdkMetric(String name, Class clzz, Set categories) {
+ super(clzz);
+ this.name = Validate.notBlank(name, "name must not be blank");
+ this.clzz = Validate.notNull(clzz, "clzz must not be null");
+ Validate.notEmpty(categories, "categories must not be empty");
+ this.categories = EnumSet.copyOf(categories);
+ }
+
+ /**
+ * @return The name of this event.
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * @return The categories of this event.
+ */
+ public Set categories() {
+ return Collections.unmodifiableSet(categories);
+ }
+
+ /**
+ * @return The class of the value associated with this event.
+ */
+ public Class valueClass() {
+ return clzz;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ DefaultSdkMetric> that = (DefaultSdkMetric>) o;
+
+ return name.equals(that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ /**
+ * Create a new metric.
+ *
+ * @param name The name of this metric.
+ * @param clzz The class of the object containing the associated value for this metric.
+ * @param c1 A category associated with this metric.
+ * @param cn Additional categories associated with this metric.
+ * @param The type of the object containing the associated value for this metric.
+ * @return The created metric.
+ *
+ * @throws IllegalArgumentException If a metric of the same name has already been created.
+ */
+ public static SdkMetric create(String name, Class clzz, MetricCategory c1, MetricCategory... cn) {
+ Stream categoryStream = Stream.of(c1);
+ if (cn != null) {
+ categoryStream = Stream.concat(categoryStream, Stream.of(cn));
+ }
+ Set categories = categoryStream.collect(Collectors.toSet());
+ return create(name, clzz, categories);
+ }
+
+ /**
+ * Create a new metric.
+ *
+ * @param name The name of this metric.
+ * @param clzz The class of the object containing the associated value for this metric.
+ * @param categories The categories associated with this metric.
+ * @param The type of the object containing the associated value for this metric.
+ * @return The created metric.
+ *
+ * @throws IllegalArgumentException If a metric of the same name has already been created.
+ */
+ public static SdkMetric create(String name, Class clzz, Set categories) {
+ Validate.noNullElements(categories, "categories must not contain null elements");
+ SdkMetric event = new DefaultSdkMetric<>(name, clzz, categories);
+ if (SDK_METRICS.putIfAbsent(event, Boolean.TRUE) != null) {
+ throw new IllegalArgumentException("Metric with name " + name + " has already been created");
+ }
+ return event;
+ }
+
+ @SdkTestInternalApi
+ static void clearDeclaredMetrics() {
+ SDK_METRICS.clear();
+ }
+
+ @SdkTestInternalApi
+ static Set> declaredEvents() {
+ return SDK_METRICS.keySet();
+ }
+}
diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/util/MetricUtil.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/util/MetricUtil.java
new file mode 100644
index 000000000000..b97e4b38a8ff
--- /dev/null
+++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/util/MetricUtil.java
@@ -0,0 +1,44 @@
+/*
+ * 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.internal.util;
+
+import java.time.Duration;
+import java.util.concurrent.Callable;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.utils.Pair;
+
+@SdkInternalApi
+public final class MetricUtil {
+
+ private MetricUtil() {
+ }
+
+ public static Pair measureDuration(Callable c) {
+ long start = System.nanoTime();
+
+ T result;
+
+ try {
+ result = c.call();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ Duration d = Duration.ofNanos(System.nanoTime() - start);
+
+ return Pair.of(result, d);
+ }
+}
diff --git a/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/internal/DefaultMetricCollectionTest.java b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/internal/DefaultMetricCollectionTest.java
new file mode 100644
index 000000000000..0eb28a7e56b9
--- /dev/null
+++ b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/internal/DefaultMetricCollectionTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import org.junit.AfterClass;
+import org.junit.Test;
+import software.amazon.awssdk.metrics.MetricCategory;
+import software.amazon.awssdk.metrics.MetricRecord;
+import software.amazon.awssdk.metrics.SdkMetric;
+
+public class DefaultMetricCollectionTest {
+ private static final SdkMetric M1 = SdkMetric.create("m1", Integer.class, MetricCategory.DEFAULT);
+
+ @AfterClass
+ public static void teardown() {
+ DefaultSdkMetric.clearDeclaredMetrics();
+ }
+
+ @Test
+ public void testMetricValues_noValues_returnsEmptyList() {
+ DefaultMetricCollection foo = new DefaultMetricCollection("foo", Collections.emptyMap(), Collections.emptyList());
+ assertThat(foo.metricValues(M1)).isEmpty();
+ }
+
+ @Test
+ public void testChildren_noChildren_returnsEmptyList() {
+ DefaultMetricCollection foo = new DefaultMetricCollection("foo", Collections.emptyMap(), Collections.emptyList());
+ assertThat(foo.children()).isEmpty();
+ }
+
+ @Test
+ public void testIterator_iteratesOverAllValues() {
+ Integer[] values = {1, 2, 3};
+ Map, List>> recordMap = new HashMap<>();
+ List> records = Stream.of(values).map(v -> new DefaultMetricRecord<>(M1, v)).collect(Collectors.toList());
+ recordMap.put(M1, records);
+
+ DefaultMetricCollection collection = new DefaultMetricCollection("foo", recordMap, Collections.emptyList());
+ final Set iteratorValues = StreamSupport.stream(collection.spliterator(), false)
+ .map(MetricRecord::value)
+ .map(Integer.class::cast)
+ .collect(Collectors.toSet());
+
+ assertThat(iteratorValues).containsExactly(values);
+ }
+}
diff --git a/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/internal/DefaultMetricCollectorTest.java b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/internal/DefaultMetricCollectorTest.java
new file mode 100644
index 000000000000..2125a6233b3a
--- /dev/null
+++ b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/internal/DefaultMetricCollectorTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import java.util.stream.Stream;
+import org.junit.AfterClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import software.amazon.awssdk.metrics.MetricCategory;
+import software.amazon.awssdk.metrics.MetricCollection;
+import software.amazon.awssdk.metrics.MetricCollector;
+import software.amazon.awssdk.metrics.SdkMetric;
+
+public class DefaultMetricCollectorTest {
+ private static final SdkMetric M1 = SdkMetric.create("m1", Integer.class, MetricCategory.DEFAULT);
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @AfterClass
+ public static void teardown() {
+ DefaultSdkMetric.clearDeclaredMetrics();
+ }
+
+ @Test
+ public void testName_returnsName() {
+ MetricCollector collector = MetricCollector.create("collector");
+ assertThat(collector.name()).isEqualTo("collector");
+ }
+
+ @Test
+ public void testCreateChild_returnsChildWithCorrectName() {
+ MetricCollector parent = MetricCollector.create("parent");
+ MetricCollector child = parent.createChild("child");
+
+ assertThat(child.name()).isEqualTo("child");
+ }
+
+ @Test
+ public void testCollect_allReportedMetricsInCollection() {
+ MetricCollector collector = MetricCollector.create("collector");
+ Integer[] values = {1, 2, 3};
+ Stream.of(values).forEach(v -> collector.reportMetric(M1, v));
+ MetricCollection collect = collector.collect();
+ assertThat(collect.metricValues(M1)).containsExactly(values);
+ }
+
+ @Test
+ public void testCollect_returnedCollectionContainsAllChildren() {
+ MetricCollector parent = MetricCollector.create("parent");
+ String[] childNames = {"c1", "c2", "c3" };
+ Stream.of(childNames).forEach(parent::createChild);
+ MetricCollection collected = parent.collect();
+ assertThat(collected.children().stream().map(MetricCollection::name)).containsExactly(childNames);
+ }
+
+ @Test
+ public void testReportMetric_collected_throws() {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("This collector has already been closed");
+
+ MetricCollector collector = MetricCollector.create("collector");
+ collector.collect();
+ collector.reportMetric(M1, 42);
+ }
+}
diff --git a/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/internal/DefaultSdkMetricRecordTest.java b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/internal/DefaultSdkMetricRecordTest.java
new file mode 100644
index 000000000000..fc64e6b64190
--- /dev/null
+++ b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/internal/DefaultSdkMetricRecordTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.Test;
+import software.amazon.awssdk.metrics.MetricCategory;
+import software.amazon.awssdk.metrics.SdkMetric;
+import software.amazon.awssdk.metrics.MetricRecord;
+
+/**
+ * Tests for {@link DefaultMetricRecord}.
+ */
+public class DefaultSdkMetricRecordTest {
+ @Test
+ public void testGetters() {
+ SdkMetric event = SdkMetric.create("foo", Integer.class, MetricCategory.DEFAULT);
+
+ MetricRecord record = new DefaultMetricRecord<>(event, 2);
+
+ assertThat(record.metric()).isEqualTo(event);
+ assertThat(record.value()).isEqualTo(2);
+ }
+}
diff --git a/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/internal/DefaultSdkMetricTest.java b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/internal/DefaultSdkMetricTest.java
new file mode 100644
index 000000000000..003e7b716ffc
--- /dev/null
+++ b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/internal/DefaultSdkMetricTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import software.amazon.awssdk.metrics.MetricCategory;
+import software.amazon.awssdk.metrics.SdkMetric;
+
+public class DefaultSdkMetricTest {
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Before
+ public void methodSetup() {
+ DefaultSdkMetric.clearDeclaredMetrics();
+ }
+
+ @Test
+ public void testOf_variadicOverload_createdProperly() {
+ SdkMetric event = SdkMetric.create("event", Integer.class, MetricCategory.DEFAULT);
+
+ assertThat(event.categories()).containsExactly(MetricCategory.DEFAULT);
+ assertThat(event.name()).isEqualTo("event");
+ assertThat(event.valueClass()).isEqualTo(Integer.class);
+ }
+
+ @Test
+ public void testOf_setOverload_createdProperly() {
+ SdkMetric event = SdkMetric.create("event", Integer.class, Stream.of(MetricCategory.DEFAULT)
+ .collect(Collectors.toSet()));
+
+ assertThat(event.categories()).containsExactly(MetricCategory.DEFAULT);
+ assertThat(event.name()).isEqualTo("event");
+ assertThat(event.valueClass()).isEqualTo(Integer.class);
+ }
+
+ @Test
+ public void testOf_variadicOverload_c1Null_throws() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("must not contain null elements");
+ SdkMetric.create("event", Integer.class, (MetricCategory) null);
+ }
+
+ @Test
+ public void testOf_variadicOverload_c1NotNull_cnNull_doesNotThrow() {
+ SdkMetric.create("event", Integer.class, MetricCategory.DEFAULT, null);
+ }
+
+ @Test
+ public void testOf_variadicOverload_cnContainsNull_throws() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("must not contain null elements");
+ SdkMetric.create("event", Integer.class, MetricCategory.DEFAULT, new MetricCategory[]{ null });
+ }
+
+ @Test
+ public void testOf_setOverload_null_throws() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("object is null");
+ SdkMetric.create("event", Integer.class, (Set) null);
+ }
+
+ @Test
+ public void testOf_setOverload_nullElement_throws() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("categories must not contain null elements");
+ SdkMetric.create("event", Integer.class, Stream.of((MetricCategory) null).collect(Collectors.toSet()));
+ }
+
+ @Test
+ public void testOf_namePreviouslyUsed_throws() {
+ String fooName = "metricEvent";
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage(fooName + " has already been created");
+
+ SdkMetric.create(fooName, Integer.class, MetricCategory.DEFAULT);
+ SdkMetric.create(fooName, Integer.class, MetricCategory.DEFAULT);
+ }
+
+ @Test
+ public void testOf_namePreviouslyUsed_differentArgs_throws() {
+ String fooName = "metricEvent";
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage(fooName + " has already been created");
+
+ SdkMetric.create(fooName, Integer.class, MetricCategory.DEFAULT);
+ SdkMetric.create(fooName, Long.class, MetricCategory.STREAMING);
+ }
+
+ @Test
+ public void testOf_namePreviouslyUsed_doesNotReplaceExisting() {
+ String fooName = "fooMetric";
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage(fooName + " has already been created");
+
+ SdkMetric.create(fooName, Integer.class, MetricCategory.DEFAULT);
+ try {
+ SdkMetric.create(fooName, Long.class, MetricCategory.STREAMING);
+ } finally {
+ SdkMetric> fooMetric = DefaultSdkMetric.declaredEvents()
+ .stream()
+ .filter(e -> e.name().equals(fooName))
+ .findFirst()
+ .get();
+
+ assertThat(fooMetric.name()).isEqualTo(fooName);
+ assertThat(fooMetric.valueClass()).isEqualTo(Integer.class);
+ assertThat(fooMetric.categories()).containsExactly(MetricCategory.DEFAULT);
+ }
+ }
+}
diff --git a/core/pom.xml b/core/pom.xml
index effab355ac8d..fabeffc943f3 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -41,6 +41,7 @@
profilesregionsprotocols
+ metrics-spi
diff --git a/docs/design/core/metrics/prototype/SdkMetric.java b/docs/design/core/metrics/prototype/SdkMetric.java
index 7aaf41565034..12fad806fb72 100644
--- a/docs/design/core/metrics/prototype/SdkMetric.java
+++ b/docs/design/core/metrics/prototype/SdkMetric.java
@@ -18,6 +18,7 @@
*
* @param The type for values of this metric.
*/
+@SdkPublicApi
public interface SdkMetric {
/**
@@ -43,5 +44,5 @@ public interface SdkMetric {
* @throws ClassCastException If {@code o} is not an instance of type {@code
* T}.
*/
- public T convertToType(Object o);
+ public T convertValue(Object o);
}