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 @@ profiles regions protocols + 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); }