Package com.blacklocus.metrics

Source Code of com.blacklocus.metrics.CloudWatchReporter

/**
* Copyright 2013 BlackLocus
*
* 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.blacklocus.metrics;

import com.amazonaws.services.cloudwatch.AmazonCloudWatchAsync;
import com.amazonaws.services.cloudwatch.model.Dimension;
import com.amazonaws.services.cloudwatch.model.MetricDatum;
import com.amazonaws.services.cloudwatch.model.PutMetricDataRequest;
import com.amazonaws.services.cloudwatch.model.StandardUnit;
import com.amazonaws.services.cloudwatch.model.StatisticSet;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Counting;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Metric;
import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Sampling;
import com.codahale.metrics.ScheduledReporter;
import com.codahale.metrics.Snapshot;
import com.codahale.metrics.Timer;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
* <h2>Included dimension</h2>
*
* One dimension is always included with the dimension name defined in {@link #METRIC_TYPE_DIMENSION}. This corresponds
* to a type of metric submissions to CloudWatch.
* <p>
* Refer to [README.md](https://github.com/blacklocus/metrics-cloudwatch/blob/master/README.md) for documentation on
* metric submission types.
*
* <h2>Custom dimensions</h2>
*
* CloudWatch does not aggregate over dimensionChain on custom metrics, see
* http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/cloudwatch_concepts.html#Dimension
* To achieve similar convenience, we can submit metrics in duplicate, once for each tuple of attributes against which
* we would aggregate metrics.
* <p>
* Refer to [README.md](https://github.com/blacklocus/metrics-cloudwatch/blob/master/README.md) for documentation on
* the name-encoded syntax of dimensions and permutable components.
*
*
* @author Jason Dunkelberger (dirkraft)
*/
public class CloudWatchReporter extends ScheduledReporter {

    private static final Logger LOG = LoggerFactory.getLogger(CloudWatchReporter.class);


    /**
     * Delimiter of tokens in the metric name. Plain tokens will be retained as the CloudWatch "Metric Name".
     */
    public static final String NAME_TOKEN_DELIMITER_RGX = "\\s";
    // For building; should qualify against NAME_TOKEN_DELIMITER_RGX
    public static final String NAME_TOKEN_DELIMITER = " ";

    /**
     * Separator of key and value segments of a metric name. These segments will be split into the key and value of
     * a CloudWatch {@link Dimension}.
     */
    public static final String NAME_DIMENSION_SEPARATOR = "=";

    /**
     * If any token, whether a simple string or a dimension pair ends with this marker, then metrics will be sent once
     * with and once without.
     */
    public static final String NAME_PERMUTE_MARKER = "*";

    /**
     * Key of {@link Dimension#name} for the type of metric submission, e.g. gauge, counterSum, meterSum, ...
     */
    public static final String METRIC_TYPE_DIMENSION = "type";


    // Should line up with constants. Name should not contain any special character, and may optionally end with the
    // permute marker.
    public static final String VALID_NAME_TOKEN_RGX = "[^\\s=\\*]+\\*?";
    public static final String VALID_DIMENSION_PART_RGX = "[^\\s=\\*]+";

    /**
     * Carried into to CloudWatch namespace
     */
    private final String metricNamespace;

    private final AmazonCloudWatchAsync cloudWatch;

    private final Map<Counting, Long> lastPolledCounts = new HashMap<Counting, Long>();


    /**
     * @see #CloudWatchReporter(MetricRegistry, String, AmazonCloudWatchAsync)
     */
    public CloudWatchReporter(MetricRegistry registry, AmazonCloudWatchAsync cloudWatch) {
        this(registry, null, cloudWatch);
    }

    /**
     * Creates a new {@link ScheduledReporter} instance.
     *
     * @param registry        the {@link MetricRegistry} containing the metrics this reporter will report
     * @param metricNamespace (optional) CloudWatch metric namespace that all metrics reported by this reporter will
     *                        fall under
     */
    public CloudWatchReporter(MetricRegistry registry,
                              String metricNamespace,
                              AmazonCloudWatchAsync cloudWatch) {

        super(registry, "CloudWatchReporter:" + metricNamespace, ALL, TimeUnit.MINUTES, TimeUnit.MINUTES);

        this.metricNamespace = metricNamespace;
        this.cloudWatch = cloudWatch;
    }


    @Override
    public void report(SortedMap<String, Gauge> gauges,
                       SortedMap<String, Counter> counters,
                       SortedMap<String, Histogram> histograms,
                       SortedMap<String, Meter> meters,
                       SortedMap<String, Timer> timers) {

        try {
            List<MetricDatum> data = new ArrayList<MetricDatum>(
                    gauges.size() + counters.size() + meters.size() + 2 * histograms.size() + 2 * timers.size()
            );
            // something like that


            for (Map.Entry<String, Gauge> gaugeEntry : gauges.entrySet()) {
                reportGauge(gaugeEntry, "gauge", data);
            }

            for (Map.Entry<String, Counter> counterEntry : counters.entrySet()) {
                reportCounter(counterEntry, "counterSum", data);
            }

            for (Map.Entry<String, Meter> meterEntry : meters.entrySet()) {
                reportCounter(meterEntry, "meterSum", data);
            }

            for (Map.Entry<String, Histogram> histogramEntry : histograms.entrySet()) {
                reportCounter(histogramEntry, "histogramCount", data);
                reportSampling(histogramEntry, "histogramSet", 1.0, data);
            }

            for (Map.Entry<String, Timer> timerEntry : timers.entrySet()) {
                reportCounter(timerEntry, "timerCount", data);
                reportSampling(timerEntry, "timerSet", 0.000001, data); // nanos -> millis
            }


            // Each CloudWatch API request may contain at maximum 20 datums.
            List<List<MetricDatum>> dataPartitions = Lists.partition(data, 20);
            List<Future<?>> cloudWatchFutures = new ArrayList<Future<?>>(dataPartitions.size());

            for (List<MetricDatum> dataSubset : dataPartitions) {
                cloudWatchFutures.add(cloudWatch.putMetricDataAsync(new PutMetricDataRequest()
                        .withNamespace(metricNamespace)
                        .withMetricData(dataSubset)));
            }
            for (Future<?> cloudWatchFuture : cloudWatchFutures) {
                // We can't let an exception leak out of here, or else the reporter will cease running per mechanics of
                // java.util.concurrent.ScheduledExecutorService.scheduleAtFixedRate(Runnable, long, long, TimeUnit unit)
                try {
                    // See what happened in case of an error.
                    cloudWatchFuture.get();
                } catch (Exception e) {
                    LOG.error("Exception reporting metrics to CloudWatch. The data sent in this CloudWatch API request " +
                            "may have been discarded.", e);
                }
            }

            LOG.info("Sent {} metric data to CloudWatch. namespace: {}", data.size(), metricNamespace);

        } catch (RuntimeException e) {
            LOG.error("Error marshalling CloudWatch metrics.", e);
        }
    }


    void reportGauge(Map.Entry<String, Gauge> gaugeEntry, String type, List<MetricDatum> data) {
        Gauge gauge = gaugeEntry.getValue();

        Object valueObj = gauge.getValue();
        if (valueObj == null) {
            return;
        }

        String valueStr = valueObj.toString();
        if (NumberUtils.isNumber(valueStr)) {
            final Number value = NumberUtils.createNumber(valueStr);

            DemuxedKey key = new DemuxedKey(gaugeEntry.getKey());
            Iterables.addAll(data, key.newDatums(type, new Function<MetricDatum, MetricDatum>() {
                @Override
                public MetricDatum apply(MetricDatum datum) {
                    return datum.withValue(value.doubleValue());
                }
            }));
        }
    }

    void reportCounter(Map.Entry<String, ? extends Counting> entry, String type, List<MetricDatum> data) {
        Counting metric = entry.getValue();
        final long diff = diffLast(metric);
        if (diff == 0) {
            // Don't submit metrics that have not changed. No reason to keep these alive. Also saves on CloudWatch
            // costs.
            return;
        }

        DemuxedKey key = new DemuxedKey(entry.getKey());
        Iterables.addAll(data, key.newDatums(type, new Function<MetricDatum, MetricDatum>() {
            @Override
            public MetricDatum apply(MetricDatum datum) {
                return datum.withValue((double) diff).withUnit(StandardUnit.Count);
            }
        }));
    }

    /**
     * @param rescale the submitted sum by this multiplier. 1.0 is the identity (no rescale).
     */
    void reportSampling(Map.Entry<String, ? extends Sampling> entry, String type, double rescale, List<MetricDatum> data) {
        Sampling metric = entry.getValue();
        Snapshot snapshot = metric.getSnapshot();
        double scaledSum = sum(snapshot.getValues()) * rescale;
        final StatisticSet statisticSet = new StatisticSet()
                .withSum(scaledSum)
                .withSampleCount((double) snapshot.size())
                .withMinimum((double) snapshot.getMin())
                .withMaximum((double) snapshot.getMax());

        DemuxedKey key = new DemuxedKey(entry.getKey());
        Iterables.addAll(data, key.newDatums(type, new Function<MetricDatum, MetricDatum>() {
            @Override
            public MetricDatum apply(MetricDatum datum) {
                return datum.withStatisticValues(statisticSet);
            }
        }));
    }


    private long diffLast(Counting metric) {
        long count = metric.getCount();

        Long lastCount = lastPolledCounts.get(metric);
        lastPolledCounts.put(metric, count);

        if (lastCount == null) {
            lastCount = 0L;
        }
        return count - lastCount;
    }

    private long sum(long[] values) {
        long sum = 0L;
        for (long value : values) sum += value;
        return sum;
    }


    static final MetricFilter ALL = new MetricFilter() {
        @Override
        public boolean matches(String name, Metric metric) {
            return true;
        }
    };

}
TOP

Related Classes of com.blacklocus.metrics.CloudWatchReporter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.