/* * Copyright 2013-2014 the original author or authors. * * 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 org.springframework.cloud.aws.actuate.metrics; import com.amazonaws.handlers.AsyncHandler; import com.amazonaws.services.cloudwatch.AmazonCloudWatchAsync; import com.amazonaws.services.cloudwatch.model.MetricDatum; import com.amazonaws.services.cloudwatch.model.PutMetricDataRequest; import com.amazonaws.services.cloudwatch.model.PutMetricDataResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.SmartLifecycle; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.Assert; import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * A {@link CloudWatchMetricSender} implementation that internally uses a queue * to buffer {@link MetricDatum} values. * * @author Simon Buettner * @author Agim Emruli * @since 1.1 */ public class BufferingCloudWatchMetricSender implements CloudWatchMetricSender, InitializingBean, DisposableBean, SmartLifecycle { private static final Logger LOGGER = LoggerFactory.getLogger(CloudWatchMetricWriter.class); /** * Maximum number of {@link MetricDatum} values per request * limit of CloudWatch. */ private static final int MAX_METRIC_DATA_PER_REQUEST = 20; private static final int FLUSH_TIMEOUT = 1000; private final String namespace; private final int maxBuffer; private final long fixedDelayBetweenRuns; private final AmazonCloudWatchAsync amazonCloudWatchAsync; private final LinkedBlockingQueue<MetricDatum> metricDataBuffer; private ScheduledFuture<?> scheduledFuture; private ThreadPoolTaskScheduler taskScheduler; public BufferingCloudWatchMetricSender(String namespace, int maxBuffer, long fixedDelayBetweenRuns, AmazonCloudWatchAsync amazonCloudWatchAsync) { Assert.hasText(namespace, "Namespace must not be null"); this.namespace = namespace.trim(); this.maxBuffer = maxBuffer; this.fixedDelayBetweenRuns = fixedDelayBetweenRuns; this.amazonCloudWatchAsync = amazonCloudWatchAsync; this.metricDataBuffer = new LinkedBlockingQueue<>(this.maxBuffer); } @Override public void send(MetricDatum metricDatum) { try { this.metricDataBuffer.put(metricDatum); } catch (InterruptedException e) { LOGGER.error("Error adding metric to queue", e); Thread.currentThread().interrupt(); } } public String getNamespace() { return this.namespace; } public int getMaxBuffer() { return this.maxBuffer; } public long getFixedDelayBetweenRuns() { return this.fixedDelayBetweenRuns; } @Override public void afterPropertiesSet() throws Exception { ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.setWaitForTasksToCompleteOnShutdown(true); taskScheduler.afterPropertiesSet(); this.taskScheduler = taskScheduler; } @Override public void destroy() throws Exception { this.taskScheduler.destroy(); } @Override public boolean isAutoStartup() { return true; } @Override public void stop(Runnable callback) { this.stop(); callback.run(); } @Override public void start() { this.scheduledFuture = this.taskScheduler.scheduleWithFixedDelay(new CloudWatchMetricSenderRunnable(), this.fixedDelayBetweenRuns); } @Override public void stop() { if (!this.scheduledFuture.isCancelled()) { this.scheduledFuture.cancel(false); } flushMetrics(); } private void flushMetrics() { Future<?> future = this.taskScheduler.submit(new CloudWatchMetricSenderRunnable()); try { future.get(FLUSH_TIMEOUT, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { LOGGER.error("Error flushing metrics", e); } } @Override public boolean isRunning() { return this.scheduledFuture != null && !this.scheduledFuture.isCancelled() && !this.scheduledFuture.isDone(); } @Override public int getPhase() { return Integer.MAX_VALUE; } private class CloudWatchMetricSenderRunnable implements Runnable { @Override public void run() { try { while (!BufferingCloudWatchMetricSender.this.metricDataBuffer.isEmpty()) { Collection<MetricDatum> metricData = collectNextMetricData(); if (!metricData.isEmpty()) { sendToCloudWatch(metricData); } } } catch (Exception e) { LOGGER.error("Error executing metric collection run.", e); } } private Collection<MetricDatum> collectNextMetricData() { Collection<MetricDatum> metricData = new ArrayList<>(MAX_METRIC_DATA_PER_REQUEST); BufferingCloudWatchMetricSender.this.metricDataBuffer.drainTo(metricData, MAX_METRIC_DATA_PER_REQUEST); return metricData; } private void sendToCloudWatch(Collection<MetricDatum> metricData) { PutMetricDataRequest putMetricDataRequest = new PutMetricDataRequest() .withNamespace(BufferingCloudWatchMetricSender.this.namespace) .withMetricData(metricData); BufferingCloudWatchMetricSender.this.amazonCloudWatchAsync.putMetricDataAsync(putMetricDataRequest, new AsyncHandler<PutMetricDataRequest, PutMetricDataResult>() { @Override public void onError(Exception exception) { LOGGER.error("Error sending metric data.", exception); } @Override public void onSuccess(PutMetricDataRequest request, PutMetricDataResult result) { LOGGER.debug("Published metric with namespace:{}", request.getNamespace()); } }); } } }