All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.awssdk.metrics.publishers.cloudwatch.CloudWatchMetricPublisher Maven / Gradle / Ivy

There is a newer version: 2.30.1
Show newest version
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package software.amazon.awssdk.metrics.publishers.cloudwatch;

import static software.amazon.awssdk.metrics.publishers.cloudwatch.internal.CloudWatchMetricLogger.METRIC_LOGGER;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import software.amazon.awssdk.annotations.Immutable;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.metrics.CoreMetric;
import software.amazon.awssdk.metrics.MetricCategory;
import software.amazon.awssdk.metrics.MetricCollection;
import software.amazon.awssdk.metrics.MetricCollector;
import software.amazon.awssdk.metrics.MetricLevel;
import software.amazon.awssdk.metrics.MetricPublisher;
import software.amazon.awssdk.metrics.SdkMetric;
import software.amazon.awssdk.metrics.publishers.cloudwatch.internal.MetricUploader;
import software.amazon.awssdk.metrics.publishers.cloudwatch.internal.task.AggregateMetricsTask;
import software.amazon.awssdk.metrics.publishers.cloudwatch.internal.task.UploadMetricsTasks;
import software.amazon.awssdk.metrics.publishers.cloudwatch.internal.transform.MetricCollectionAggregator;
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient;
import software.amazon.awssdk.services.cloudwatch.model.Dimension;
import software.amazon.awssdk.services.cloudwatch.model.MetricDatum;
import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataRequest;
import software.amazon.awssdk.services.cloudwatch.model.StatisticSet;
import software.amazon.awssdk.utils.ThreadFactoryBuilder;

/**
 * An implementation of {@link MetricPublisher} that aggregates and uploads metrics to Amazon CloudWatch on a periodic basis.
 *
 * 

This simplifies the process of uploading custom metrics to CloudWatch, and can also be configured on the AWS * SDK clients directly to upload AWS SDK-specific metrics (e.g. request latencies, failure rates) to CloudWatch. * *

Overview * *

This publisher aggregates metric data in memory, and periodically uploads it to CloudWatch in a background thread. This * minimizes the work necessary to upload metrics, allowing the caller to focus on collecting the data. * *

The default settings of the metrics publisher are meant to minimize memory usage and CloudWatch cost, while still * providing a useful amount of insight into the metric data. Care should be taken when overriding the default values on the * publisher, because they can result in an associated increased in memory usage and CloudWatch cost. * *

By default, all metrics are uploaded using summary statistics. This means that only count, maximum, minimum, sum and * average data is available in CloudWatch. Metric details (e.g. p90, p99) can be enabled on a per-metric basis using * {@link Builder#detailedMetrics(Collection)}. * *

See {@link Builder} for the configuration values that are available for the publisher, and how they can be used to * increase the functionality or decrease the cost the publisher. * *

Logging * * The CloudWatchMetricPublisher logs all aggregation and upload-related logs to the * {@code software.amazon.awssdk.metrics.publishers.cloudwatch} namespace. To determine how many metrics are being uploaded * successfully without checking the CloudWatch console, you can check for a "success" message at the DEBUG level. At the TRACE * level, you can see exactly which metrics are being uploaded. * *

Configuring AWS SDK clients to upload client metrics * *

* Create a {@link CloudWatchMetricPublisher}, and configure it via * {@link ClientOverrideConfiguration.Builder#addMetricPublisher(MetricPublisher)} * *

 *     CloudWatchMetricPublisher cloudWatchMetricPublisher = CloudWatchMetricPublisher.create();
 *     S3Client s3 = S3Client.builder()
 *                           .overrideConfiguration(o -> o.addMetricPublisher(cloudWatchMetricPublisher))
 *                           .build();
 * 
*

Uploading your own custom metrics * * Step 1: Define which metrics you wish to collect * *

Metrics are described using the {@link SdkMetric#create} method. When you describe your metric, you specify * the name that will appear in CloudWatch and the Java data-type of the metric. The metric should be described once for your * entire application. * *

Supported types: (1) {@link Number} types (e.g. {@link Integer}, {@link Double}, etc.), (2) {@link Duration}. * *

 *     // In this and the following examples, we want to collect metrics about calls to a method we have defined: "myMethod"
 *     public static final class MyMethodMetrics {
 *         // The number of times "myMethod" has been called.
 *         private static final SdkMetric<Integer> MY_METHOD_CALL_COUNT =
 *                 SdkMetric.create("MyMethodCallCount", Integer.class, MetricLevel.INFO, MetricCategory.CUSTOM);
 *
 *         // The amount of time that "myMethod" took to execute.
 *         private static final SdkMetric<Duration> MY_METHOD_LATENCY =
 *                 SdkMetric.create("MyMethodLatency", Duration.class, MetricLevel.INFO, MetricCategory.CUSTOM);
 *     }
 * 
* *

Step 2: Create a {@code CloudWatchMetricPublisher} * *

A {@code CloudWatchMetricPublisher} should be created once for your entire application, and be reused wherever it is * needed. {@code CloudWatchMetricPublisher}s are thread-safe, so there should be no need to create multiple instances. Most * people create and manage the publisher in their inversion-of-control (IoC) container (e.g. Spring/Dagger/Guice). * *

Note: When your application is finished with the {@code CloudWatchMetricPublisher}, make sure to {@link #close()} it. Your * inversion-of-control container may handle this for you on JVM shutdown. * *

See {@link CloudWatchMetricPublisher.Builder} for all available configuration options. * *

 *     // Create a CloudWatchMetricPublisher using a custom namespace.
 *     MetricPublisher metricPublisher = CloudWatchMetricPublisher.builder()
 *                                                                .namespace("MyApplication")
 *                                                                .build();
 * 
* *

Step 3: Collect and Publish Metrics * *

Create and use a {@link MetricCollector} to collect data about your configured metrics. * *

 *     // Call "myMethod" and collect metrics about the call.
 *     Instant methodCallStartTime = Instant.now();
 *     myMethod();
 *     Duration methodCallDuration = Duration.between(methodCallStartTime, Instant.now());
 *
 *     // Write the metrics to the CloudWatchMetricPublisher.
 *     MetricCollector metricCollector = MetricCollector.create("MyMethodCall");
 *     metricCollector.reportMetric(MyCustomMetrics.MY_METHOD_CALL_COUNT, 1);
 *     metricCollector.reportMetric(MyCustomMetrics.MY_METHOD_LATENCY, methodCallDuration);
 *     MetricCollection metricCollection = metricCollector.collect();
 *
 *     metricPublisher.publish(metricCollection);
 * 
* *

Warning: Make sure the {@link #close()} this publisher when it is done being used to release all resources it * consumes. Failure to do so will result in possible thread or file descriptor leaks. */ @ThreadSafe @Immutable @SdkPublicApi public final class CloudWatchMetricPublisher implements MetricPublisher { /** * The maximum queue size for the internal {@link #executor} that is used to aggregate metric data and upload it to * CloudWatch. If this value is too high, memory is wasted. If this value is too low, metrics could be dropped. * * This value is not currently configurable, because it's unlikely that this is a value that customers should need to modify. * If customers really need control over this value, we might consider letting them instead configure the * {@link BlockingQueue} used on the executor. The value here depends on the type of {@code BlockingQueue} in use, and * we should probably not indirectly couple people to the type of blocking queue we're using. */ private static final int MAXIMUM_TASK_QUEUE_SIZE = 128; private static final String DEFAULT_NAMESPACE = "AwsSdk/JavaSdk2"; private static final int DEFAULT_MAXIMUM_CALLS_PER_UPLOAD = 10; private static final Duration DEFAULT_UPLOAD_FREQUENCY = Duration.ofMinutes(1); private static final Set> DEFAULT_DIMENSIONS = Stream.of(CoreMetric.SERVICE_ID, CoreMetric.OPERATION_NAME) .collect(Collectors.toSet()); private static final Set DEFAULT_METRIC_CATEGORIES = Collections.singleton(MetricCategory.ALL); private static final MetricLevel DEFAULT_METRIC_LEVEL = MetricLevel.INFO; private static final Set> DEFAULT_DETAILED_METRICS = Collections.emptySet(); /** * Whether {@link #close()} should call {@link CloudWatchAsyncClient#close()}. This is false when * {@link Builder#cloudWatchClient(CloudWatchAsyncClient)} was specified, meaning the customer has to close the client * themselves. */ private final boolean closeClientWithPublisher; /** * The aggregator that takes {@link MetricCollection}s and converts them into {@link PutMetricDataRequest}s. This aggregator * is *not* thread safe, so it should only ever be accessed from the {@link #executor}'s thread. */ private final MetricCollectionAggregator metricAggregator; /** * The uploader that takes {@link PutMetricDataRequest}s and sends them to a {@link CloudWatchAsyncClient}. */ private final MetricUploader metricUploader; /** * The executor that executes {@link AggregateMetricsTask}s and {@link UploadMetricsTasks}s. */ private final ExecutorService executor; /** * A scheduled executor that periodically schedules a {@link UploadMetricsTasks} on the {@link #executor} thread. Note: this * executor should never execute the flush task itself, because that needs access to the {@link #metricAggregator}, and the * {@code metricAggregator} should only ever be accessed from the {@link #executor} thread. */ private final ScheduledExecutorService scheduledExecutor; /** * The maximum number of {@link PutMetricDataRequest}s that should ever be executed as part of a single * {@link UploadMetricsTasks}. */ private final int maximumCallsPerUpload; private CloudWatchMetricPublisher(Builder builder) { this.closeClientWithPublisher = resolveCloseClientWithPublisher(builder); this.metricAggregator = new MetricCollectionAggregator(resolveNamespace(builder), resolveDimensions(builder), resolveMetricCategories(builder), resolveMetricLevel(builder), resolveDetailedMetrics(builder)); this.metricUploader = new MetricUploader(resolveClient(builder)); this.maximumCallsPerUpload = resolveMaximumCallsPerUpload(builder); ThreadFactory threadFactory = new ThreadFactoryBuilder().threadNamePrefix("cloud-watch-metric-publisher").build(); this.scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory); // Do not increase above 1 thread: access to MetricCollectionAggregator is not thread safe. this.executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(MAXIMUM_TASK_QUEUE_SIZE), threadFactory); long flushFrequencyInMillis = resolveUploadFrequency(builder).toMillis(); this.scheduledExecutor.scheduleAtFixedRate(this::flushMetricsQuietly, flushFrequencyInMillis, flushFrequencyInMillis, TimeUnit.MILLISECONDS); } private Set resolveMetricCategories(Builder builder) { return builder.metricCategories == null ? DEFAULT_METRIC_CATEGORIES : new HashSet<>(builder.metricCategories); } private MetricLevel resolveMetricLevel(Builder builder) { return builder.metricLevel == null ? DEFAULT_METRIC_LEVEL : builder.metricLevel; } private Set> resolveDetailedMetrics(Builder builder) { return builder.detailedMetrics == null ? DEFAULT_DETAILED_METRICS : new HashSet<>(builder.detailedMetrics); } private Set> resolveDimensions(Builder builder) { return builder.dimensions == null ? DEFAULT_DIMENSIONS : new HashSet<>(builder.dimensions); } private boolean resolveCloseClientWithPublisher(Builder builder) { return builder.client == null; } private CloudWatchAsyncClient resolveClient(Builder builder) { return builder.client == null ? CloudWatchAsyncClient.create() : builder.client; } private Duration resolveUploadFrequency(Builder builder) { return builder.uploadFrequency == null ? DEFAULT_UPLOAD_FREQUENCY : builder.uploadFrequency; } private String resolveNamespace(Builder builder) { return builder.namespace == null ? DEFAULT_NAMESPACE : builder.namespace; } private int resolveMaximumCallsPerUpload(Builder builder) { return builder.maximumCallsPerUpload == null ? DEFAULT_MAXIMUM_CALLS_PER_UPLOAD : builder.maximumCallsPerUpload; } @Override public void publish(MetricCollection metricCollection) { try { executor.submit(new AggregateMetricsTask(metricAggregator, metricCollection)); } catch (RejectedExecutionException e) { METRIC_LOGGER.warn(() -> "Some AWS SDK client-side metrics have been dropped because an internal executor did not " + "accept them. This usually occurs because your publisher has been shut down or you have " + "generated too many requests for the publisher to handle in a timely fashion.", e); } } /** * Flush the metrics (via a {@link UploadMetricsTasks}). In the event that the {@link #executor} task queue is full, this * this will retry automatically. * * This returns when the {@code UploadMetricsTask} has been submitted to the executor. The returned future is completed * when the metrics upload to cloudwatch has started. The inner-most future is finally completed when the upload to cloudwatch * has finished. */ private Future> flushMetrics() throws InterruptedException { while (!executor.isShutdown()) { try { return executor.submit(new UploadMetricsTasks(metricAggregator, metricUploader, maximumCallsPerUpload)); } catch (RejectedExecutionException e) { Thread.sleep(100); } } return CompletableFuture.completedFuture(CompletableFuture.completedFuture(null)); } private void flushMetricsQuietly() { try { flushMetrics(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); METRIC_LOGGER.error(() -> "Interrupted during metric flushing.", e); } } @Override public void close() { try { scheduledExecutor.shutdownNow(); Future> flushFuture = flushMetrics(); executor.shutdown(); flushFuture.get(60, TimeUnit.SECONDS) // Wait for flush to start .get(60, TimeUnit.SECONDS); // Wait for flush to finish if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { throw new TimeoutException("Internal executor did not shut down in 60 seconds."); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); METRIC_LOGGER.error(() -> "Interrupted during graceful metric publisher shutdown.", e); } catch (ExecutionException e) { METRIC_LOGGER.error(() -> "Failed during graceful metric publisher shutdown.", e); } catch (TimeoutException e) { METRIC_LOGGER.error(() -> "Timed out during graceful metric publisher shutdown.", e); } finally { runQuietly(scheduledExecutor::shutdownNow, "shutting down scheduled executor"); runQuietly(executor::shutdownNow, "shutting down executor"); runQuietly(() -> metricUploader.close(closeClientWithPublisher), "closing metric uploader"); } } private void runQuietly(Runnable runnable, String taskName) { try { runnable.run(); } catch (Exception e) { METRIC_LOGGER.warn(() -> "Failed while " + taskName + ".", e); } } /** * Create a new {@link Builder} that can be used to create {@link CloudWatchMetricPublisher}s. */ public static Builder builder() { return new Builder(); } /** * Create a {@link CloudWatchMetricPublisher} using all default values. */ public static CloudWatchMetricPublisher create() { return builder().build(); } /** * Returns {@code true} when the internal executors for this publisher are shut down. */ boolean isShutdown() { return scheduledExecutor.isShutdown() && executor.isShutdown(); } /** * Builder class to construct {@link CloudWatchMetricPublisher} instances. See the individual properties for which * configuration settings are available. */ public static final class Builder { private CloudWatchAsyncClient client; private Duration uploadFrequency; private String namespace; private Integer maximumCallsPerUpload; private Collection> dimensions; private Collection metricCategories; private MetricLevel metricLevel; private Collection> detailedMetrics; private Builder() { } /** * Configure the {@link PutMetricDataRequest#namespace()} used for all put-metric-data calls from this publisher. * *

If this is not specified, {@code AwsSdk/JavaSdk2} will be used. */ public Builder namespace(String namespace) { this.namespace = namespace; return this; } /** * Configure the {@link CloudWatchAsyncClient} instance that should be used to communicate with CloudWatch. * *

If this is not specified, the {@code CloudWatchAsyncClient} will be created via * {@link CloudWatchAsyncClient#create()} (and will be closed when {@link #close()} is invoked). * *

If you specify a {@code CloudWatchAsyncClient} via this method, it will not be closed when this publisher * is closed. You will need to need to manage the lifecycle of the client yourself. */ public Builder cloudWatchClient(CloudWatchAsyncClient client) { this.client = client; return this; } /** * Configure the frequency at which aggregated metrics are uploaded to CloudWatch and released from memory. * *

If this is not specified, metrics will be uploaded once per minute. * *

Smaller values will: (1) reduce the amount of memory used by the library (particularly when * {@link #detailedMetrics(Collection)} are enabled), (2) increase the number of CloudWatch calls (and therefore * increase CloudWatch usage cost). * *

Larger values will: (1) increase the amount of memory used by the library (particularly when * {@code detailedMetrics} are enabled), (2) increase the time it takes for metric data to appear in * CloudWatch, (3) reduce the number of CloudWatch calls (and therefore decrease CloudWatch usage cost). * *

Warning: When {@code detailedMetrics} are enabled, all unique metric values are stored in memory until they * can be published to CloudWatch. A high {@code uploadFrequency} with multiple {@code detailedMetrics} enabled can * quickly consume heap memory while the values wait to be published to CloudWatch. In memory constrained environments, it * is recommended to minimize the number of {@code detailedMetrics} configured on the publisher, or to upload metric data * more frequently. As with all performance and resource concerns, profiling in a production-like environment is * encouraged. */ public Builder uploadFrequency(Duration uploadFrequency) { this.uploadFrequency = uploadFrequency; return this; } /** * Configure the maximum number of {@link CloudWatchAsyncClient#putMetricData(PutMetricDataRequest)} calls that an * individual "upload" event can make to CloudWatch. Any metrics that would exceed this limit are dropped during the * upload, logging a warning on the {@code software.amazon.awssdk.metrics.publishers.cloudwatch} namespace. * *

The SDK will always attempt to maximize the number of metrics per put-metric-data call, but uploads will be split * into multiple put-metric-data calls if they include a lot of different metrics or if there are a lot of high-value- * distribution {@link #detailedMetrics(Collection)} being monitored. * *

This value combined with the {@link #uploadFrequency(Duration)} effectively provide a "hard cap" on the number of * put-metric-data calls, to prevent unbounded cost in the event that too many metrics are enabled by the user. * *

If this is not specified, put-metric-data calls will be capped at 10 per upload. */ public Builder maximumCallsPerUpload(Integer maximumCallsPerUpload) { this.maximumCallsPerUpload = maximumCallsPerUpload; return this; } /** * Configure the {@link SdkMetric}s that are used to define the {@link Dimension}s metrics are aggregated under. * *

If this is not specified, {@link CoreMetric#SERVICE_ID} and {@link CoreMetric#OPERATION_NAME} are used, allowing * you to compare metrics for different services and operations. * *

Warning: Configuring the dimensions incorrectly can result in a large increase in the number of unique * metrics and put-metric-data calls to cloudwatch, which have an associated monetary cost. Be sure you're choosing your * metric dimensions wisely, and that you always evaluate the cost of modifying these values on your monthly usage costs. * *

Example useful settings: *

    *
  • {@code CoreMetric.SERVICE_ID} and {@code CoreMetric.OPERATION_NAME} (default): Separate metrics by service and * operation, so that you can compare latencies between AWS services and operations.
  • *
  • {@code CoreMetric.SERVICE_ID}, {@code CoreMetric.OPERATION_NAME} and {@code CoreMetric.HOST_NAME}: Separate * metrics by service, operation and host so that you can compare latencies across hosts in your fleet. Note: This should * only be used when your fleet is relatively small. Large fleets result in a large number of unique metrics being * generated.
  • *
  • {@code CoreMetric.SERVICE_ID}, {@code CoreMetric.OPERATION_NAME} and {@code HttpMetric.HTTP_CLIENT_NAME}: Separate * metrics by service, operation and HTTP client type so that you can compare latencies between different HTTP client * implementations.
  • *
*/ public Builder dimensions(Collection> dimensions) { this.dimensions = new ArrayList<>(dimensions); return this; } /** * @see #dimensions(SdkMetric[]) */ @SafeVarargs public final Builder dimensions(SdkMetric... dimensions) { return dimensions(Arrays.asList(dimensions)); } /** * Configure the {@link MetricCategory}s that should be uploaded to CloudWatch. * *

If this is not specified, {@link MetricCategory#ALL} is used. * *

All {@link SdkMetric}s are associated with at least one {@code MetricCategory}. This setting determines which * category of metrics uploaded to CloudWatch. Any metrics {@link #publish(MetricCollection)}ed that do not fall under * these configured categories are ignored. * *

Note: If there are {@link #dimensions(Collection)} configured that do not fall under these {@code MetricCategory} * values, the dimensions will NOT be ignored. In other words, the metric category configuration only affects which * metrics are uploaded to CloudWatch, not which values can be used for {@code dimensions}. */ public Builder metricCategories(Collection metricCategories) { this.metricCategories = new ArrayList<>(metricCategories); return this; } /** * @see #metricCategories(Collection) */ public Builder metricCategories(MetricCategory... metricCategories) { return metricCategories(Arrays.asList(metricCategories)); } /** * Configure the {@link MetricLevel} that should be uploaded to CloudWatch. * *

If this is not specified, {@link MetricLevel#INFO} is used. * *

All {@link SdkMetric}s are associated with one {@code MetricLevel}. This setting determines which level of metrics * uploaded to CloudWatch. Any metrics {@link #publish(MetricCollection)}ed that do not fall under these configured * categories are ignored. * *

Note: If there are {@link #dimensions(Collection)} configured that do not fall under this {@code MetricLevel} * values, the dimensions will NOT be ignored. In other words, the metric category configuration only affects which * metrics are uploaded to CloudWatch, not which values can be used for {@code dimensions}. */ public Builder metricLevel(MetricLevel metricLevel) { this.metricLevel = metricLevel; return this; } /** * Configure the set of metrics for which detailed values and counts are uploaded to CloudWatch, instead of summaries. * *

By default, all metrics published to this publisher are summarized using {@link StatisticSet}s. This saves memory, * because it allows the publisher to store a fixed amount of information in memory, no matter how many different metric * values are published. The drawback is that metrics other than count, sum, average, maximum and minimum are not made * available in CloudWatch. The {@code detailedMetrics} setting instructs the publisher to store and publish itemized * {@link MetricDatum#values()} and {@link MetricDatum#counts()}, which enables other metrics like p90 and p99 to be * queried in CloudWatch. * *

Warning: When {@code detailedMetrics} are enabled, all unique metric values are stored in memory until they * can be published to CloudWatch. A high {@code uploadFrequency} with multiple {@code detailedMetrics} enabled can * quickly consume heap memory while the values wait to be published to CloudWatch. In memory constrained environments, it * is recommended to minimize the number of {@code detailedMetrics} configured on the publisher, or to upload metric data * more frequently. As with all performance and resource concerns, profiling in a production-like environment is * encouraged. * *

In addition to additional heap memory usage, detailed metrics can result in more requests being sent to CloudWatch, * which can also introduce additional usage cost. The {@link #maximumCallsPerUpload(Integer)} acts as a safeguard against * too many calls being made, but if you configure multiple {@code detailedMetrics}, you may need to increase the * {@code maximumCallsPerUpload} limit. */ public Builder detailedMetrics(Collection> detailedMetrics) { this.detailedMetrics = new ArrayList<>(detailedMetrics); return this; } /** * @see #detailedMetrics(Collection) */ public Builder detailedMetrics(SdkMetric... detailedMetrics) { return detailedMetrics(Arrays.asList(detailedMetrics)); } /** * Build a {@link CloudWatchMetricPublisher} using the configuration currently configured on this publisher. */ public CloudWatchMetricPublisher build() { return new CloudWatchMetricPublisher(this); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy