monasca.thresh.utils.StatsdMetricConsumer.java Source code

Java tutorial

Introduction

Here is the source code for monasca.thresh.utils.StatsdMetricConsumer.java

Source

/*
 * (C) Copyright 2015-2016 Hewlett Packard Enterprise Development Company LP.
 *
 * 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 monasca.thresh.utils;

import java.io.IOException;
import java.io.StringWriter;
import java.lang.Boolean;
import java.lang.String;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.storm.Config;
import org.apache.storm.metric.api.IMetricsConsumer;
import org.apache.storm.task.IErrorReporter;
import org.apache.storm.task.TopologyContext;

import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.timgroup.statsd.NonBlockingUdpSender;
import com.timgroup.statsd.StatsDClientErrorHandler;

public class StatsdMetricConsumer implements IMetricsConsumer {

    public static final String STATSD_HOST = "metrics.statsd.host";
    public static final String STATSD_PORT = "metrics.statsd.port";
    public static final String STATSD_METRICMAP = "metrics.statsd.metricmap";
    public static final String STATSD_WHITELIST = "metrics.statsd.whitelist";
    public static final String STATSD_DIMENSIONS = "metrics.statsd.dimensions";
    public static final String STATSD_DEBUGMETRICS = "metrics.statsd.debugmetrics";

    private String topologyName;
    private String statsdHost = "localhost";
    private int statsdPort = 8125;
    private String monascaStatsdDimPrefix = "|#";
    private List<String> whiteList = new ArrayList<String>();
    private Map<String, String> metricMap = new HashMap<String, String>();
    private Boolean debugMetrics = false;

    String defaultDimensions = new StringBuilder().append(monascaStatsdDimPrefix)
            .append("{\"service\":\"monitoring\",\"component\":\"storm\"}").toString();
    String statsdDimensions = defaultDimensions;

    /*
     * https://github.com/openstack/monasca-agent#statsd
     *
     * Example metric produced from this code from Monasca statsd
     * filtering-bolt.sendqueue.read_pos:69
     * |c|#{"hostname":"localhost","service":"monitoring","component":"storm"}
     *
     * This is the Monasca specific string that adds the dimension element to
     * StatsD
     * |#{"hostname":"localhost","service":"monitoring","component":"storm"}
     *
     * To debug this code:
     * vi /usr/local/lib/python2.7/dist-packages/monasca_agent/statsd/udp.py
     * start():186 log.info('%s' % str(message))
     * sudo service monasca-agent restart
     * tail -f /var/log/monasca/agent/statsd.log
     * /vagrant/tests/smoke.py
     *
     * Note: You only know that "|#" is a delimeter by looking at the Monasca
     * Python Agent code since the Monasca StatsD server is a derivative of what
     * the general purpose StatsD implements and it is executed in the Monasca
     * Agent which was forked from DataDog. It extends UDP data by postfixing a
     * json struct describing the dimensions.
     */

    transient NonBlockingUdpSender udpclient;
    private transient StatsDClientErrorHandler handler;
    private transient Logger logger;

    @Override
    public void prepare(Map stormConf, Object registrationArgument, TopologyContext context,
            IErrorReporter errorReporter) {

        logger = LoggerFactory.getLogger(Logging.categoryFor(getClass(), context));

        /* Sets up locals from the config STATSD_WHITELIST, STATSD_HOST ... */
        parseConfig(stormConf);

        /* Sets up local vars from config vars if present */
        if (registrationArgument instanceof Map) {
            parseConfig((Map<?, ?>) registrationArgument);
        }

        initClient();

        logger.info("topologyName ({}), " + "clean(topologyName) ({})",
                new Object[] { topologyName, clean(topologyName) });
    }

    private void initClient() {
        try {
            handler = statsdErrorHandler;
            udpclient = new NonBlockingUdpSender(statsdHost, statsdPort, Charset.defaultCharset(), handler);
        } catch (IOException e) {
            /* NonBlockingUdpSender only throws an IOException */
            logger.error("{}", e);
        } catch (Exception e) {
            /* General purpose exception */
            logger.error("{}", e);
        }
    }

    StatsDClientErrorHandler statsdErrorHandler = new StatsDClientErrorHandler() {

        @Override
        public void handle(Exception e) {
            logger.error("Error with StatsD UDP client! {}", e);
        }
    };

    @SuppressWarnings("unchecked")
    void parseConfig(Map<?, ?> conf) {
        if (conf.containsKey(Config.TOPOLOGY_NAME)) {
            topologyName = (String) conf.get(Config.TOPOLOGY_NAME);
        }

        if (conf.containsKey(STATSD_HOST)) {
            statsdHost = (String) conf.get(STATSD_HOST);
        }

        if (conf.containsKey(STATSD_PORT)) {
            statsdPort = ((Number) conf.get(STATSD_PORT)).intValue();
        }

        if (conf.containsKey(STATSD_DIMENSIONS)) {
            statsdDimensions = mapToJsonStr((Map<String, String>) conf.get(STATSD_DIMENSIONS));
            if (!isValidJSON(statsdDimensions)) {
                logger.error("Ignoring dimensions element invalid JSON ({})", new Object[] { statsdDimensions });
                // You get default dimensions
                statsdDimensions = monascaStatsdDimPrefix + defaultDimensions;
            } else {
                statsdDimensions = monascaStatsdDimPrefix + statsdDimensions;
            }
        }

        if (conf.containsKey(STATSD_WHITELIST)) {
            whiteList = (List<String>) conf.get(STATSD_WHITELIST);
        }

        if (conf.containsKey(STATSD_METRICMAP)) {
            metricMap = (Map<String, String>) conf.get(STATSD_METRICMAP);
        }

        if (conf.containsKey(STATSD_DEBUGMETRICS)) {
            debugMetrics = (Boolean) conf.get(STATSD_DEBUGMETRICS);
        }
    }

    private String mapToJsonStr(Map<String, String> inputMap) {
        String results = new String();
        ObjectMapper mapper = new ObjectMapper();
        StringWriter sw = new StringWriter();

        try {
            mapper.writeValue(sw, inputMap);
            results = sw.toString();
        } catch (JsonGenerationException e) {
            logger.error("{}", e);
        } catch (JsonMappingException e) {
            logger.error("{}", e);
        } catch (IOException e) {
            logger.error("{}", e);
        }

        return results;
    }

    private boolean isValidJSON(final String json) {
        boolean valid = false;
        try {
            final JsonParser parser = new ObjectMapper().getFactory().createParser(json);
            while (parser.nextToken() != null) {
            }
            valid = true;
        } catch (JsonParseException jpe) {
            valid = false;
        } catch (IOException ioe) {
            valid = false;
        }
        return valid;
    }

    String clean(String s) {
        /* storm metrics look pretty bad so cleanup is needed */
        return s.replace('.', '_').replace('/', '_').replace(':', '_').replaceAll("__", "");
    }

    @Override
    public void handleDataPoints(TaskInfo taskInfo, Collection<DataPoint> dataPoints) {
        for (Metric metric : dataPointsToMetrics(taskInfo, dataPoints)) {
            reportUOM(metric.name, metric.value);
        }
    }

    public static class Metric {
        String name;
        Double value;

        public Metric(String name, Double value) {
            this.name = name;
            this.value = value;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            Metric other = (Metric) obj;
            if (name == null) {
                if (other.name != null)
                    return false;
            } else if (!name.equals(other.name))
                return false;
            if (value != other.value)
                return false;
            return true;
        }

        @Override
        public String toString() {
            return "Metric [name=" + name + ", value=" + value + "]";
        }
    }

    private List<Metric> dataPointsToMetrics(TaskInfo taskInfo, Collection<DataPoint> dataPoints) {
        List<Metric> res = new LinkedList<>();

        StringBuilder sb = new StringBuilder().append(clean(taskInfo.srcComponentId)).append(".");

        int hdrLength = sb.length();

        for (DataPoint p : dataPoints) {

            sb.delete(hdrLength, sb.length());
            sb.append(clean(p.name));

            logger.debug("Storm StatsD metric p.name ({}) p.value ({})", new Object[] { p.name, p.value });

            if (p.value instanceof Number) {
                res.add(new Metric(sb.toString(), ((Number) p.value).doubleValue()));
            }
            // There is a map of data points and it's not empty
            else if (p.value instanceof Map && !(((Map<?, ?>) (p.value)).isEmpty())) {
                int hdrAndNameLength = sb.length();
                @SuppressWarnings("rawtypes")
                Map map = (Map) p.value;
                for (Object subName : map.keySet()) {
                    Object subValue = map.get(subName);
                    if (subValue instanceof Number) {
                        sb.delete(hdrAndNameLength, sb.length());
                        sb.append(".").append(clean(subName.toString()));

                        res.add(new Metric(sb.toString(), ((Number) subValue).doubleValue()));
                    }
                }
            }
        }
        return res;
    }

    /*
     * Since the Java client doesn't support the Monasca metric type we need to
     * build it with a raw UDP request
     */
    public void report(String s) {
        if (udpclient != null) {
            logger.debug("reporting: {}", s);
            udpclient.send(s);
        } else {
            /* Try to setup the UDP client since it was null */
            initClient();
        }
    }

    private void reportUOM(String s, Double number) {
        String metricName = null;
        StringBuilder results = new StringBuilder();
        Boolean published = false;

        if (whiteList.contains(s)) {

            if (!metricMap.isEmpty() && metricMap.containsKey(s)) {
                metricName = metricMap.get(s);
            }
            /* Send the unmapped uom as the same name storm calls it */
            else {
                metricName = s;
            }

            /* Make sure we don't send metric names that may be null or empty */
            if (metricName != null && !metricName.isEmpty()) {
                published = true;
            }
        }

        /*
         * To enable debug message, you also need to add an entry like this:
         *
         * <logger name="monasca.thresh" additivity="false">
         *   <level value="INFO" />
         *   <appender-ref ref="A1" />
         * </logger>
         *
         * Storm/Thresh logger config file:
         *   /opt/storm/apache-storm-0.9.5/logback/cluster.xml
         *
        */

        if (debugMetrics) {
            String mappedName = new String();

            if (!metricMap.isEmpty() && metricMap.containsKey(s)) {
                mappedName = metricMap.get(s);
            } else {
                mappedName = s;
            }

            logger.info(", RawMetricName, {}, MappedMetricName, {}, " + "val, {}, {}",
                    new Object[] { s, mappedName, number, published == true ? "PUBLISHED" : "UNPUBLISHED" });
        }

        if (published) {
            results = results.append(metricName).append(":").append(String.valueOf(number)).append("|c")
                    .append(statsdDimensions);

            report(results.toString());
        }
    }

    @Override
    public void cleanup() {
        udpclient.stop();
    }
}