/* * Copyright 2014 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License 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 com.netflix.servo.publish.cloudwatch; import com.amazonaws.AmazonServiceException; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.services.cloudwatch.AmazonCloudWatch; import com.amazonaws.services.cloudwatch.AmazonCloudWatchClient; import com.amazonaws.services.cloudwatch.model.Dimension; import com.amazonaws.services.cloudwatch.model.MetricDatum; import com.amazonaws.services.cloudwatch.model.PutMetricDataRequest; import com.netflix.servo.DefaultMonitorRegistry; import com.netflix.servo.Metric; import com.netflix.servo.aws.AwsServiceClients; import com.netflix.servo.monitor.BasicTimer; import com.netflix.servo.monitor.Counter; import com.netflix.servo.monitor.DynamicCounter; import com.netflix.servo.monitor.MonitorConfig; import com.netflix.servo.monitor.StepCounter; import com.netflix.servo.monitor.Stopwatch; import com.netflix.servo.monitor.Timer; import com.netflix.servo.publish.BaseMetricObserver; import com.netflix.servo.tag.BasicTag; import com.netflix.servo.tag.Tag; import com.netflix.servo.tag.TagList; import com.netflix.servo.util.Preconditions; import com.netflix.servo.util.Throwables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.stream.Collectors; /** * Writes observations to Amazon's CloudWatch. */ public class CloudWatchMetricObserver extends BaseMetricObserver { private static final Logger LOG = LoggerFactory.getLogger(CloudWatchMetricObserver.class); /** * Experimentally derived value for the largest exponent that can be sent to cloudwatch * without triggering an InvalidParameterValue exception. See CloudWatchValueTest for the test * program that was used. */ private static final int MAX_EXPONENT = 360; /** * Experimentally derived value for the smallest exponent that can be sent to cloudwatch * without triggering an InvalidParameterValue exception. See CloudWatchValueTest for the test * program that was used. */ private static final int MIN_EXPONENT = -360; /** * Maximum value that can be represented in cloudwatch. */ static final double MAX_VALUE = java.lang.Math.pow(2.0, MAX_EXPONENT); /** * Number of cloudwatch metrics reported. */ private static final Counter METRICS_COUNTER = new StepCounter( new MonitorConfig.Builder("servo.cloudwatch.metrics").build()); /** * Number of cloudwatch put calls. */ private static final Timer PUTS_TIMER = new BasicTimer( new MonitorConfig.Builder("servo.cloudwatch.puts").build()); /** * Number of cloudwatch errors. */ private static final MonitorConfig ERRORS_COUNTER_ID = new MonitorConfig.Builder("servo.cloudwatch.errors").build(); static { DefaultMonitorRegistry.getInstance().register(METRICS_COUNTER); DefaultMonitorRegistry.getInstance().register(PUTS_TIMER); } private int batchSize; private boolean truncateEnabled = false; private final AmazonCloudWatch cloudWatch; private final String cloudWatchNamespace; /** * @param name Unique name of the observer. * @param namespace Namespace to use in CloudWatch. * @param credentials Amazon credentials. * @deprecated use equivalent constructor that accepts an AWSCredentialsProvider. */ @Deprecated public CloudWatchMetricObserver(String name, String namespace, AWSCredentials credentials) { this(name, namespace, new AmazonCloudWatchClient(credentials)); } /** * @param name Unique name of the observer. * @param namespace Namespace to use in CloudWatch. * @param credentials Amazon credentials. * @param batchSize Batch size to send to Amazon. They currently enforce a max of 20. * @deprecated use equivalent constructor that accepts an AWSCredentialsProvider. */ @Deprecated public CloudWatchMetricObserver(String name, String namespace, AWSCredentials credentials, int batchSize) { this(name, namespace, credentials); this.batchSize = batchSize; } /** * @param name Unique name of the observer. * @param namespace Namespace to use in CloudWatch. * @param provider Amazon credentials provider */ public CloudWatchMetricObserver(String name, String namespace, AWSCredentialsProvider provider) { this(name, namespace, AwsServiceClients.cloudWatch(provider)); } /** * @param name Unique name of the observer. * @param namespace Namespace to use in CloudWatch. * @param provider Amazon credentials provider. * @param batchSize Batch size to send to Amazon. They currently enforce a max of 20. */ public CloudWatchMetricObserver(String name, String namespace, AWSCredentialsProvider provider, int batchSize) { this(name, namespace, provider); this.batchSize = batchSize; } /** * @param name Unique name of the observer. * @param namespace Namespace to use in CloudWatch. * @param cloudWatch AWS cloudwatch. */ public CloudWatchMetricObserver(String name, String namespace, AmazonCloudWatch cloudWatch) { super(name); this.cloudWatch = cloudWatch; this.cloudWatchNamespace = namespace; batchSize = 20; } /** * @param name Unique name of the observer. * @param namespace Namespace to use in CloudWatch. * @param cloudWatch AWS cloudwatch. * @param batchSize Batch size to send to Amazon. They currently enforce a max of 20. */ public CloudWatchMetricObserver(String name, String namespace, AmazonCloudWatch cloudWatch, int batchSize) { this(name, namespace, cloudWatch); this.batchSize = batchSize; } /** * @param metrics The list of metrics you want to send to CloudWatch */ public void updateImpl(List<Metric> metrics) { Preconditions.checkNotNull(metrics, "metrics"); List<Metric> batch = new ArrayList<>(batchSize); for (final Metric m : metrics) { if (m.hasNumberValue()) { batch.add(m); if (batch.size() % batchSize == 0) { putMetricData(batch); batch.clear(); } } } if (!batch.isEmpty()) { putMetricData(batch); } } private void putMetricData(List<Metric> batch) { METRICS_COUNTER.increment(batch.size()); final Stopwatch s = PUTS_TIMER.start(); try { cloudWatch.putMetricData(createPutRequest(batch)); } catch (AmazonServiceException e) { final Tag error = new BasicTag("error", e.getErrorCode()); DynamicCounter.increment(ERRORS_COUNTER_ID.withAdditionalTag(error)); } catch (Exception e) { final Tag error = new BasicTag("error", e.getClass().getSimpleName()); DynamicCounter.increment(ERRORS_COUNTER_ID.withAdditionalTag(error)); LOG.error("Error while submitting data for metrics : " + batch, e); } catch (Error e) { final Tag error = new BasicTag("error", e.getClass().getSimpleName()); DynamicCounter.increment(ERRORS_COUNTER_ID.withAdditionalTag(error)); throw Throwables.propagate(e); } finally { s.stop(); } } PutMetricDataRequest createPutRequest(List<Metric> batch) { List<MetricDatum> datumList = batch.stream().map(this::createMetricDatum) .collect(Collectors.toList()); return new PutMetricDataRequest().withNamespace(cloudWatchNamespace) .withMetricData(datumList); } MetricDatum createMetricDatum(Metric metric) { MetricDatum metricDatum = new MetricDatum(); return metricDatum.withMetricName(metric.getConfig().getName()) .withDimensions(createDimensions(metric.getConfig().getTags())) .withUnit("None")//DataSourceTypeToAwsUnit.getUnit(metric.)) .withTimestamp(new Date(metric.getTimestamp())) .withValue(truncate(metric.getNumberValue())); //TODO Need to convert into reasonable units based on DataType } /** * Adjust a double value so it can be successfully written to cloudwatch. This involves capping * values with large exponents to an experimentally determined max value and converting values * with large negative exponents to 0. In addition, NaN values will be converted to 0. */ Double truncate(Number numberValue) { // http://docs.amazonwebservices.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html double doubleValue = numberValue.doubleValue(); if (truncateEnabled) { final int exponent = Math.getExponent(doubleValue); if (Double.isNaN(doubleValue)) { doubleValue = 0.0; } else if (exponent >= MAX_EXPONENT) { doubleValue = (doubleValue < 0.0) ? -MAX_VALUE : MAX_VALUE; } else if (exponent <= MIN_EXPONENT) { doubleValue = 0.0; } } return doubleValue; } List<Dimension> createDimensions(TagList tags) { List<Dimension> dimensionList = new ArrayList<>(tags.size()); for (Tag tag : tags) { dimensionList.add(new Dimension().withName(tag.getKey()).withValue(tag.getValue())); } return dimensionList; } public CloudWatchMetricObserver withTruncateEnabled(boolean truncateEnabled) { this.truncateEnabled = truncateEnabled; return this; } }