diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtils.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtils.java index b690c0fe15d0..d329a3e5210d 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtils.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtils.java @@ -40,6 +40,7 @@ import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.ExecutionInterceptorChain; import software.amazon.awssdk.core.interceptor.InterceptorContext; +import software.amazon.awssdk.core.interceptor.MetricExecutionAttribute; import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; import software.amazon.awssdk.core.signer.Signer; @@ -84,6 +85,10 @@ static ExecutionContext .putAttribute(SdkExecutionAttribute.CLIENT_TYPE, clientConfig.option(SdkClientOption.CLIENT_TYPE)) .putAttribute(SdkExecutionAttribute.SERVICE_NAME, clientConfig.option(SdkClientOption.SERVICE_NAME)) .putAttribute(SdkExecutionAttribute.OPERATION_NAME, executionParams.getOperationName()) + .putAttribute(MetricExecutionAttribute.METRIC_CONFIGURATION_PROVIDER, + clientConfig.option(SdkClientOption.METRIC_CONFIGURATION_PROVIDER)) + .putAttribute(MetricExecutionAttribute.METRIC_PUBLISHER_CONFIGURATION, + clientConfig.option(SdkClientOption.METRIC_PUBLISHER_CONFIGURATION)) .putAttribute(SdkExecutionAttribute.ENDPOINT_OVERRIDDEN, clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN)); diff --git a/core/metrics-spi/pom.xml b/core/metrics-spi/pom.xml new file mode 100644 index 000000000000..c208cbb97045 --- /dev/null +++ b/core/metrics-spi/pom.xml @@ -0,0 +1,82 @@ + + + + core + software.amazon.awssdk + 2.10.46-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..07980f95b3cf --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/MetricCategory.java @@ -0,0 +1,85 @@ +/* + * Copyright 2010-2020 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.metrics.metrics.SdkDefaultMetric; + +/** + * 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. + * + * @see SdkDefaultMetric + */ +@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/internal/MetricSystemSetting.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/MetricSystemSetting.java new file mode 100644 index 000000000000..c494ab953879 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/internal/MetricSystemSetting.java @@ -0,0 +1,69 @@ +/* + * Copyright 2010-2020 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.MetricCategory; +import software.amazon.awssdk.utils.SystemSetting; + +/** + * System properties to configure metrics options in the SDK. + */ +@SdkInternalApi +public enum MetricSystemSetting implements SystemSetting { + + /** + * Enable the Java SDK Metrics by setting this property. Metrics feature is disabled if this feature is not specified or + * specified to anything other than "true" + */ + AWS_JAVA_SDK_METRICS_ENABLED("aws.javasdk2x.metrics.enabled", null), + + /** + * Specify comma separated {@link MetricCategory} values to enable the categories for metrics collection. + * Only metrics belonging to these categories are collected by the SDK. + * + *

+ * This value is defaulted to {@link MetricCategory#DEFAULT}. If this property is not set but metrics are enabled, + * then metrics belonging to {@link MetricCategory#DEFAULT} category are collected. + *

+ */ + AWS_JAVA_SDK_METRICS_CATEGORY("aws.javasdk2x.metrics.category", "Default") + + ; + + private final String systemProperty; + private final String defaultValue; + + MetricSystemSetting(String systemProperty, String defaultValue) { + this.systemProperty = systemProperty; + this.defaultValue = defaultValue; + } + + @Override + public String property() { + return systemProperty; + } + + @Override + public String environmentVariable() { + return name(); + } + + @Override + public String defaultValue() { + return defaultValue; + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/ConstantGauge.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/ConstantGauge.java new file mode 100644 index 000000000000..7c3c18f1054b --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/ConstantGauge.java @@ -0,0 +1,105 @@ +/* + * Copyright 2010-2020 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.meter; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.utils.Validate; + +/** + * A {@link Gauge} implementation that stores a constant value for a metric. + * The value stored cannot be changed after object creation + * + * @param the type of the value recorded in the Gauge + */ +@SdkPublicApi +public final class ConstantGauge implements Gauge { + + private final TypeT value; + private final Set categories; + + private ConstantGauge(Builder builder) { + this.value = Validate.notNull(builder.value, "Value cannot be null"); + this.categories = Collections.unmodifiableSet(builder.categories); + } + + @Override + public TypeT value() { + return value; + } + + @Override + public Set categories() { + return categories; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * @param value the value to store in the guage + * @param type of the value + * @return An instance of {@link ConstantGauge} with the given {@link #value} stored in the gauge. + */ + public static ConstantGauge create(T value) { + return builder().value(value).build(); + } + + public static final class Builder { + private BuilderT value; + private final Set categories = new HashSet<>(); + + private Builder() { + } + + /** + * @param value the value to store in the gauge + * @return This object for method chaining + */ + public Builder value(BuilderT value) { + this.value = value; + return this; + } + + /** + * Register the given categories in this metric + * @param categories the set of {@link MetricCategory} this metric belongs to + * @return This object for method chaining + */ + public Builder categories(Set categories) { + this.categories.addAll(categories); + return this; + } + + /** + * Register the given {@link MetricCategory} in this metric + * @param category the {@link MetricCategory} to tag the metric with + * @return This object for method chaining + */ + public Builder addCategory(MetricCategory category) { + this.categories.add(category); + return this; + } + + public ConstantGauge build() { + return new ConstantGauge(this); + } + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Counter.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Counter.java new file mode 100644 index 000000000000..d48bfd81b771 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Counter.java @@ -0,0 +1,49 @@ +/* + * Copyright 2010-2020 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.meter; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * A metric type to store incrementing and decrementing values + * + * @param type of the stored value + */ +@SdkPublicApi +public interface Counter extends Metric, Counting { + + /** + * Increment the metric value by 1 unit + */ + void increment(); + + /** + * Increment the metric value by given #value amount + * @param value amount to increment the metric value + */ + void increment(T value); + + /** + * Decrement the metric value by 1 unit + */ + void decrement(); + + /** + * Decrement the metric value by given #value amount + * @param value amount to decrement the metric value + */ + void decrement(T value); +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Counting.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Counting.java new file mode 100644 index 000000000000..20b31a8e18d0 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Counting.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2020 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.meter; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * An interface for metric types which have counts + * @param type of the count value + */ +@SdkPublicApi +public interface Counting { + + /** + * Returns the current count. + * + * @return the current count + */ + T count(); +} + diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/DefaultGauge.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/DefaultGauge.java new file mode 100644 index 000000000000..a7e639a78d01 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/DefaultGauge.java @@ -0,0 +1,112 @@ +/* + * Copyright 2010-2020 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.meter; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.metrics.MetricCategory; + +/** + * A basic implementation of {@link Gauge} that has ability to set, update and return a single value. + * + * @param type of the value stored in gauge + */ +@SdkPublicApi +public final class DefaultGauge implements Gauge { + + private final AtomicReference atomicReference; + private final Set categories; + + private DefaultGauge(Builder builder) { + this.atomicReference = new AtomicReference<>(builder.value); + this.categories = Collections.unmodifiableSet(builder.categories); + } + + /** + * Update the value stored in the gauge + * @param value the new value to store in the gauge + */ + public void value(TypeT value) { + this.atomicReference.set(value); + } + + @Override + public TypeT value() { + return atomicReference.get(); + } + + @Override + public Set categories() { + return categories; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * @param initialValue the value to stored in the gauge instance when its created + * @param type of the value + * @return A new instance of {@link DefaultGauge} with the given #initialValue stored in the gauge. + */ + public static DefaultGauge create(T initialValue) { + return builder().value(initialValue).build(); + } + + public static final class Builder { + private BuilderT value; + private final Set categories = new HashSet<>(); + + private Builder() { + } + + /** + * @param value the initial value to store in the gauge + * @return This object for method chaining + */ + public Builder value(BuilderT value) { + this.value = value; + return this; + } + + /** + * Register the given categories in this metric + * @param categories the set of {@link MetricCategory} this metric belongs to + * @return This object for method chaining + */ + public Builder categories(Set categories) { + this.categories.addAll(categories); + return this; + } + + /** + * Register the given {@link MetricCategory} in this metric + * @param category the {@link MetricCategory} to tag the metric with + * @return This object for method chaining + */ + public Builder addCategory(MetricCategory category) { + this.categories.add(category); + return this; + } + + public DefaultGauge build() { + return new DefaultGauge(this); + } + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/DefaultTimer.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/DefaultTimer.java new file mode 100644 index 000000000000..24573b5a646f --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/DefaultTimer.java @@ -0,0 +1,171 @@ +/* + * Copyright 2010-2020 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.meter; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.utils.Validate; + +/** + * A default implementation of {@link Timer} metric interface. + */ +@SdkPublicApi +public final class DefaultTimer implements Timer { + + private final Clock clock; + private Instant startTime; + private Instant endTime; + private final Set categories; + + private DefaultTimer(Builder builder) { + this.clock = Validate.notNull(builder.clock, "Clock"); + this.startTime = clock.instant(); + this.endTime = clock.instant(); + this.categories = Collections.unmodifiableSet(builder.categories); + } + + @Override + public void record(long duration, TimeUnit timeUnit) { + if (duration <= 0) { + throw new IllegalArgumentException("Duration cannot be negative"); + } + + this.endTime.plusNanos(TimeUnit.NANOSECONDS.convert(duration, timeUnit)); + } + + @Override + public void record(Runnable task) { + startTime = clock.instant(); + try { + task.run(); + } finally { + endTime = clock.instant(); + } + } + + @Override + public T record(Callable task) { + startTime = clock.instant(); + try { + return task.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + endTime = clock.instant(); + } + } + + @Override + public void start() { + startTime = clock.instant(); + } + + @Override + public void end() { + endTime = clock.instant(); + } + + @Override + public Instant startTime() { + return startTime; + } + + @Override + public Instant endTime() { + validateEndTime(); + return endTime; + } + + @Override + public Duration totalTime() { + validateEndTime(); + return Duration.between(startTime, endTime); + } + + @Override + public Set categories() { + return categories; + } + + private void validateEndTime() { + if (endTime == null) { + throw new IllegalStateException("End time was never updated. You need to call one of the " + + "record() methods to set the end time"); + } + } + + /** + * @return a new {@link Builder} instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class to create instances of {@link DefaultTimer} + */ + public static final class Builder { + + private Clock clock = Clock.systemUTC(); + private final Set categories = new HashSet<>(); + + private Builder() { + } + + /** + * Sets the {@link Clock} implementation the timer should use. The default value is {@link Clock#systemUTC()} + * + * @param clock the {@link Clock} implementation used by the timer + * @return This object for method chaining + */ + Builder clock(Clock clock) { + this.clock = clock; + return this; + } + + /** + * Register the given categories in this metric + * @param categories the set of {@link MetricCategory} this metric belongs to + * @return This object for method chaining + */ + public Builder categories(Set categories) { + this.categories.addAll(categories); + return this; + } + + /** + * Register the given {@link MetricCategory} in this metric + * @param category the {@link MetricCategory} to tag the metric with + * @return This object for method chaining + */ + public Builder addCategory(MetricCategory category) { + this.categories.add(category); + return this; + } + + public DefaultTimer build() { + return new DefaultTimer(this); + } + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Gauge.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Gauge.java new file mode 100644 index 000000000000..5efa2ec32ccf --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Gauge.java @@ -0,0 +1,33 @@ +/* + * Copyright 2010-2020 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.meter; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * A gauge metric is an instantaneous value recorded at a point in time. + * + * @param the type of the value recorded in the Gauge + */ +@FunctionalInterface +@SdkPublicApi +public interface Gauge extends Metric { + + /** + * @return the current value of the gauge metric + */ + T value(); +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/LongCounter.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/LongCounter.java new file mode 100644 index 000000000000..3b3dbed676bf --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/LongCounter.java @@ -0,0 +1,113 @@ +/* + * Copyright 2010-2020 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.meter; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.LongAdder; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.utils.Validate; + +/** + * A {@link Counter} implementation that stores {@link Long} values. + */ +@SdkPublicApi +public final class LongCounter implements Counter { + + private final LongAdder count; + private final Set categories; + + private LongCounter(Builder builder) { + this.count = new LongAdder(); + this.categories = Collections.unmodifiableSet(builder.categories); + } + + @Override + public void increment() { + increment(1L); + } + + @Override + public void increment(Long value) { + Validate.isNotNegative(value, "Value should not be negative. Use decrement(...) method to reduce count."); + count.add(value); + } + + @Override + public void decrement() { + decrement(1L); + } + + @Override + public void decrement(Long value) { + Validate.isNotNegative(value, "Value should be positive. Use increment(...) method to increase count."); + count.add(-value); + } + + @Override + public Long count() { + return count.sum(); + } + + @Override + public Set categories() { + return categories; + } + + public static Builder builder() { + return new Builder(); + } + + public static LongCounter create() { + return builder().build(); + } + + /** + * Builder class to create instances of {@link LongCounter} + */ + public static final class Builder { + private final Set categories = new HashSet<>(); + + private Builder() { + } + + /** + * Register the given categories in this metric + * @param categories the set of {@link MetricCategory} this metric belongs to + * @return This object for method chaining + */ + public Builder categories(Set categories) { + this.categories.addAll(categories); + return this; + } + + /** + * Register the given {@link MetricCategory} in this metric + * @param category the {@link MetricCategory} to tag the metric with + * @return This object for method chaining + */ + public Builder addCategory(MetricCategory category) { + this.categories.add(category); + return this; + } + + public LongCounter build() { + return new LongCounter(this); + } + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Metric.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Metric.java new file mode 100644 index 000000000000..e9474219195f --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Metric.java @@ -0,0 +1,35 @@ +/* + * Copyright 2010-2020 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.meter; + +import java.util.Collections; +import java.util.Set; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.metrics.MetricCategory; + +/** + * Base interface to represent a metric + */ +@SdkPublicApi +public interface Metric { + + /** + * @return the set of {@link MetricCategory} that this metric belongs to + */ + default Set categories() { + return Collections.emptySet(); + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/NoOpCounter.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/NoOpCounter.java new file mode 100644 index 000000000000..78a1c9478236 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/NoOpCounter.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010-2020 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.meter; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * A NoOp implementation of the {@link Counter} metric + */ +@SdkPublicApi +public final class NoOpCounter implements Counter { + private static final NoOpCounter INSTANCE = new NoOpCounter(); + + private NoOpCounter() { + } + + @Override + public void increment() { + + } + + @Override + public void increment(Number value) { + + } + + @Override + public void decrement() { + + } + + @Override + public void decrement(Number value) { + + } + + @Override + public Number count() { + return 0; + } + + public static NoOpCounter instance() { + return INSTANCE; + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/NoOpGauge.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/NoOpGauge.java new file mode 100644 index 000000000000..56b20a70744d --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/NoOpGauge.java @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2020 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.meter; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * A NoOp implementation of the {@link Gauge} metric + */ +@SdkPublicApi +public final class NoOpGauge implements Gauge { + private static final NoOpGauge INSTANCE = new NoOpGauge(); + private static final Object EMPTY_OBJECT = new Object(); + + private NoOpGauge() { + } + + @Override + public Object value() { + return EMPTY_OBJECT; + } + + public static NoOpGauge instance() { + return INSTANCE; + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/NoOpTimer.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/NoOpTimer.java new file mode 100644 index 000000000000..ec3cdff614f4 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/NoOpTimer.java @@ -0,0 +1,76 @@ +/* + * Copyright 2010-2020 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.meter; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * A NoOp implementation of the {@link Timer} metric. + *

+ * Always returns {@link Duration#ZERO} as the total duration. + */ +@SdkPublicApi +public final class NoOpTimer implements Timer { + private static final NoOpTimer INSTANCE = new NoOpTimer(); + + private NoOpTimer() { + } + + @Override + public void record(long duration, TimeUnit timeUnit) { + } + + @Override + public void record(Runnable task) { + task.run(); + } + + @Override + public T record(Callable task) throws Exception { + return task.call(); + } + + @Override + public void start() { + } + + @Override + public void end() { + } + + @Override + public Instant startTime() { + return Instant.now(); + } + + @Override + public Instant endTime() { + return Instant.now(); + } + + @Override + public Duration totalTime() { + return Duration.ZERO; + } + + public static NoOpTimer instance() { + return INSTANCE; + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Timer.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Timer.java new file mode 100644 index 000000000000..9289572b7f96 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/meter/Timer.java @@ -0,0 +1,81 @@ +/* + * Copyright 2010-2020 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.meter; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * Type of {@link Metric} to record the timing information. This is used to record start time, end time of tasks + * and compute the latency. + */ +@SdkPublicApi +public interface Timer extends Metric { + + /** + * Adds the given duration to the recorded timing info. + * + * Calling this method will update the result of {@link #endTime()} and {@link #totalTime()} methods. + * + * @param duration Duration of the event to be added to the timer + * @param timeUnit Time unit of the duration + */ + void record(long duration, TimeUnit timeUnit); + + /** + * Runs the given runnable task and records the duration. + * @param task the task the run + */ + void record(Runnable task); + + /** + * Runs the given callable task and records the duration. + * + * @param task The task to call + * @param Return type of the Callable result + * @return Result of the Callable task + */ + T record(Callable task) throws Exception; + + /** + * Record the current time to indicate start of an action. This affects the {@link #startTime()} in the timer. + */ + void start(); + + /** + * Record the current time to indicate the end of a previously started action. + * This affects the {@link #endTime()} ()} in the timer. + */ + void end(); + + /** + * @return the start time recorded in the Timer + */ + Instant startTime(); + + /** + * @return the end time recorded in the Timer + */ + Instant endTime(); + + /** + * @return the latency stored in the Timer. This is the difference between {@link #endTime()} and {@link #startTime()}. + */ + Duration totalTime(); +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/metrics/SdkDefaultMetric.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/metrics/SdkDefaultMetric.java new file mode 100644 index 000000000000..d761948d550c --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/metrics/SdkDefaultMetric.java @@ -0,0 +1,124 @@ +/* + * Copyright 2010-2020 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.metrics; + +import static software.amazon.awssdk.metrics.MetricCategory.DEFAULT; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.metrics.meter.Metric; +import software.amazon.awssdk.utils.ToString; + +/** + * A enum representing the metrics supported by the SDK. All metrics collected by the SDK will be declared in this + * enum class along with the metric's corresponding {@link MetricCategory}. + */ +// TODO List is not complete +@SdkPublicApi +public enum SdkDefaultMetric implements Metric { + + /** + * Service ID of the AWS service that the API request is made against + */ + SERVICE("Service", DEFAULT), + + /** + * The name of the AWS operation the request is made to + */ + OPERATION("Operation", DEFAULT), + + /** + * The total time taken to finish (success or fail) a request (inclusive of all retries) + */ + API_CALL("ApiCallLatency", DEFAULT), + + /** + * The time taken to marshall the request + */ + MARSHALLING_LATENCY("MarshallingLatency", DEFAULT), + + /** + * Total number of attempts that were made by the service client to fulfill this request before succeeding or failing. + * (This value would be 1 if there are no retries) + */ + API_CALL_ATTEMPT_COUNT("ApiCallAttemptCount", DEFAULT), + + /** + * The time taken to sign the request + */ + SIGNING_LATENCY("SigningLatency", DEFAULT), + + /** + * The time taken by the underlying http client to start the Operation call attempt and return the response + */ + HTTP_REQUEST_ROUND_TRIP_LATENCY("HttpRequestRoundTripLatency", DEFAULT), + + /** + * The time taken to unmarshall the response (either successful and failed response) + */ + UNMARSHALLING_LATENCY("UnmarshallingLatency", DEFAULT), + + /** + * The total time taken for an Operation call attempt + */ + API_CALL_ATTEMPT_LATENCY("ApiCallAttemptLatency", DEFAULT), + + /** + * The http status code returned in the response + */ + HTTP_STATUS_CODE("HttpStatusCode", DEFAULT), + + /** + * The request Id for the request. Represented by x-amz-request-id header in response + */ + AWS_REQUEST_ID("AwsRequestId", DEFAULT), + + /** + * The extended request Id for the request. Represented by x-amz-id-2 header in response + */ + EXTENDED_REQUEST_ID("ExtendedRequestId", DEFAULT), + + ; + + private final String value; + + private final Set categories; + + SdkDefaultMetric(String value, MetricCategory... categories) { + this.value = value; + this.categories = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(categories))); + } + + public String value() { + return value; + } + + @Override + public Set categories() { + return categories; + } + + @Override + public String toString() { + return ToString.builder(value) + .add("categories", categories) + .build(); + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/DefaultMetricConfigurationProvider.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/DefaultMetricConfigurationProvider.java new file mode 100644 index 000000000000..05d8a2d8d5a7 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/DefaultMetricConfigurationProvider.java @@ -0,0 +1,107 @@ +/* + * Copyright 2010-2020 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.provider; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.utils.CollectionUtils; + +/** + * The default implementation of {@link MetricConfigurationProvider} interface. + */ +@SdkPublicApi +public final class DefaultMetricConfigurationProvider implements MetricConfigurationProvider { + + private final boolean enabled; + private final Set metricCategories; + + private DefaultMetricConfigurationProvider(Builder builder) { + this.enabled = builder.enabled; + this.metricCategories = Collections.unmodifiableSet(resolveCategories(builder.categories)); + } + + @Override + public boolean enabled() { + return enabled; + } + + @Override + public Set metricCategories() { + return metricCategories; + } + + /** + * @return a new {@link Builder} instance + */ + public static Builder builder() { + return new Builder(); + } + + private Set resolveCategories(Set categories) { + if (CollectionUtils.isNullOrEmpty(categories)) { + return new HashSet<>(Arrays.asList(MetricCategory.DEFAULT)); + } + + return categories; + } + + public static final class Builder { + // Metrics are disabled by default + private boolean enabled = false; + + private final Set categories = new HashSet<>(); + + private Builder() { + } + + /** + * @param enabled set true to enable metrics. Set to false by default + * @return + */ + public Builder enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + /** + * Register the given categories in this metric + * @param categories the set of {@link MetricCategory} this metric belongs to + * @return This object for method chaining + */ + public Builder categories(Set categories) { + this.categories.addAll(categories); + return this; + } + + /** + * Register the given {@link MetricCategory} in this metric + * @param category the {@link MetricCategory} to tag the metric with + * @return This object for method chaining + */ + public Builder addCategory(MetricCategory category) { + this.categories.add(category); + return this; + } + + public DefaultMetricConfigurationProvider build() { + return new DefaultMetricConfigurationProvider(this); + } + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/DefaultMetricConfigurationProviderChain.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/DefaultMetricConfigurationProviderChain.java new file mode 100644 index 000000000000..fd2908e2df3e --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/DefaultMetricConfigurationProviderChain.java @@ -0,0 +1,29 @@ +/* + * Copyright 2010-2020 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.provider; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * The default implementation of the {@link MetricConfigurationProviderChain}. + */ +@SdkPublicApi +public final class DefaultMetricConfigurationProviderChain extends MetricConfigurationProviderChain { + + public DefaultMetricConfigurationProviderChain() { + super(SystemSettingsMetricConfigurationProvider.create()); + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/MetricConfigurationProvider.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/MetricConfigurationProvider.java new file mode 100644 index 000000000000..46eaa33b7161 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/MetricConfigurationProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2020 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.provider; + +import java.util.Set; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.metrics.MetricCategory; + +/** + * Interface to configure the options in metrics feature. + *

+ * This interface acts as a feature flag for metrics. The methods in the interface are called for each request. + * This gives flexibility for metrics feature to be enabled/disabled at runtime and configuration changes + * can be picked up at runtime without need for deploying the application (depending on the implementation). + * + * @see DefaultMetricConfigurationProvider + * @see SystemSettingsMetricConfigurationProvider + */ +@SdkPublicApi +public interface MetricConfigurationProvider { + + /** + * @return true if the metrics feature is enabled. + * false if the feature is disabled. + */ + boolean enabled(); + + /** + * Return the set of {@link MetricCategory} that are enabled for metrics collection. + * Only metrics belonging to these categories will be collected. + */ + Set metricCategories(); +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/MetricConfigurationProviderChain.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/MetricConfigurationProviderChain.java new file mode 100644 index 000000000000..d296c3fdf984 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/MetricConfigurationProviderChain.java @@ -0,0 +1,76 @@ +/* + * Copyright 2010-2020 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.provider; + +import java.util.Set; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.Validate; + +/** + * Composite {@link MetricConfigurationProvider} that lets you specify a list of providers to get metric configuration. + *

+ * This chain decides the {@link MetricConfigurationProvider} to use at object construction time and cannot be changed + * later. The resolution policy is as follows: + * + * 1) Use the first provider in the chain that has returns true in {@link MetricConfigurationProvider#enabled()} method. + * + * 2) If none of the providers has metrics enabled as mentioned in above step, then use the first provider in the chain. + */ +@SdkProtectedApi +public class MetricConfigurationProviderChain implements MetricConfigurationProvider { + private static final Logger log = Logger.loggerFor(MetricConfigurationProviderChain.class); + + private final MetricConfigurationProvider resolvedProvider; + + public MetricConfigurationProviderChain(MetricConfigurationProvider... providers) { + Validate.isPositive(providers.length, "Atleast one provider should be set"); + this.resolvedProvider = resolve(providers); + } + + /** + * Determine the provider to use from the chain. This is determined at object construction time and cannot + * be changed later. + */ + private MetricConfigurationProvider resolve(MetricConfigurationProvider... providers) { + for (MetricConfigurationProvider provider : providers) { + try { + if (provider.enabled()) { + return provider; + } + } catch (Exception e) { + // Ignore any exceptions and move onto the next provider + log.debug(() -> "Metrics are not enabled in " + provider.toString(), e); + } + } + + log.debug(() -> "Metrics are not enabled in any of the providers in the chain. " + + "So using the first provider in the chain: " + providers[0]); + + return providers[0]; + } + + @Override + public boolean enabled() { + return resolvedProvider.enabled(); + } + + @Override + public Set metricCategories() { + return resolvedProvider.metricCategories(); + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/SystemSettingsMetricConfigurationProvider.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/SystemSettingsMetricConfigurationProvider.java new file mode 100644 index 000000000000..d240a1f5866e --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/provider/SystemSettingsMetricConfigurationProvider.java @@ -0,0 +1,79 @@ +/* + * Copyright 2010-2020 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.provider; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.metrics.internal.MetricSystemSetting; + +/** + * A {@link MetricConfigurationProvider} implementation that uses system properties to configure the metrics feature. + *

+ * As Java system properties can only be set during application start time or using application code, + * it is not possible to change the behavior at runtime without code changes. + */ +@SdkProtectedApi +public final class SystemSettingsMetricConfigurationProvider implements MetricConfigurationProvider { + + private final boolean enabled; + private final Set categories; + + private SystemSettingsMetricConfigurationProvider() { + this.enabled = resolveEnabled(); + this.categories = resolveCategories(); + } + + public static SystemSettingsMetricConfigurationProvider create() { + return new SystemSettingsMetricConfigurationProvider(); + } + + @Override + public boolean enabled() { + return enabled; + } + + @Override + public Set metricCategories() { + return Collections.unmodifiableSet(categories); + } + + private boolean resolveEnabled() { + return MetricSystemSetting.AWS_JAVA_SDK_METRICS_ENABLED.getBooleanValue().orElse(false); + } + + /** + * @return the set of {@link MetricCategory} computed using the + * {@link MetricSystemSetting#AWS_JAVA_SDK_METRICS_CATEGORY} property. + */ + private Set resolveCategories() { + Set result = new HashSet<>(); + + String category = MetricSystemSetting.AWS_JAVA_SDK_METRICS_CATEGORY + .getStringValue() + .orElse("default"); + + String[] list = category.trim().split(","); + for (String entry : list) { + result.add(MetricCategory.fromString(entry)); + } + + return Collections.unmodifiableSet(result); + } + +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/publisher/MetricPublisher.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/publisher/MetricPublisher.java new file mode 100644 index 000000000000..a53a61210fd8 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/publisher/MetricPublisher.java @@ -0,0 +1,70 @@ +/* + * Copyright 2010-2020 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.publisher; + +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.metrics.registry.MetricRegistry; +import software.amazon.awssdk.utils.SdkAutoCloseable; + +/** + * Interface to report and publish the collected SDK metrics to external sources. + * + * Publisher implementations create and maintain resources (like clients, thread pool etc) that are used for publishing. + * They should be closed in the close() method to avoid resource leakage. + * + *

+ * As metrics are not part of the business logic, failures caused by metrics features should not fail the application. + * So SDK publisher implementations suppress all errors during the metrics publishing and log them. + * + *

+ * In certain situations (high throttling errors, metrics are reported faster than publishing etc), storing all the metrics + * might take up lot of memory and can crash the application. In these cases, it is recommended to have a max limit on + * number of metrics stored or memory used for metrics and drop the metrics when the limit is breached. + */ +@SdkPublicApi +public interface MetricPublisher extends SdkAutoCloseable { + + /** + * Registers the metric information supplied in MetricsRegistry. The reported metrics can be transformed and + * stored in a format the publisher uses to publish the metrics. + *

+ * This method is called at the end of each request execution to report all the metrics collected + * for that request (including retry attempt metrics) + *

+ */ + void registerMetrics(MetricRegistry metricsRegistry); + + /** + * Publish all metrics stored in the publisher. If all available metrics cannot be published in a single call, + * multiple calls will be made to publish the metrics. + * + *

+ * It is recommended to publish the metrics in a non-blocking way. As it is common to publish metrics to an external + * source which involves network calls, the method is intended to be implemented in a non-blocking way and thus + * returns a {@link CompletableFuture}. + * + * Depending on the implementation, the metrics are published to the external source periodically like: + * a) after a certain time period + * b) after n metrics are registered + * c) after the buffer is full + * + * Implementations can also call publish method for every reported metric. But this can be expensive and + * is not recommended. + *

+ */ + CompletableFuture publish(); +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/publisher/MetricPublisherConfiguration.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/publisher/MetricPublisherConfiguration.java new file mode 100644 index 000000000000..2fdc1026749e --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/publisher/MetricPublisherConfiguration.java @@ -0,0 +1,94 @@ +/* + * Copyright 2010-2020 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.publisher; + +import java.util.ArrayList; +import java.util.List; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.builder.CopyableBuilder; +import software.amazon.awssdk.utils.builder.ToCopyableBuilder; + +/** + * Configure the options to publish the metrics. + *

+ * By default, SDK creates and uses only CloudWatch publisher with default options (Default credential chain + * and region chain). + * To use CloudWatch publisher with custom options or any other publishers, create a + * #PublisherConfiguration object and set it in the ClientOverrideConfiguration on the client. + *

+ * + *

+ * SDK exposes the CloudWatch and CSM publisher implementation, so instances of these classes with + * different configuration options can be set in this class. + *

+ */ +@SdkPublicApi +public final class MetricPublisherConfiguration implements + ToCopyableBuilder { + + private final List publishers = new ArrayList<>(); + + public MetricPublisherConfiguration(Builder builder) { + this.publishers.addAll(builder.publishers); + } + + /** + * @return the list of #MetricPublisher to be used for publishing the metrics + */ + public List publishers() { + return publishers; + } + + /** + * @return a {@link Builder} object to construct a PublisherConfiguration instance. + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public Builder toBuilder() { + return new Builder(); + } + + public static final class Builder implements CopyableBuilder { + + private final List publishers = new ArrayList<>(); + + private Builder() { + } + + /** + * Sets the list of publishers used for publishing the metrics. + */ + public Builder publishers(List publishers) { + this.publishers.addAll(publishers); + return this; + } + + /** + * Add a publisher to the list of publishers used for publishing the metrics. + */ + public Builder addPublisher(MetricPublisher publisher) { + this.publishers.add(publisher); + return this; + } + + public MetricPublisherConfiguration build() { + return new MetricPublisherConfiguration(this); + } + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/DefaultMetricRegistry.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/DefaultMetricRegistry.java new file mode 100644 index 000000000000..d6fb3179c174 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/DefaultMetricRegistry.java @@ -0,0 +1,194 @@ +/* + * Copyright 2010-2020 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.registry; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.metrics.meter.Counter; +import software.amazon.awssdk.metrics.meter.DefaultGauge; +import software.amazon.awssdk.metrics.meter.DefaultTimer; +import software.amazon.awssdk.metrics.meter.Gauge; +import software.amazon.awssdk.metrics.meter.LongCounter; +import software.amazon.awssdk.metrics.meter.Metric; +import software.amazon.awssdk.metrics.meter.Timer; + +/** + * Default implementation of {@link MetricRegistry} used by the SDK + */ +@SdkProtectedApi +public final class DefaultMetricRegistry implements MetricRegistry { + + private final ConcurrentMap metrics; + private final List attemptMetrics; + + private DefaultMetricRegistry() { + this.metrics = new ConcurrentHashMap<>(); + this.attemptMetrics = new ArrayList<>(); + } + + /** + * @return a new instance of the {@link DefaultMetricRegistry} + */ + public static DefaultMetricRegistry create() { + return new DefaultMetricRegistry(); + } + + @Override + public Map metrics() { + return Collections.unmodifiableMap(metrics); + } + + @Override + public List apiCallAttemptMetrics() { + return Collections.unmodifiableList(attemptMetrics); + } + + @Override + public MetricRegistry registerApiCallAttemptMetrics() { + MetricRegistry registry = create(); + this.attemptMetrics.add(registry); + return registry; + } + + @Override + public Metric register(String name, Metric metric) { + Metric existing = metrics.putIfAbsent(name, metric); + + if (existing != null) { + throw new IllegalArgumentException("A metric with name " + name + " already exists in the registry"); + } + + return metric; + } + + @Override + public Optional metric(String name) { + return Optional.ofNullable(metrics.getOrDefault(name, null)); + } + + @Override + public boolean remove(String name) { + return metrics.remove(name) != null; + } + + @Override + public Counter counter(String name, MetricBuilderParams metricBuilderParams) { + return getOrAdd(name, MetricBuilder.COUNTERS, metricBuilderParams); + } + + @Override + public Timer timer(String name, MetricBuilderParams metricBuilderParams) { + return getOrAdd(name, MetricBuilder.TIMERS, metricBuilderParams); + } + + @Override + public Gauge gauge(String name, T value, MetricBuilderParams metricBuilderParams) { + DefaultGauge gauge = (DefaultGauge) getOrAdd(name, MetricBuilder.GAUGES, metricBuilderParams); + gauge.value(value); + return gauge; + } + + @Override + public void clear() { + if (metrics != null) { + metrics.clear(); + } + + for (MetricRegistry mr : attemptMetrics) { + mr.clear(); + } + } + + private T getOrAdd(String name, MetricBuilder builder, MetricBuilderParams metricBuilderParams) { + Metric metric = metrics.get(name); + if (builder.isInstance(metric)) { + return (T) metric; + } else if (metric == null) { + try { + return (T) register(name, builder.newMetric(metricBuilderParams)); + } catch (IllegalArgumentException e) { + Metric added = metrics.get(name); + if (builder.isInstance(added)) { + return (T) added; + } + } + } + + throw new IllegalArgumentException(name + " is already used for a different type of metric"); + } + + + private interface MetricBuilder { + MetricBuilder COUNTERS = new MetricBuilder() { + @Override + public Counter newMetric(MetricBuilderParams metricBuilderParams) { + return LongCounter.builder() + .categories(metricBuilderParams.categories()) + .build(); + } + + @Override + public boolean isInstance(Metric metric) { + return Counter.class.isInstance(metric); + } + }; + + MetricBuilder TIMERS = new MetricBuilder() { + @Override + public Timer newMetric(MetricBuilderParams metricBuilderParams) { + return DefaultTimer.builder() + .categories(metricBuilderParams.categories()) + .build(); + } + + @Override + public boolean isInstance(Metric metric) { + return Timer.class.isInstance(metric); + } + }; + + MetricBuilder GAUGES = new MetricBuilder() { + @Override + public Gauge newMetric(MetricBuilderParams metricBuilderParams) { + return DefaultGauge.builder() + .categories(metricBuilderParams.categories()) + .build(); + } + + @Override + public boolean isInstance(Metric metric) { + return Gauge.class.isInstance(metric); + } + }; + + /** + * @return a new metric instance of type T + * @param metricBuilderParams Optional parameters that can be used for constructing a new metric + */ + T newMetric(MetricBuilderParams metricBuilderParams); + + /** + * @return true if given #metric is instance of type T. Otherwise false. + */ + boolean isInstance(Metric metric); + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/MetricBuilderParams.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/MetricBuilderParams.java new file mode 100644 index 000000000000..f6ea9b07ca0c --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/MetricBuilderParams.java @@ -0,0 +1,77 @@ +/* + * Copyright 2010-2020 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.registry; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.metrics.MetricCategory; + +/** + * Optional common parameters that can be applied to all metric types. These parameters are used to build a metric instance. + */ +@SdkPublicApi +public final class MetricBuilderParams { + + private final Set categories; + + private MetricBuilderParams(Builder builder) { + this.categories = Collections.unmodifiableSet(builder.categories); + } + + public Set categories() { + return categories; + } + + /** + * @return a new {@link Builder} instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class to create instances of {@link MetricBuilderParams} + */ + public static final class Builder { + + private final Set categories = new HashSet<>(); + + private Builder() { + } + + /** + * Set the set of {@link MetricCategory} to add to the metric + */ + public Builder categories(Set categories) { + this.categories.addAll(categories); + return this; + } + + /** + * Set the {@link MetricCategory} to add to the metric + */ + public Builder addCategory(MetricCategory category) { + this.categories.add(category); + return this; + } + + public MetricBuilderParams build() { + return new MetricBuilderParams(this); + } + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/MetricCategoryAwareRegistry.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/MetricCategoryAwareRegistry.java new file mode 100644 index 000000000000..6692464d3594 --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/MetricCategoryAwareRegistry.java @@ -0,0 +1,175 @@ +/* + * Copyright 2010-2020 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.registry; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.metrics.meter.Counter; +import software.amazon.awssdk.metrics.meter.Gauge; +import software.amazon.awssdk.metrics.meter.Metric; +import software.amazon.awssdk.metrics.meter.NoOpCounter; +import software.amazon.awssdk.metrics.meter.NoOpGauge; +import software.amazon.awssdk.metrics.meter.NoOpTimer; +import software.amazon.awssdk.metrics.meter.Timer; +import software.amazon.awssdk.utils.Validate; + +/** + * An implementation of {@link MetricCategory} that registers a set of {@link MetricCategory} and + * delegates calls to the underlying registry only if the given metric belongs to one of these categories. + */ +@SdkProtectedApi +public class MetricCategoryAwareRegistry implements MetricRegistry { + + private final MetricRegistry delegate; + private final Set whitelisted; + + private MetricCategoryAwareRegistry(Builder builder) { + this.delegate = Validate.notNull(builder.delegate, "MetricRegistry cannot be null"); + this.whitelisted = Collections.unmodifiableSet(builder.whitelisted); + } + + @Override + public Map metrics() { + return delegate.metrics(); + } + + @Override + public List apiCallAttemptMetrics() { + return delegate.apiCallAttemptMetrics(); + } + + @Override + public MetricRegistry registerApiCallAttemptMetrics() { + return delegate.registerApiCallAttemptMetrics(); + } + + @Override + public Metric register(String name, Metric metric) { + if (shouldDelegate(metric)) { + return delegate.register(name, metric); + } + return metric; + } + + @Override + public Optional metric(String name) { + return delegate.metric(name); + } + + @Override + public boolean remove(String name) { + return delegate.remove(name); + } + + @Override + public Counter counter(String name, MetricBuilderParams metricBuilderParams) { + if (shouldDelegate(metricBuilderParams)) { + return delegate.counter(name, metricBuilderParams); + } + + return NoOpCounter.instance(); + } + + @Override + public Timer timer(String name, MetricBuilderParams metricBuilderParams) { + if (shouldDelegate(metricBuilderParams)) { + return delegate.timer(name, metricBuilderParams); + } + + return NoOpTimer.instance(); + } + + @Override + public Gauge gauge(String name, T value, MetricBuilderParams metricBuilderParams) { + if (shouldDelegate(metricBuilderParams)) { + return delegate.gauge(name, value, metricBuilderParams); + } + + return NoOpGauge.instance(); + } + + @Override + public void clear() { + delegate.clear(); + } + + /** + * @return a new {@link Builder} instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns true if metric belongs to one of the {@link MetricCategory} in the whitelisted set. + * Otherwise false. + */ + private boolean shouldDelegate(Metric metric) { + return shouldDelegate(metric.categories()); + } + + /** + * Returns true if metric created by #metricBuilderParams belongs to one of the {@link MetricCategory} in the + * whitelisted set. Otherwise false. + */ + private boolean shouldDelegate(MetricBuilderParams metricBuilderParams) { + return shouldDelegate(metricBuilderParams.categories()); + } + + private boolean shouldDelegate(Set categories) { + return categories.stream().anyMatch(whitelisted::contains); + } + + /** + * Builder class to build instance of {@link MetricCategoryAwareRegistry}. + */ + public static final class Builder { + private MetricRegistry delegate; + private final Set whitelisted = new HashSet<>(); + + private Builder() { + } + + /** + * @param delegate the {@link MetricRegistry} to delegate the method calls + * @return This object for method chaining + */ + public Builder metricRegistry(MetricRegistry delegate) { + this.delegate = delegate; + return this; + } + + public Builder categories(Set whitelisted) { + this.whitelisted.addAll(whitelisted); + return this; + } + + public Builder addCategory(MetricCategory category) { + this.whitelisted.add(category); + return this; + } + + public MetricCategoryAwareRegistry build() { + return new MetricCategoryAwareRegistry(this); + } + } +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/MetricRegistry.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/MetricRegistry.java new file mode 100644 index 000000000000..7f0cb7f6a28b --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/MetricRegistry.java @@ -0,0 +1,135 @@ +/* + * Copyright 2010-2020 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.registry; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.metrics.meter.Counter; +import software.amazon.awssdk.metrics.meter.Gauge; +import software.amazon.awssdk.metrics.meter.Metric; +import software.amazon.awssdk.metrics.meter.Timer; + +/** + * Registry to store the collected metrics data. The interface can be used to store metrics for ApiCall and ApiCallAttempt. + * For a ApiCall, there can be multiple attempts and so a MetricRegistry has the option to store other MetricRegistry instances. + */ +@SdkPublicApi +public interface MetricRegistry { + + /** + * Return the ApiCall level metrics registered in this metric registry as a map of metric name to metric instance. + * Only metrics that can be recorded once for entire request lifecycle are recorded here. + *

+ * The method does not return the Api Call Attempt metrics. For metrics recorded separately for each attempt, + * see {@link #apiCallAttemptMetrics()}. + */ + Map metrics(); + + + /** + * Return an ordered list of {@link MetricRegistry} instances recorded for each Api Call Attempt in the request execution. + * Each Api call attempt metrics are recorded as a separate {@link MetricRegistry} instance in the given list. + *

+ * For example, + * If the Api finishes (succeed or fail) in the first attempt, the returned list size will be 1. + * + * If the Api finishes after 4 attempts (1 initial attempt + 3 retries), the returned list size will be 4. In this case, + * The 0th entry in the list has the metrics for the initial attempt, + * The 1st entry in the list has the metrics for the second attempt (1st retry) and so on. + * + * @return an ordered list of {@link MetricRegistry} instances, one for each Api Call Attempt in the request execution + */ + List apiCallAttemptMetrics(); + + /** + * Create and return a new instance of {@link MetricRegistry} for the current ApiCall Attempt. + * Records the registry instance within the class. The instance for the current attempt can be accessed by calling + * the {@link #apiCallAttemptMetrics()} method and getting the last element in the output list. + *

+ * If the Api Call finishes in the first attempt, this method is only called once. + * If the Api Call finishes after n retry attempts, this method is called n + 1 times + * (1 time for initial attempt, n times for n retries) + * + * @return a instance of {@link MetricRegistry} to record metrics for a ApiCall Attempt + */ + MetricRegistry registerApiCallAttemptMetrics(); + + /** + * Given a {@link Metric}, registers it under the given name. + * If a metric with given name is already present, method throws {@link IllegalArgumentException}. + * + * @param name the name of the metric + * @param metric the metric + * @return the given metric + */ + Metric register(String name, Metric metric); + + /** + * Returns an optional representing the metric registered with the given name. If no metric is registered + * with the given name, an empty optional will be returned. + * + * @param name the name of the metric + * @return an optional representing the metric registered with the given name. + */ + Optional metric(String name); + + /** + * Removes the metric with the given name. + * + * @param name the name of the metric + * @return True if the metric was removed. False is the metric doesn't exist or cannot be removed + */ + boolean remove(String name); + + /** + * Return the {@link Counter} registered under this name. + * If there is none registered already, create and register a new {@link Counter}. + * + * @param name name of the metric + * @param metricBuilderParams parameters that can be used to construct the metric + * @return a new or pre-existing {@link Counter} + */ + Counter counter(String name, MetricBuilderParams metricBuilderParams); + + /** + * Return the {@link Timer} registered under this name. + * If there is none registered already, create and register a new {@link Timer}. + * + * @param name name of the metric + * @param metricBuilderParams parameters that can be used to construct the metric + * @return a new or pre-existing {@link Timer} + */ + Timer timer(String name, MetricBuilderParams metricBuilderParams); + + /** + * Return a {@link Gauge} registered under this name and updates its value with #value. + * If there is none registered already, create and register a new {@link Gauge} with the given initial #value. + * + * @param type of the value + * @param name name of the metric + * @param value initial value of the guage + * @param metricBuilderParams parameters that can be used to construct the metric + * @return a new or pre-existing {@link Gauge} with updated value + */ + Gauge gauge(String name, T value, MetricBuilderParams metricBuilderParams); + + /** + * Clear all metrics stored in this registry including the metrics stored by apiCallAttemptMetrics + */ + void clear(); +} diff --git a/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/NoOpMetricRegistry.java b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/NoOpMetricRegistry.java new file mode 100644 index 000000000000..58e59327a9ef --- /dev/null +++ b/core/metrics-spi/src/main/java/software/amazon/awssdk/metrics/registry/NoOpMetricRegistry.java @@ -0,0 +1,95 @@ +/* + * Copyright 2010-2020 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.registry; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.metrics.meter.Counter; +import software.amazon.awssdk.metrics.meter.Gauge; +import software.amazon.awssdk.metrics.meter.Metric; +import software.amazon.awssdk.metrics.meter.NoOpCounter; +import software.amazon.awssdk.metrics.meter.NoOpGauge; +import software.amazon.awssdk.metrics.meter.NoOpTimer; +import software.amazon.awssdk.metrics.meter.Timer; + +/** + * A NoOp implementation of {@link MetricRegistry} interface. + */ +@SdkInternalApi +public final class NoOpMetricRegistry implements MetricRegistry { + + private static final NoOpMetricRegistry INSTANCE = new NoOpMetricRegistry(); + + /** + * @return A singleton instance of the {@link NoOpMetricRegistry}. + */ + public static NoOpMetricRegistry getInstance() { + return INSTANCE; + } + + @Override + public Map metrics() { + return Collections.unmodifiableMap(Collections.EMPTY_MAP); + } + + @Override + public List apiCallAttemptMetrics() { + return Collections.unmodifiableList(Collections.EMPTY_LIST); + } + + @Override + public MetricRegistry registerApiCallAttemptMetrics() { + return getInstance(); + } + + @Override + public Metric register(String name, Metric metric) { + return metric; + } + + @Override + public Optional metric(String name) { + return Optional.empty(); + } + + @Override + public boolean remove(String name) { + return false; + } + + @Override + public Counter counter(String name, MetricBuilderParams metricBuilderParams) { + return NoOpCounter.instance(); + } + + @Override + public Timer timer(String name, MetricBuilderParams metricBuilderParams) { + return NoOpTimer.instance(); + } + + @Override + public Gauge gauge(String name, T value, MetricBuilderParams metricBuilderParams) { + return NoOpGauge.instance(); + } + + @Override + public void clear() { + + } +} diff --git a/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/meter/DefaultTimerTest.java b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/meter/DefaultTimerTest.java new file mode 100644 index 000000000000..998dffac5dcd --- /dev/null +++ b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/meter/DefaultTimerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2010-2020 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.meter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import software.amazon.awssdk.metrics.MetricCategory; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultTimerTest { + + private static final long DURATION_MILLIS = 15L; + + private static final int CALLABLE_RESULT = 1234; + + @Mock + private Clock mockClock; + + private static Timer timer; + + @Before + public void setupCase() { + timer = DefaultTimer.builder() + .clock(mockClock) + .addCategory(MetricCategory.DEFAULT) + .build(); + + when(mockClock.instant()).thenReturn(Instant.ofEpochMilli(0), Instant.ofEpochMilli(DURATION_MILLIS)); + } + + @Test + public void testRunnable() { + AtomicBoolean flag = new AtomicBoolean(false); + + timer.record(() -> flag.set(true)); + + // validates runnable is run + assertThat(flag.get()).isTrue(); + + assertThat(timer.totalTime()).isEqualTo(Duration.ofMillis(DURATION_MILLIS)); + } + + @Test + public void testCallable() throws Exception { + int callResult = timer.record(new MyCallable()); + + assertThat(callResult).isEqualTo(CALLABLE_RESULT); + + assertThat(timer.totalTime()).isEqualTo(Duration.ofMillis(DURATION_MILLIS)); + } + + private static class MyCallable implements Callable { + @Override + public Integer call() throws Exception { + return CALLABLE_RESULT; + } + } +} diff --git a/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/meter/GaugeTest.java b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/meter/GaugeTest.java new file mode 100644 index 000000000000..73c52372e8bc --- /dev/null +++ b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/meter/GaugeTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010-2020 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.meter; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import software.amazon.awssdk.metrics.MetricCategory; + +public class GaugeTest { + + @Test + public void constantGauge_CreateMethod() { + String value = "foobar"; + Gauge gauge = ConstantGauge.create(value); + + assertThat(gauge.value()).isEqualTo(value); + assertThat(gauge.categories()).isEmpty(); + } + + + @Test + public void constantGauge_Builder() { + Gauge gauge = ConstantGauge.builder() + .value(5L) + .addCategory(MetricCategory.ALL) + .build(); + + assertThat(gauge.value()).isEqualTo(5L); + assertThat(gauge.categories()).containsExactly(MetricCategory.ALL); + } + + @Test + public void testDefaultGauge() { + String value = "foobar"; + String newValue = "barbaz"; + DefaultGauge gauge = DefaultGauge.create(value); + + assertThat(gauge.value()).isEqualTo(value); + assertThat(gauge.categories()).isEmpty(); + + gauge.value(newValue); + assertThat(gauge.value()).isEqualTo(newValue); + } +} diff --git a/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/meter/LongCounterTest.java b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/meter/LongCounterTest.java new file mode 100644 index 000000000000..9bb7f8fcaea5 --- /dev/null +++ b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/meter/LongCounterTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2010-2020 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.meter; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Before; +import org.junit.Test; + +public class LongCounterTest { + + private Counter counter; + + @Before + public void setup() { + counter = LongCounter.create(); + } + + @Test + public void noarg_increment() { + counter.increment(); + assertThat(counter.count()).isEqualTo(1L); + } + + @Test + public void noarg_decrement() { + counter.decrement(); + assertThat(counter.count()).isEqualTo(-1L); + } + + @Test + public void increment_positive_values() { + counter.increment(5L); + counter.increment(); + + assertThat(counter.count()).isEqualTo(6L); + } + + @Test + public void decrement_positive_values() { + counter.decrement(5L); + + assertThat(counter.count()).isEqualTo(-5L); + } + + @Test (expected = IllegalArgumentException.class) + public void increment_negative_values() { + counter.increment(-5L); + } + + @Test (expected = IllegalArgumentException.class) + public void decrement_negative_values() { + counter.decrement(-5L); + } + + @Test + public void operate_on_zero_values() { + counter.increment(0L); + counter.decrement(0L); + counter.decrement(0L); + + assertThat(counter.count()).isEqualTo(0L); + } + + @Test + public void increment_boundary_values() { + counter.increment(Long.MAX_VALUE); + + assertThat(counter.count()).isEqualTo(Long.MAX_VALUE); + } + + @Test + public void decrement_boundary_values() { + counter.decrement(Long.MAX_VALUE); + + assertThat(counter.count()).isEqualTo(-Long.MAX_VALUE); + } +} diff --git a/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/meter/NoOpMetricsTest.java b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/meter/NoOpMetricsTest.java new file mode 100644 index 000000000000..dde206f79579 --- /dev/null +++ b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/meter/NoOpMetricsTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2020 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.meter; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Test; + +public class NoOpMetricsTest { + + @Test + public void testCounter() { + Counter counter = NoOpCounter.instance(); + + counter.increment(); + counter.increment(5); + counter.decrement(1); + + assertThat(counter.count()).isEqualTo(0); + } + + @Test + public void noopGauge_always_return_sameObjectInstance() { + assertThat(NoOpGauge.instance().value()) + .isEqualTo(NoOpGauge.instance().value()); + } + + @Test + public void noopTimer_runnable_returnsZeroDuration() { + AtomicBoolean flag = new AtomicBoolean(false); + Timer timer = NoOpTimer.instance(); + timer.record(() -> flag.set(true)); + + assertThat(flag.get()).isTrue(); + assertThat(timer.totalTime()).isEqualTo(Duration.ZERO); + } + + @Test + public void noopTimer_startAndEndMethods() throws Exception { + Timer timer = NoOpTimer.instance(); + timer.start(); + Thread.sleep(Duration.ofSeconds(1).toMillis()); + timer.end(); + + assertThat(timer.totalTime()).isEqualTo(Duration.ZERO); + } +} diff --git a/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/registry/DefaultMetricRegistryTest.java b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/registry/DefaultMetricRegistryTest.java new file mode 100644 index 000000000000..630dcf4a8074 --- /dev/null +++ b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/registry/DefaultMetricRegistryTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2010-2020 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.registry; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.metrics.meter.ConstantGauge; +import software.amazon.awssdk.metrics.meter.Counter; +import software.amazon.awssdk.metrics.meter.DefaultGauge; +import software.amazon.awssdk.metrics.meter.DefaultTimer; +import software.amazon.awssdk.metrics.meter.Gauge; +import software.amazon.awssdk.metrics.meter.LongCounter; +import software.amazon.awssdk.metrics.meter.Metric; +import software.amazon.awssdk.metrics.meter.Timer; + +public class DefaultMetricRegistryTest { + + private static MetricRegistry registry; + private static MetricBuilderParams params; + + @BeforeClass + public static void setup() { + registry = DefaultMetricRegistry.create(); + params = MetricBuilderParams.builder().addCategory(MetricCategory.DEFAULT).build(); + } + + @Before + public void clear() { + registry.clear(); + assertThat(registry.metrics().size()).isEqualTo(0); + } + + @Test + public void register_method_storesAllGivenMetrics() { + String metricOne = "foo", metricTwo = "bar"; + + registry.register(metricOne, LongCounter.create()); + registry.register(metricTwo, ConstantGauge.create(5)); + + Map metrics = registry.metrics(); + assertThat(metrics.size()).isEqualTo(2); + assertThat(metrics.keySet()).containsExactlyInAnyOrder(metricOne, metricTwo); + assertThat(metrics.get(metricOne)).isInstanceOf(LongCounter.class); + assertThat(metrics.get(metricTwo)).isInstanceOf(ConstantGauge.class); + } + + @Test + public void test_apiCallAttemptMetrics() { + registry.registerApiCallAttemptMetrics(); + MetricRegistry mr = registry.registerApiCallAttemptMetrics(); + mr.register("foo", LongCounter.create()); + + assertThat(registry.metrics()).isEmpty(); + + List apiCallAttemptMetrics = registry.apiCallAttemptMetrics(); + assertThat(apiCallAttemptMetrics.size()).isEqualTo(2); + assertThat(apiCallAttemptMetrics.get(0).metrics()).isEmpty(); + assertThat(apiCallAttemptMetrics.get(1).metrics().size()).isEqualTo(1); + assertThat(apiCallAttemptMetrics.get(1).metrics().keySet()).containsExactly("foo"); + } + + @Test + public void test_nonExisting_metric() { + assertThat(registry.metric("foo")).isNotPresent(); + } + + @Test + public void test_remove_metric() { + String metric = "foo"; + registry.register(metric , LongCounter.create()); + + assertThat(registry.remove(metric)).isTrue(); + assertThat(registry.remove("bar")).isFalse(); + } + + @Test (expected = IllegalArgumentException.class) + public void register_throwsException_ifMetricAlreadyExists() { + String metric = "foo"; + registry.register(metric , LongCounter.create()); + registry.register(metric , ConstantGauge.create(5)); + } + + @Test + public void testCounter() { + String metric = "foo"; + Counter counter = registry.counter(metric, params); + + assertThat(counter).isInstanceOf(LongCounter.class); + assertThat(registry.metrics().size()).isEqualTo(1); + } + + @Test + public void counter_returns_existingInstance_IfNameIsSame() { + String metric = "foo"; + Counter counter1 = registry.counter(metric, params); + Counter counter2 = registry.counter(metric, params); + + assertThat(counter1).isEqualTo(counter2); + assertThat(registry.metrics().size()).isEqualTo(1); + } + + @Test (expected = IllegalArgumentException.class) + public void counter_throwsError_IfNameIsRegisteredWithAnotherMetric() { + String metric = "foo"; + registry.timer(metric, params); + registry.counter(metric, params); + } + + @Test + public void testMetricRegistryMethods() { + registry.register("counter", LongCounter.create()); + registry.counter("counter", params); + + Timer timer = registry.timer("timer", params); + assertThat(timer).isInstanceOf(DefaultTimer.class); + + Gauge gauge = registry.gauge("gauge", "foobar", params); + assertThat(gauge).isInstanceOf(DefaultGauge.class); + + assertThat(registry.metrics().size()).isEqualTo(3); + assertThat(registry.metrics().keySet()).containsExactlyInAnyOrder("counter", "timer", "gauge"); + } +} diff --git a/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/registry/MetricCategoryAwareRegistryTest.java b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/registry/MetricCategoryAwareRegistryTest.java new file mode 100644 index 000000000000..2b5c50066dbb --- /dev/null +++ b/core/metrics-spi/src/test/java/software/amazon/awssdk/metrics/registry/MetricCategoryAwareRegistryTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2010-2020 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.registry; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.metrics.meter.DefaultTimer; +import software.amazon.awssdk.metrics.meter.Metric; +import software.amazon.awssdk.metrics.meter.NoOpCounter; +import software.amazon.awssdk.metrics.meter.NoOpGauge; +import software.amazon.awssdk.metrics.meter.NoOpTimer; +import software.amazon.awssdk.metrics.meter.Timer; + +public class MetricCategoryAwareRegistryTest { + + private static MetricRegistry delegate; + private static MetricRegistry registry; + private static MetricBuilderParams nonWhitelistParams; + private static final Set whitelisted = new HashSet<>(); + + @BeforeClass + public static void setup() { + delegate = DefaultMetricRegistry.create(); + whitelisted.add(MetricCategory.DEFAULT); + whitelisted.add(MetricCategory.HTTP_CLIENT); + + registry = MetricCategoryAwareRegistry.builder() + .metricRegistry(delegate) + .categories(whitelisted) + .build(); + + nonWhitelistParams = MetricBuilderParams.builder() + .addCategory(MetricCategory.STREAMING) + .build(); + } + + @Before + public void clear() { + registry.clear(); + assertThat(registry.metrics().size()).isEqualTo(0); + } + + @Test + public void register_method_onlyStoresMetrics_InWhitelistedCategories() { + String metricOne = "foo", metricTwo = "bar"; + + registry.register(metricOne, timer(MetricCategory.HTTP_CLIENT)); + registry.register(metricTwo, timer(MetricCategory.STREAMING)); + + Map metrics = registry.metrics(); + assertThat(metrics.size()).isEqualTo(1); + assertThat(metrics.keySet()).containsExactlyInAnyOrder(metricOne); + assertThat(metrics.get(metricOne)).isInstanceOf(DefaultTimer.class); + + assertThat(registry.metrics()).isEqualTo(delegate.metrics()); + } + + @Test + public void noopInstances_are_returned_ifCategoryIsNotInWhitelisted() { + String metric = "foo"; + assertThat(registry.counter(metric, nonWhitelistParams)).isEqualTo(NoOpCounter.instance()); + assertThat(registry.timer(metric, nonWhitelistParams)).isEqualTo(NoOpTimer.instance()); + assertThat(registry.gauge(metric, "foo", nonWhitelistParams)).isEqualTo(NoOpGauge.instance()); + + assertThat(registry.metrics()).isEmpty(); + } + + private Timer timer(MetricCategory metricCategory) { + return DefaultTimer.builder() + .addCategory(metricCategory) + .build(); + } +} diff --git a/core/pom.xml b/core/pom.xml index 7e6b8281ede7..c87ead9fdd5a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -41,6 +41,7 @@ profiles regions protocols + metrics-spi diff --git a/core/sdk-core/pom.xml b/core/sdk-core/pom.xml index 8f41b2c399a7..c6575c53c11b 100644 --- a/core/sdk-core/pom.xml +++ b/core/sdk-core/pom.xml @@ -51,6 +51,11 @@ profiles ${awsjavasdk.version} + + software.amazon.awssdk + metrics-spi + ${awsjavasdk.version} + org.slf4j diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index 199eef00e404..ecd31c34172b 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -28,6 +28,8 @@ import static software.amazon.awssdk.core.client.config.SdkClientOption.ASYNC_HTTP_CLIENT; import static software.amazon.awssdk.core.client.config.SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED; import static software.amazon.awssdk.core.client.config.SdkClientOption.EXECUTION_INTERCEPTORS; +import static software.amazon.awssdk.core.client.config.SdkClientOption.METRIC_CONFIGURATION_PROVIDER; +import static software.amazon.awssdk.core.client.config.SdkClientOption.METRIC_PUBLISHER_CONFIGURATION; import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_POLICY; import static software.amazon.awssdk.core.client.config.SdkClientOption.SCHEDULED_EXECUTOR_SERVICE; import static software.amazon.awssdk.utils.CollectionUtils.mergeLists; @@ -35,6 +37,7 @@ import java.net.URI; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Optional; @@ -56,6 +59,7 @@ import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.core.internal.http.loader.DefaultSdkAsyncHttpClientBuilder; import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder; +import software.amazon.awssdk.core.internal.metrics.MetricsExecutionInterceptor; import software.amazon.awssdk.core.internal.util.UserAgentUtils; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.http.ExecutableHttpRequest; @@ -63,6 +67,8 @@ import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.async.AsyncExecuteRequest; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.metrics.provider.DefaultMetricConfigurationProviderChain; +import software.amazon.awssdk.metrics.publisher.MetricPublisherConfiguration; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.Either; import software.amazon.awssdk.utils.ThreadFactoryBuilder; @@ -142,6 +148,7 @@ protected final SdkClientConfiguration syncClientConfiguration() { // Apply defaults configuration = mergeChildDefaults(configuration); configuration = mergeGlobalDefaults(configuration); + configuration = mergeMetricDefaults(configuration); // Create additional configuration from the default-applied configuration configuration = finalizeChildConfiguration(configuration); @@ -165,6 +172,7 @@ protected final SdkClientConfiguration asyncClientConfiguration() { // Apply defaults configuration = mergeChildDefaults(configuration); configuration = mergeGlobalDefaults(configuration); + configuration = mergeMetricDefaults(configuration); // Create additional configuration from the default-applied configuration configuration = finalizeChildConfiguration(configuration); @@ -195,8 +203,31 @@ private SdkClientConfiguration mergeGlobalDefaults(SdkClientConfiguration config } /** - * Optionally overridden by child implementations to derive implementation-specific configuration from the - * default-applied configuration. (eg. AWS's endpoint, derived from the region). + * Add metric configuration defaults if not present already in the configuration. + */ + private SdkClientConfiguration mergeMetricDefaults(SdkClientConfiguration configuration) { + SdkClientConfiguration.Builder defaults = SdkClientConfiguration.builder(); + if (configuration.option(METRIC_CONFIGURATION_PROVIDER) == null) { + defaults.option(METRIC_CONFIGURATION_PROVIDER, new DefaultMetricConfigurationProviderChain()); + } + if (configuration.option(METRIC_PUBLISHER_CONFIGURATION) == null) { + defaults.option(METRIC_PUBLISHER_CONFIGURATION, loadDefaultMetricPublisher()); + } + + return configuration.merge(defaults.build()); + } + + // TODO + // Create an instance of Cloudwatch publisher from classloader and set is using addPublisher() method + private MetricPublisherConfiguration loadDefaultMetricPublisher() { + return MetricPublisherConfiguration.builder() + //.addPublisher() + .build(); + } + + /** + * Optionally overidden by child implementations to derive implementation-specific configuration from the default-applied + * configuration. (eg. AWS's endpoint, derived from the region). */ protected SdkClientConfiguration finalizeChildConfiguration(SdkClientConfiguration configuration) { return configuration; @@ -301,7 +332,16 @@ private ScheduledExecutorService resolveScheduledExecutorService() { */ private List resolveExecutionInterceptors(SdkClientConfiguration config) { List globalInterceptors = new ClasspathInterceptorChainFactory().getGlobalInterceptors(); - return mergeLists(globalInterceptors, config.option(EXECUTION_INTERCEPTORS)); + List resolved = mergeLists(globalInterceptors, config.option(EXECUTION_INTERCEPTORS)); + return mergeLists(resolved, metricsInterceptor()); + } + + /** + * Return a singleton list containing the metrics interceptor. This will always be the + * last interceptor in the interceptor chain. + */ + private List metricsInterceptor() { + return Collections.singletonList(new MetricsExecutionInterceptor()); } @Override @@ -338,6 +378,9 @@ public final B overrideConfiguration(ClientOverrideConfiguration overrideConfig) clientConfiguration.option(API_CALL_ATTEMPT_TIMEOUT, overrideConfig.apiCallAttemptTimeout().orElse(null)); clientConfiguration.option(DISABLE_HOST_PREFIX_INJECTION, overrideConfig.advancedOption(DISABLE_HOST_PREFIX_INJECTION).orElse(null)); + clientConfiguration.option(METRIC_CONFIGURATION_PROVIDER, overrideConfig.metricConfigurationProvider().orElse(null)); + clientConfiguration.option(METRIC_PUBLISHER_CONFIGURATION, overrideConfig.metricPublisherConfiguration().orElse(null)); + return thisBuilder(); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java index 3e06af8c074f..46151c3ec6af 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java @@ -28,6 +28,8 @@ import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.sync.ResponseTransformer; +import software.amazon.awssdk.metrics.provider.MetricConfigurationProvider; +import software.amazon.awssdk.metrics.publisher.MetricPublisherConfiguration; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.CollectionUtils; import software.amazon.awssdk.utils.ToString; @@ -50,6 +52,8 @@ public final class ClientOverrideConfiguration private final AttributeMap advancedOptions; private final Duration apiCallAttemptTimeout; private final Duration apiCallTimeout; + private final MetricConfigurationProvider metricConfigurationProvider; + private final MetricPublisherConfiguration metricPublisherConfiguration; /** * Initialize this configuration. Private to require use of {@link #builder()}. @@ -61,6 +65,8 @@ private ClientOverrideConfiguration(Builder builder) { this.advancedOptions = builder.advancedOptions(); this.apiCallTimeout = Validate.isPositiveOrNull(builder.apiCallTimeout(), "apiCallTimeout"); this.apiCallAttemptTimeout = Validate.isPositiveOrNull(builder.apiCallAttemptTimeout(), "apiCallAttemptTimeout"); + this.metricConfigurationProvider = builder.metricConfigurationProvider(); + this.metricPublisherConfiguration = builder.metricPublisherConfiguration(); } @Override @@ -101,6 +107,20 @@ public Optional retryPolicy() { return Optional.ofNullable(retryPolicy); } + /** + * @return The optional {@link MetricConfigurationProvider} used to configure metrics feature + */ + public Optional metricConfigurationProvider() { + return Optional.ofNullable(metricConfigurationProvider); + } + + /** + * @return The optional {@link MetricPublisherConfiguration} used to configure metrics publishers + */ + public Optional metricPublisherConfiguration() { + return Optional.ofNullable(metricPublisherConfiguration); + } + /** * Load the optional requested advanced option that was configured on the client builder. * @@ -232,6 +252,31 @@ default Builder retryPolicy(Consumer retryPolicy) { return retryPolicy(RetryPolicy.builder().applyMutation(retryPolicy).build()); } + /** + * Configure the provider with options to control the metrics feature + * + * @return This object for method chaining + */ + Builder metricConfigurationProvider(MetricConfigurationProvider metricConfigurationProvider); + + /** + * @return the provider with options to control the metrics feature + */ + MetricConfigurationProvider metricConfigurationProvider(); + + /** + * Configure the configuration for metric publishers + * + * @param metricPublisherConfiguration + * @return This object for method chaining + */ + Builder metricPublisherConfiguration(MetricPublisherConfiguration metricPublisherConfiguration); + + /** + * @return the configuration for metric publishers + */ + MetricPublisherConfiguration metricPublisherConfiguration(); + /** * Configure a list of execution interceptors that will have access to read and modify the request and response objcets as * they are processed by the SDK. These will replace any interceptors configured previously with this method or @@ -333,6 +378,8 @@ default Builder retryPolicy(Consumer retryPolicy) { private static final class DefaultClientOverrideConfigurationBuilder implements Builder { private Map> headers = new HashMap<>(); private RetryPolicy retryPolicy; + private MetricConfigurationProvider metricConfigurationProvider; + private MetricPublisherConfiguration metricPublisherConfiguration; private List executionInterceptors = new ArrayList<>(); private AttributeMap.Builder advancedOptions = AttributeMap.builder(); private Duration apiCallTimeout; @@ -377,6 +424,36 @@ public RetryPolicy retryPolicy() { return retryPolicy; } + @Override + public Builder metricConfigurationProvider(MetricConfigurationProvider metricConfigurationProvider) { + this.metricConfigurationProvider = metricConfigurationProvider; + return this; + } + + public void setMetricConfigurationProvider(MetricConfigurationProvider metricConfigurationProvider) { + metricConfigurationProvider(metricConfigurationProvider); + } + + @Override + public MetricConfigurationProvider metricConfigurationProvider() { + return metricConfigurationProvider; + } + + @Override + public Builder metricPublisherConfiguration(MetricPublisherConfiguration metricPublisherConfiguration) { + this.metricPublisherConfiguration = metricPublisherConfiguration; + return this; + } + + public void setMetricPublisherConfiguration(MetricPublisherConfiguration metricPublisherConfiguration) { + metricPublisherConfiguration(metricPublisherConfiguration); + } + + @Override + public MetricPublisherConfiguration metricPublisherConfiguration() { + return metricPublisherConfiguration; + } + @Override public Builder executionInterceptors(List executionInterceptors) { Validate.paramNotNull(executionInterceptors, "executionInterceptors"); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkAdvancedClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkAdvancedClientOption.java index ae02e1228226..bbb170f99b09 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkAdvancedClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkAdvancedClientOption.java @@ -18,7 +18,6 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.signer.Signer; - /** * A collection of advanced options that can be configured on an AWS client via * {@link ClientOverrideConfiguration.Builder#putAdvancedOption(SdkAdvancedClientOption, Object)}. diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java index 8e271cf2f391..23c6e39136c2 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java @@ -27,6 +27,8 @@ import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.metrics.provider.MetricConfigurationProvider; +import software.amazon.awssdk.metrics.publisher.MetricPublisherConfiguration; /** * A set of internal options required by the SDK via {@link SdkClientConfiguration}. @@ -118,6 +120,18 @@ public final class SdkClientOption extends ClientOption { */ public static final SdkClientOption ENDPOINT_DISCOVERY_ENABLED = new SdkClientOption<>(Boolean.class); + /** + * Set the client option to configure {@link MetricConfigurationProvider} + */ + public static final SdkAdvancedClientOption METRIC_CONFIGURATION_PROVIDER = + new SdkAdvancedClientOption<>(MetricConfigurationProvider.class); + + /** + * Set the client option to configure {@link MetricPublisherConfiguration} + */ + public static final SdkAdvancedClientOption METRIC_PUBLISHER_CONFIGURATION = + new SdkAdvancedClientOption<>(MetricPublisherConfiguration.class); + private SdkClientOption(Class valueClass) { super(valueClass); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/MetricExecutionAttribute.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/MetricExecutionAttribute.java new file mode 100644 index 000000000000..44d08548b200 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/MetricExecutionAttribute.java @@ -0,0 +1,64 @@ +/* + * Copyright 2010-2020 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.core.interceptor; + +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.metrics.provider.MetricConfigurationProvider; +import software.amazon.awssdk.metrics.publisher.MetricPublisherConfiguration; +import software.amazon.awssdk.metrics.registry.MetricRegistry; + +/** + * Contains attributes related to the metrics feature. This information is used to determine the metrics behavior. + * + * Only SDK should set these values + */ +@SdkProtectedApi +public final class MetricExecutionAttribute { + + /** + * The key to store the {@link MetricRegistry} for the ApiCall execution + * + * @see MetricRegistry + */ + public static final ExecutionAttribute METRIC_REGISTRY = new ExecutionAttribute<>("MetricRegistry"); + + /** + * The key to store the {@link MetricRegistry} for an ApiCall Attempt + * + * @see MetricRegistry + */ + public static final ExecutionAttribute ATTEMPT_METRIC_REGISTRY = + new ExecutionAttribute<>("AttemptMetricRegistry"); + + /** + * The key to store the {@link MetricConfigurationProvider} + * + * @see MetricConfigurationProvider + */ + public static final ExecutionAttribute METRIC_CONFIGURATION_PROVIDER = + new ExecutionAttribute<>("MetricConfigurationProvider"); + + /** + * The key to store the {@link MetricPublisherConfiguration} + * + * @see MetricPublisherConfiguration + */ + public static final ExecutionAttribute METRIC_PUBLISHER_CONFIGURATION = + new ExecutionAttribute<>("MetricPublisherConfiguration"); + + private MetricExecutionAttribute() { + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseAsyncClientHandler.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseAsyncClientHandler.java index dc8f71c97908..d34fb7b38abf 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseAsyncClientHandler.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseAsyncClientHandler.java @@ -179,6 +179,9 @@ private Comple try { + // Initialize metrics registry and register some constant metrics + initializeMetrics(executionContext); + // Running beforeExecution interceptors and modifyRequest interceptors. InterceptorContext finalizeSdkRequestContext = finalizeSdkRequest(executionContext); InputT inputT = (InputT) finalizeSdkRequestContext.request(); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseClientHandler.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseClientHandler.java index 0b55625614a7..58c2c38d297a 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseClientHandler.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseClientHandler.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.core.internal.handler; +import static software.amazon.awssdk.core.interceptor.MetricExecutionAttribute.METRIC_REGISTRY; + import java.net.URI; import java.util.function.BiFunction; @@ -31,10 +33,19 @@ import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.ExecutionInterceptorChain; import software.amazon.awssdk.core.interceptor.InterceptorContext; +import software.amazon.awssdk.core.interceptor.MetricExecutionAttribute; import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; import software.amazon.awssdk.core.internal.InternalCoreExecutionAttribute; +import software.amazon.awssdk.core.internal.http.pipeline.stages.utils.MetricUtils; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpFullResponse; +import software.amazon.awssdk.metrics.meter.Timer; +import software.amazon.awssdk.metrics.metrics.SdkDefaultMetric; +import software.amazon.awssdk.metrics.provider.MetricConfigurationProvider; +import software.amazon.awssdk.metrics.registry.DefaultMetricRegistry; +import software.amazon.awssdk.metrics.registry.MetricCategoryAwareRegistry; +import software.amazon.awssdk.metrics.registry.MetricRegistry; +import software.amazon.awssdk.metrics.registry.NoOpMetricRegistry; import software.amazon.awssdk.utils.StringUtils; @SdkInternalApi @@ -66,7 +77,15 @@ static InterceptorContext finalizeSdkHttpFu SdkClientConfiguration clientConfiguration) { runBeforeMarshallingInterceptors(executionContext); + + Timer marshallTimer = MetricUtils.timer(executionContext.executionAttributes() + .getAttribute(METRIC_REGISTRY), + SdkDefaultMetric.MARSHALLING_LATENCY); + + marshallTimer.start(); SdkHttpFullRequest request = executionParams.getMarshaller().marshall(inputT); + marshallTimer.end(); + request = modifyEndpointHostIfNeeded(request, clientConfiguration, executionParams); addHttpRequest(executionContext, request); @@ -272,4 +291,38 @@ private static BiFunction composeResponseFunctions(BiFunction function2) { return (x, y) -> function2.apply(function1.apply(x, y), y); } + + /** + * Sets the metrics registry instance in execution attributes. + * Register some constant metrics regarding the client and the API + */ + protected void initializeMetrics(ExecutionContext executionContext) { + MetricRegistry metricRegistry; + MetricConfigurationProvider metricProvider = executionContext.executionAttributes() + .getAttribute(MetricExecutionAttribute + .METRIC_CONFIGURATION_PROVIDER); + + if (metricProvider != null && metricProvider.enabled()) { + metricRegistry = MetricCategoryAwareRegistry.builder() + .metricRegistry(DefaultMetricRegistry.create()) + .categories(metricProvider.metricCategories()) + .build(); + } else { + metricRegistry = NoOpMetricRegistry.getInstance(); + } + + executionContext.executionAttributes().putAttribute(METRIC_REGISTRY, metricRegistry); + + MetricUtils.registerConstantGauge(executionContext.executionAttributes() + .getAttribute(SdkExecutionAttribute.SERVICE_NAME), + metricRegistry, + SdkDefaultMetric.SERVICE); + + MetricUtils.registerConstantGauge(executionContext.executionAttributes() + .getAttribute(SdkExecutionAttribute.OPERATION_NAME), + metricRegistry, + SdkDefaultMetric.OPERATION); + + MetricUtils.timer(metricRegistry, SdkDefaultMetric.API_CALL).start(); + } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseSyncClientHandler.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseSyncClientHandler.java index c9e8901ecbc1..6227f7125ce1 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseSyncClientHandler.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseSyncClientHandler.java @@ -127,6 +127,9 @@ private ReturnT doExecute( ExecutionContext executionContext, HttpResponseHandler> responseHandler) { + // Initialize metrics registry and register some constant metrics + initializeMetrics(executionContext); + InputT inputT = (InputT) finalizeSdkRequest(executionContext).request(); InterceptorContext sdkHttpFullRequestContext = finalizeSdkHttpFullRequest(executionParams, diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/RequestExecutionContext.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/RequestExecutionContext.java index caf32332a2c2..bdba4cd86e40 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/RequestExecutionContext.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/RequestExecutionContext.java @@ -23,9 +23,11 @@ import software.amazon.awssdk.core.http.ExecutionContext; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.ExecutionInterceptorChain; +import software.amazon.awssdk.core.interceptor.MetricExecutionAttribute; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; import software.amazon.awssdk.core.internal.http.timers.TimeoutTracker; import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.metrics.registry.MetricRegistry; import software.amazon.awssdk.utils.Validate; /** @@ -124,6 +126,13 @@ public void requestProvider(AsyncRequestBody publisher) { requestProvider = publisher; } + /** + * @return the per attempt metric registry stored by {@link MetricExecutionAttribute#ATTEMPT_METRIC_REGISTRY} + */ + public MetricRegistry attemptMetricRegistry() { + return executionAttributes().getAttribute(MetricExecutionAttribute.ATTEMPT_METRIC_REGISTRY); + } + /** * An SDK-internal implementation of {@link Builder}. */ diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/async/AsyncAfterTransmissionInterceptorCallingResponseHandler.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/async/AsyncAfterTransmissionInterceptorCallingResponseHandler.java index 3fe65418ec1d..7b0863b1d7ed 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/async/AsyncAfterTransmissionInterceptorCallingResponseHandler.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/async/AsyncAfterTransmissionInterceptorCallingResponseHandler.java @@ -93,4 +93,4 @@ public void onStream(Publisher publisher) { public CompletableFuture prepare() { return delegate.prepare(); } -} \ No newline at end of file +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/async/CombinedResponseAsyncHttpResponseHandler.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/async/CombinedResponseAsyncHttpResponseHandler.java index ab07dc5ad1db..f456df2ada37 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/async/CombinedResponseAsyncHttpResponseHandler.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/async/CombinedResponseAsyncHttpResponseHandler.java @@ -121,4 +121,4 @@ private static SdkHttpFullResponse toFullResponse(SdkHttpResponse response) { response.statusText().ifPresent(builder::statusText); return builder.build(); } -} \ No newline at end of file +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java index 11950f1b7540..ea2cd2d776fa 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AsyncRetryableStage.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.core.internal.http.pipeline.stages; +import static software.amazon.awssdk.core.interceptor.MetricExecutionAttribute.METRIC_REGISTRY; + import java.time.Duration; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -36,6 +38,7 @@ import software.amazon.awssdk.core.internal.http.RequestExecutionContext; import software.amazon.awssdk.core.internal.http.TransformingAsyncResponseHandler; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; +import software.amazon.awssdk.core.internal.http.pipeline.stages.utils.MetricUtils; import software.amazon.awssdk.core.internal.retry.ClockSkewAdjuster; import software.amazon.awssdk.core.internal.retry.RetryHandler; import software.amazon.awssdk.core.internal.util.CapacityManager; @@ -43,6 +46,9 @@ import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpFullResponse; +import software.amazon.awssdk.metrics.meter.Timer; +import software.amazon.awssdk.metrics.metrics.SdkDefaultMetric; +import software.amazon.awssdk.metrics.registry.MetricRegistry; import software.amazon.awssdk.utils.CompletableFutureUtils; /** @@ -75,7 +81,13 @@ public AsyncRetryableStage(TransformingAsyncResponseHandler> r @Override public CompletableFuture> execute(SdkHttpFullRequest request, RequestExecutionContext context) throws Exception { - return new RetryExecutor(request, context).execute(); + CompletableFuture> responseFuture = new RetryExecutor(request, context).execute(); + responseFuture.whenComplete((resp, err) -> MetricUtils.timer(context.executionContext() + .executionAttributes() + .getAttribute(METRIC_REGISTRY), + SdkDefaultMetric.API_CALL) + .end()); + return responseFuture; } /** @@ -103,9 +115,17 @@ public CompletableFuture> execute() throws Exception { } public CompletableFuture> execute(CompletableFuture> future) throws Exception { + MetricRegistry attemptRegistry = MetricUtils.newRegistry(context.executionAttributes()); + Timer apiCallAttemptTimer = MetricUtils.timer(attemptRegistry, SdkDefaultMetric.API_CALL_ATTEMPT_LATENCY); + apiCallAttemptTimer.start(); + beforeExecute(); + CompletableFuture> executeFuture = doExecute(); - executeFuture.whenComplete((resp, err) -> retryIfNeeded(future, resp, err)); + executeFuture.whenComplete((resp, err) -> { + apiCallAttemptTimer.end(); + retryIfNeeded(future, resp, err); + }); return CompletableFutureUtils.forwardExceptionTo(future, executeFuture); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStage.java index 473f6c693dfd..e753abd2a1b3 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeAsyncHttpRequestStage.java @@ -43,6 +43,7 @@ import software.amazon.awssdk.core.internal.http.TransformingAsyncResponseHandler; import software.amazon.awssdk.core.internal.http.async.SimpleHttpContentPublisher; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; +import software.amazon.awssdk.core.internal.http.pipeline.stages.utils.MetricUtils; import software.amazon.awssdk.core.internal.http.timers.TimeoutTracker; import software.amazon.awssdk.core.internal.http.timers.TimerUtils; import software.amazon.awssdk.http.SdkHttpFullRequest; @@ -51,6 +52,8 @@ import software.amazon.awssdk.http.async.AsyncExecuteRequest; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.async.SdkHttpContentPublisher; +import software.amazon.awssdk.metrics.meter.Timer; +import software.amazon.awssdk.metrics.metrics.SdkDefaultMetric; import software.amazon.awssdk.utils.Logger; /** @@ -150,7 +153,11 @@ private CompletableFuture> executeHttpRequest(SdkHttpFullReque .fullDuplex(isFullDuplex(context.executionAttributes())) .build(); + Timer timer = MetricUtils.timer(context.attemptMetricRegistry(), SdkDefaultMetric.HTTP_REQUEST_ROUND_TRIP_LATENCY); + timer.start(); + CompletableFuture httpClientFuture = sdkAsyncHttpClient.execute(executeRequest); + httpClientFuture.whenComplete((r, t) -> timer.end()); TimeoutTracker timeoutTracker = setupAttemptTimer(responseFuture, context); context.apiCallAttemptTimeoutTracker(timeoutTracker); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeHttpRequestStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeHttpRequestStage.java index df52fbd2e149..125a482d5652 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeHttpRequestStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/MakeHttpRequestStage.java @@ -21,12 +21,15 @@ import software.amazon.awssdk.core.internal.http.InterruptMonitor; import software.amazon.awssdk.core.internal.http.RequestExecutionContext; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; +import software.amazon.awssdk.core.internal.http.pipeline.stages.utils.MetricUtils; import software.amazon.awssdk.http.ExecutableHttpRequest; import software.amazon.awssdk.http.HttpExecuteRequest; import software.amazon.awssdk.http.HttpExecuteResponse; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpFullResponse; +import software.amazon.awssdk.metrics.meter.Timer; +import software.amazon.awssdk.metrics.metrics.SdkDefaultMetric; import software.amazon.awssdk.utils.Pair; /** @@ -63,6 +66,8 @@ private HttpExecuteResponse executeHttpRequest(SdkHttpFullRequest request, Reque context.apiCallTimeoutTracker().abortable(requestCallable); context.apiCallAttemptTimeoutTracker().abortable(requestCallable); - return requestCallable.call(); + + Timer timer = MetricUtils.timer(context.attemptMetricRegistry(), SdkDefaultMetric.HTTP_REQUEST_ROUND_TRIP_LATENCY); + return timer.record(() -> requestCallable.call()); } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/RetryableStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/RetryableStage.java index 33f2c0b0f7ee..a00dfa00b0e9 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/RetryableStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/RetryableStage.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.core.internal.http.pipeline.stages; +import static software.amazon.awssdk.core.interceptor.MetricExecutionAttribute.METRIC_REGISTRY; + import java.io.IOException; import java.time.Duration; import java.util.concurrent.TimeUnit; @@ -30,11 +32,15 @@ import software.amazon.awssdk.core.internal.http.RequestExecutionContext; import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline; import software.amazon.awssdk.core.internal.http.pipeline.RequestToResponsePipeline; +import software.amazon.awssdk.core.internal.http.pipeline.stages.utils.MetricUtils; import software.amazon.awssdk.core.internal.retry.ClockSkewAdjuster; import software.amazon.awssdk.core.internal.retry.RetryHandler; import software.amazon.awssdk.core.internal.util.CapacityManager; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.metrics.meter.Timer; +import software.amazon.awssdk.metrics.metrics.SdkDefaultMetric; +import software.amazon.awssdk.metrics.registry.MetricRegistry; import software.amazon.awssdk.utils.Logger; /** @@ -60,7 +66,14 @@ public RetryableStage(HttpClientDependencies dependencies, } public Response execute(SdkHttpFullRequest request, RequestExecutionContext context) throws Exception { - return new RetryExecutor(request, context).execute(); + try { + return new RetryExecutor(request, context).execute(); + } finally { + MetricUtils.timer(context.executionContext().executionAttributes() + .getAttribute(METRIC_REGISTRY), + SdkDefaultMetric.API_CALL) + .end(); + } } /** @@ -82,6 +95,11 @@ private RetryExecutor(SdkHttpFullRequest request, RequestExecutionContext contex public Response execute() throws Exception { while (true) { + + MetricRegistry attemptRegistry = MetricUtils.newRegistry(context.executionAttributes()); + Timer apiCallAttemptTimer = MetricUtils.timer(attemptRegistry, SdkDefaultMetric.API_CALL_ATTEMPT_LATENCY); + apiCallAttemptTimer.start(); + try { beforeExecute(); Response response = doExecute(); @@ -93,6 +111,8 @@ public Response execute() throws Exception { } } catch (SdkClientException | IOException e) { retryHandler.setLastRetriedException(handleThrownException(e)); + } finally { + apiCallAttemptTimer.end(); } } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/SigningStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/SigningStage.java index d121d88b729b..22ca4a8a21cf 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/SigningStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/SigningStage.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.core.internal.http.pipeline.stages; +import static software.amazon.awssdk.metrics.metrics.SdkDefaultMetric.SIGNING_LATENCY; + import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.http.ExecutionContext; @@ -24,9 +26,11 @@ import software.amazon.awssdk.core.internal.http.InterruptMonitor; import software.amazon.awssdk.core.internal.http.RequestExecutionContext; import software.amazon.awssdk.core.internal.http.pipeline.RequestToRequestPipeline; +import software.amazon.awssdk.core.internal.http.pipeline.stages.utils.MetricUtils; import software.amazon.awssdk.core.signer.AsyncRequestBodySigner; import software.amazon.awssdk.core.signer.Signer; import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.metrics.meter.Timer; /** * Sign the marshalled request (if applicable). @@ -46,7 +50,9 @@ public SigningStage(HttpClientDependencies dependencies) { */ public SdkHttpFullRequest execute(SdkHttpFullRequest request, RequestExecutionContext context) throws Exception { InterruptMonitor.checkInterrupted(); - return signRequest(request, context); + + Timer timer = MetricUtils.timer(context.attemptMetricRegistry(), SIGNING_LATENCY); + return timer.record(() -> signRequest(request, context)); } /** diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/MetricUtils.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/MetricUtils.java new file mode 100644 index 000000000000..44a47c6d5a15 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/utils/MetricUtils.java @@ -0,0 +1,112 @@ +/* + * Copyright 2010-2020 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.core.internal.http.pipeline.stages.utils; + +import static software.amazon.awssdk.core.interceptor.MetricExecutionAttribute.ATTEMPT_METRIC_REGISTRY; +import static software.amazon.awssdk.core.interceptor.MetricExecutionAttribute.METRIC_REGISTRY; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.metrics.meter.ConstantGauge; +import software.amazon.awssdk.metrics.meter.Counter; +import software.amazon.awssdk.metrics.meter.Gauge; +import software.amazon.awssdk.metrics.meter.NoOpCounter; +import software.amazon.awssdk.metrics.meter.NoOpGauge; +import software.amazon.awssdk.metrics.meter.NoOpTimer; +import software.amazon.awssdk.metrics.meter.Timer; +import software.amazon.awssdk.metrics.metrics.SdkDefaultMetric; +import software.amazon.awssdk.metrics.registry.MetricBuilderParams; +import software.amazon.awssdk.metrics.registry.MetricRegistry; +import software.amazon.awssdk.metrics.registry.NoOpMetricRegistry; + +/** + * Helper class to register metrics in the {@link MetricRegistry} instances. + */ +@SdkInternalApi +public final class MetricUtils { + + private MetricUtils() { + } + + /** + * Register a {@link Timer} in the #metricRegistry using the information in given {@link SdkDefaultMetric}. + * If there is already a metric registered with {@link SdkDefaultMetric#name()}, the existing {@link Timer} instance is + * returned. + */ + public static Timer timer(MetricRegistry metricRegistry, SdkDefaultMetric metric) { + if (metricRegistry instanceof NoOpMetricRegistry) { + return NoOpTimer.instance(); + } + + return metricRegistry.timer(metric.name(), metricBuilderParams(metric)); + } + + public static Timer startTimer(MetricRegistry metricRegistry, SdkDefaultMetric metric) { + if (metricRegistry instanceof NoOpMetricRegistry) { + return NoOpTimer.instance(); + } + + Timer timer = metricRegistry.timer(metric.name(), metricBuilderParams(metric)); + timer.start(); + + return timer; + } + + /** + * Register a {@link Counter} in the #metricRegistry using the information in given {@link SdkDefaultMetric}. + * If there is already a metric registered with {@link SdkDefaultMetric#name()}, the existing {@link Counter} instance is + * returned. + */ + public static Counter counter(MetricRegistry metricRegistry, SdkDefaultMetric metric) { + if (metricRegistry instanceof NoOpMetricRegistry) { + return NoOpCounter.instance(); + } + + return metricRegistry.counter(metric.name(), metricBuilderParams(metric)); + } + + /** + * Register a {@link ConstantGauge} in the #metricRegistry. Throws an error if there is already a metric registered with + * same {@link SdkDefaultMetric#name()}. + */ + public static Gauge registerConstantGauge(T value, MetricRegistry metricRegistry, SdkDefaultMetric metric) { + if (metricRegistry instanceof NoOpMetricRegistry) { + return NoOpGauge.instance(); + } + + return (ConstantGauge) metricRegistry.register(metric.name(), ConstantGauge.builder() + .value(value) + .categories(metric.categories()) + .build()); + } + + public static MetricRegistry newRegistry(ExecutionAttributes executionAttributes) { + MetricRegistry apiCallMR = executionAttributes.getAttribute(METRIC_REGISTRY); + MetricUtils.counter(apiCallMR, SdkDefaultMetric.API_CALL_ATTEMPT_COUNT) + .increment(); + + // From now on, downstream calls should use this attempt metric registry to record metrics + MetricRegistry attemptMR = apiCallMR.registerApiCallAttemptMetrics(); + executionAttributes.putAttribute(ATTEMPT_METRIC_REGISTRY, attemptMR); + return attemptMR; + } + + private static MetricBuilderParams metricBuilderParams(SdkDefaultMetric metric) { + return MetricBuilderParams.builder() + .categories(metric.categories()) + .build(); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/metrics/MetricsExecutionInterceptor.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/metrics/MetricsExecutionInterceptor.java new file mode 100644 index 000000000000..bde9f45441e7 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/metrics/MetricsExecutionInterceptor.java @@ -0,0 +1,57 @@ +/* + * Copyright 2010-2020 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.core.internal.metrics; + +import static software.amazon.awssdk.core.interceptor.MetricExecutionAttribute.METRIC_CONFIGURATION_PROVIDER; +import static software.amazon.awssdk.core.interceptor.MetricExecutionAttribute.METRIC_PUBLISHER_CONFIGURATION; +import static software.amazon.awssdk.core.interceptor.MetricExecutionAttribute.METRIC_REGISTRY; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.metrics.provider.MetricConfigurationProvider; +import software.amazon.awssdk.metrics.publisher.MetricPublisherConfiguration; +import software.amazon.awssdk.metrics.registry.MetricRegistry; + +/** + * Execution Interceptor to report metrics to the registered publishers after finishing the Api Call + */ +@SdkInternalApi +public final class MetricsExecutionInterceptor implements ExecutionInterceptor { + + @Override + public void afterExecution(Context.AfterExecution context, ExecutionAttributes executionAttributes) { + afterFinalApiCall(executionAttributes); + } + + @Override + public void onExecutionFailure(Context.FailedExecution context, ExecutionAttributes executionAttributes) { + afterFinalApiCall(executionAttributes); + } + + // Handle logic to report event after finishing the request execution + private void afterFinalApiCall(ExecutionAttributes executionAttributes) { + MetricRegistry registry = executionAttributes.getAttribute(METRIC_REGISTRY); + + MetricConfigurationProvider configurationProvider = executionAttributes.getAttribute(METRIC_CONFIGURATION_PROVIDER); + MetricPublisherConfiguration publisherConfiguration = executionAttributes.getAttribute(METRIC_PUBLISHER_CONFIGURATION); + + if (configurationProvider != null && publisherConfiguration != null && configurationProvider.enabled()) { + publisherConfiguration.publishers().forEach(p -> p.registerMetrics(registry)); + } + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/ClockSkewAdjuster.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/ClockSkewAdjuster.java index 323fae8c99b0..b2a53b6cf007 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/ClockSkewAdjuster.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/ClockSkewAdjuster.java @@ -58,4 +58,4 @@ public Integer getAdjustmentInSeconds(SdkHttpResponse response) { return 0; } } -} \ No newline at end of file +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java index b3831d3d7038..8e0e8782598e 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java @@ -32,6 +32,7 @@ import java.net.URI; import java.time.Duration; import java.util.Arrays; +import java.util.List; import java.util.Optional; import org.assertj.core.api.Assertions; import org.junit.Before; @@ -41,8 +42,10 @@ import org.mockito.runners.MockitoJUnitRunner; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; -import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.internal.metrics.MetricsExecutionInterceptor; import software.amazon.awssdk.core.signer.NoOpSigner; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpConfigurationOption; @@ -183,6 +186,22 @@ public void clientBuilderFieldsHaveBeanEquivalents() throws Exception { } + @Test + public void syncClientConfig_AlwaysHas_MetricsInterceptor_AtTheEndOfChain() { + TestClient client = testClientBuilder().buildClient(); + + List interceptors = client.clientConfiguration.option(SdkClientOption.EXECUTION_INTERCEPTORS); + assertThat(interceptors.get(interceptors.size() - 1)).isInstanceOf(MetricsExecutionInterceptor.class); + } + + @Test + public void asyncClientConfig_AlwaysHas_MetricsInterceptor_AtTheEndOfChain() { + TestAsyncClient client = testAsyncClientBuilder().buildClient(); + + List interceptors = client.clientConfiguration.option(SdkClientOption.EXECUTION_INTERCEPTORS); + assertThat(interceptors.get(interceptors.size() - 1)).isInstanceOf(MetricsExecutionInterceptor.class); + } + private SdkDefaultClientBuilder testClientBuilder() { ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/exception/SdkExceptionMessageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/exception/SdkExceptionMessageTest.java index 1a63390e5c3d..1fef5fb54220 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/exception/SdkExceptionMessageTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/exception/SdkExceptionMessageTest.java @@ -47,4 +47,4 @@ public void noMessage_causeWithoutMessage_implies_noMessage() { public void noMessage_causeWithMessage_implies_messageFromCause() { assertThat(SdkException.builder().cause(new Exception("bar")).build().getMessage()).isEqualTo("bar"); } -} \ No newline at end of file +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/InterruptFlagAlwaysClearsTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/InterruptFlagAlwaysClearsTest.java index 79ff2ae5c72a..0c02e17ad790 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/InterruptFlagAlwaysClearsTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/InterruptFlagAlwaysClearsTest.java @@ -168,4 +168,4 @@ protected TestClientHandler(SdkClientConfiguration clientConfiguration) { super(clientConfiguration); } } -} \ No newline at end of file +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/IdempotentTransformingAsyncResponseHandlerTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/IdempotentTransformingAsyncResponseHandlerTest.java index ae79ee4f9f82..ab731eab87db 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/IdempotentTransformingAsyncResponseHandlerTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/IdempotentTransformingAsyncResponseHandlerTest.java @@ -145,4 +145,4 @@ public void prepare_appearsToBeThreadSafe() { verify(mockResponseHandler).prepare(); verifyNoMoreInteractions(mockResponseHandler); } -} \ No newline at end of file +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/AsyncHttpClientApiCallTimeoutTests.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/AsyncHttpClientApiCallTimeoutTests.java index 011b148c68d0..28079c22ac23 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/AsyncHttpClientApiCallTimeoutTests.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/AsyncHttpClientApiCallTimeoutTests.java @@ -102,12 +102,11 @@ public void errorResponse_SlowAfterErrorRequestHandler_ThrowsApiCallTimeoutExcep .httpRequest(request) .build(); - ExecutionContext executionContext = ExecutionContext.builder() - .signer(new NoOpSigner()) - .interceptorChain(interceptors) - .executionAttributes(new ExecutionAttributes()) - .interceptorContext(incerceptorContext) - .build(); + ExecutionContext executionContext = ClientExecutionAndRequestTimerTestUtils.executionContext(null) + .toBuilder() + .interceptorChain(interceptors) + .interceptorContext(incerceptorContext) + .build(); CompletableFuture future = httpClient.requestExecutionBuilder() @@ -176,11 +175,11 @@ private ExecutionContext withInterceptors(ExecutionInterceptor... requestHandler .request(NoopTestRequest.builder().build()) .httpRequest(generateRequest()) .build(); - return ExecutionContext.builder() - .signer(new NoOpSigner()) - .interceptorChain(interceptors) - .executionAttributes(new ExecutionAttributes()) - .interceptorContext(incerceptorContext) - .build(); + + return ClientExecutionAndRequestTimerTestUtils.executionContext(null) + .toBuilder() + .interceptorChain(interceptors) + .interceptorContext(incerceptorContext) + .build(); } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/ClientExecutionAndRequestTimerTestUtils.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/ClientExecutionAndRequestTimerTestUtils.java index ac991bf4a49d..13c764fb069d 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/ClientExecutionAndRequestTimerTestUtils.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/ClientExecutionAndRequestTimerTestUtils.java @@ -28,12 +28,14 @@ import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.ExecutionInterceptorChain; import software.amazon.awssdk.core.interceptor.InterceptorContext; +import software.amazon.awssdk.core.interceptor.MetricExecutionAttribute; import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient; import software.amazon.awssdk.core.internal.http.response.ErrorDuringUnmarshallingResponseHandler; import software.amazon.awssdk.core.internal.http.response.NullErrorResponseHandler; import software.amazon.awssdk.core.signer.NoOpSigner; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.metrics.registry.NoOpMetricRegistry; /** * Useful asserts and utilities for verifying behavior or the client execution timeout and request @@ -114,7 +116,11 @@ public static ExecutionContext executionContext(SdkHttpFullRequest request) { return ExecutionContext.builder() .signer(new NoOpSigner()) .interceptorChain(new ExecutionInterceptorChain(Collections.emptyList())) - .executionAttributes(new ExecutionAttributes()) + .executionAttributes(new ExecutionAttributes() + .putAttribute(MetricExecutionAttribute.METRIC_REGISTRY, + NoOpMetricRegistry.getInstance()) + .putAttribute(MetricExecutionAttribute.ATTEMPT_METRIC_REGISTRY, + NoOpMetricRegistry.getInstance())) .interceptorContext(incerceptorContext) .build(); } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/HttpClientApiCallAttemptTimeoutTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/HttpClientApiCallAttemptTimeoutTest.java index 4c26cbc28c1e..67a53baa0596 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/HttpClientApiCallAttemptTimeoutTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/HttpClientApiCallAttemptTimeoutTest.java @@ -138,11 +138,11 @@ private ExecutionContext withInterceptors(ExecutionInterceptor... requestHandler .request(NoopTestRequest.builder().build()) .httpRequest(generateRequest()) .build(); - return ExecutionContext.builder() - .signer(new NoOpSigner()) - .interceptorChain(interceptors) - .executionAttributes(new ExecutionAttributes()) - .interceptorContext(incerceptorContext) - .build(); + + return ClientExecutionAndRequestTimerTestUtils.executionContext(null) + .toBuilder() + .interceptorChain(interceptors) + .interceptorContext(incerceptorContext) + .build(); } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/HttpClientApiCallTimeoutTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/HttpClientApiCallTimeoutTest.java index 22ba58400417..492faa3d7750 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/HttpClientApiCallTimeoutTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/timers/HttpClientApiCallTimeoutTest.java @@ -138,11 +138,11 @@ private ExecutionContext withInterceptors(ExecutionInterceptor... requestHandler .request(NoopTestRequest.builder().build()) .httpRequest(generateRequest()) .build(); - return ExecutionContext.builder() - .signer(new NoOpSigner()) - .interceptorChain(interceptors) - .executionAttributes(new ExecutionAttributes()) - .interceptorContext(incerceptorContext) - .build(); + + return ClientExecutionAndRequestTimerTestUtils.executionContext(null) + .toBuilder() + .interceptorChain(interceptors) + .interceptorContext(incerceptorContext) + .build(); } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/retry/ClockSkewAdjusterTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/retry/ClockSkewAdjusterTest.java index aa44d45ea34f..ae8d7eaece46 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/retry/ClockSkewAdjusterTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/retry/ClockSkewAdjusterTest.java @@ -56,4 +56,4 @@ private SdkHttpFullResponse responseWithDate(String date) { .putHeader("Date", date) .build(); } -} \ No newline at end of file +} diff --git a/services/codepipeline/src/it/java/software/amazon/awssdk/services/codepipeline/AwsCodePipelineClientIntegrationTest.java b/services/codepipeline/src/it/java/software/amazon/awssdk/services/codepipeline/AwsCodePipelineClientIntegrationTest.java index 179b62a41fe2..34efc4c51dab 100644 --- a/services/codepipeline/src/it/java/software/amazon/awssdk/services/codepipeline/AwsCodePipelineClientIntegrationTest.java +++ b/services/codepipeline/src/it/java/software/amazon/awssdk/services/codepipeline/AwsCodePipelineClientIntegrationTest.java @@ -15,8 +15,6 @@ package software.amazon.awssdk.services.codepipeline; -import static org.hamcrest.Matchers.greaterThan; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.util.List; @@ -33,6 +31,7 @@ import software.amazon.awssdk.services.codepipeline.model.ListActionTypesRequest; import software.amazon.awssdk.services.codepipeline.model.ListActionTypesResponse; import software.amazon.awssdk.services.codepipeline.model.ListPipelinesRequest; +import software.amazon.awssdk.services.codepipeline.model.PipelineSummary; import software.amazon.awssdk.testutils.service.AwsTestBase; /** @@ -45,7 +44,28 @@ public class AwsCodePipelineClientIntegrationTest extends AwsTestBase { @BeforeClass public static void setup() throws Exception { setUpCredentials(); - client = CodePipelineClient.builder().credentialsProvider(CREDENTIALS_PROVIDER_CHAIN).build(); + + client = CodePipelineClient.builder() + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) + .build(); + } + + @Test + public void listActionTypes_WithNoFilter_ReturnsNonEmptyList() { + List summaries = client.listPipelines().pipelines(); + + System.out.println(summaries.size()); + } + + @Test + public void async() throws Exception { + CodePipelineAsyncClient asyncClient = CodePipelineAsyncClient.builder() + .credentialsProvider(CREDENTIALS_PROVIDER_CHAIN) + .build(); + + List summaries = asyncClient.listPipelines().get().pipelines(); + + System.out.println(summaries.size()); } /** @@ -71,10 +91,6 @@ public void listPipelines_WithInvalidNextToken_ThrowsInvalidNextTokenException() client.listPipelines(ListPipelinesRequest.builder().nextToken("invalid_next_token").build()); } - @Test - public void listActionTypes_WithNoFilter_ReturnsNonEmptyList() { - assertThat(client.listActionTypes(ListActionTypesRequest.builder().build()).actionTypes().size(), greaterThan(0)); - } /** * Simple smoke test to create a custom action, make sure it was persisted, and then @@ -83,28 +99,28 @@ public void listActionTypes_WithNoFilter_ReturnsNonEmptyList() { @Test public void createFindDelete_ActionType() { ActionTypeId actionTypeId = ActionTypeId.builder() - .category(ActionCategory.BUILD) - .provider("test-provider") - .version("1") - .owner(ActionOwner.CUSTOM) - .build(); + .category(ActionCategory.BUILD) + .provider("test-provider") + .version("1") + .owner(ActionOwner.CUSTOM) + .build(); ArtifactDetails artifactDetails = ArtifactDetails.builder() - .maximumCount(1) - .minimumCount(1) - .build(); + .maximumCount(1) + .minimumCount(1) + .build(); client.createCustomActionType(CreateCustomActionTypeRequest.builder() - .category(actionTypeId.category()) - .provider(actionTypeId.provider()) - .version(actionTypeId.version()) - .inputArtifactDetails(artifactDetails) - .outputArtifactDetails(artifactDetails) - .build()); + .category(actionTypeId.category()) + .provider(actionTypeId.provider()) + .version(actionTypeId.version()) + .inputArtifactDetails(artifactDetails) + .outputArtifactDetails(artifactDetails) + .build()); final ListActionTypesResponse actionTypes = client.listActionTypes(ListActionTypesRequest.builder().build()); assertTrue(containsActionTypeId(actionTypes.actionTypes(), actionTypeId)); client.deleteCustomActionType(DeleteCustomActionTypeRequest.builder() - .category(actionTypeId.category()) - .provider(actionTypeId.provider()) - .version(actionTypeId.version()).build()); + .category(actionTypeId.category()) + .provider(actionTypeId.provider()) + .version(actionTypeId.version()).build()); } } diff --git a/utils/src/main/java/software/amazon/awssdk/utils/Validate.java b/utils/src/main/java/software/amazon/awssdk/utils/Validate.java index b9c67d1cd0cb..b457521f3546 100644 --- a/utils/src/main/java/software/amazon/awssdk/utils/Validate.java +++ b/utils/src/main/java/software/amazon/awssdk/utils/Validate.java @@ -610,6 +610,21 @@ public static int isNotNegative(int num, String fieldName) { return num; } + /** + * Asserts that the given number is non-negative. + * + * @param num Number to validate + * @param fieldName Field name to display in exception message if not positive. + * @return Number if non-negative. + */ + public static long isNotNegative(long num, String fieldName) { + if (num < 0) { + throw new IllegalArgumentException(String.format("%s must not be negative", fieldName)); + } + + return num; + } + /** * Asserts that the given duration is positive (non-negative and non-zero). *