ddf.catalog.metrics.SourceMetricsImpl.java Source code

Java tutorial

Introduction

Here is the source code for ddf.catalog.metrics.SourceMetricsImpl.java

Source

/**
 * Copyright (c) Codice Foundation
 *
 * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either
 * version 3 of the License, or any later version. 
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public License is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 *
 **/
package ddf.catalog.metrics;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.yammer.metrics.Histogram;
import com.yammer.metrics.JmxReporter;
import com.yammer.metrics.Meter;
import com.yammer.metrics.Metric;
import com.yammer.metrics.MetricRegistry;

import ddf.catalog.metrics.internal.SourceMetrics;
import ddf.catalog.source.CatalogProvider;
import ddf.catalog.source.FederatedSource;
import ddf.catalog.source.Source;

/**
 * This class manages the metrics for individual {@link CatalogProvider} and {@link FederatedSource}
 * {@link Source}s. These metrics currently include the count of queries, results per query, 
 * and exceptions per {@link Source}.
 * 
 * The metrics and their associated {@link JmxCollector}s are created when the {@link Source} is created
 * and deleted when the {@link Source} is deleted. (The associated RRD file remains available indefinitely
 * and accessible from the Metrics tab in the Web Admin console unless an administrator manually deletes it). 
 * 
 * If a {@link Source} is renamed, i.e., its ID changed, then the {@link Source}'s existing metrics' MBeans 
 * and {@link JmxCollector}s are deleted and new metrics created using the new {@link Source}'s ID. 
 * However, the RRD file for the {@link Source}'s previous source ID remains available and accessible from 
 * the Metrics tab in the Web Admin console unless an administrator manually deletes it.
 * 
 * @author rodgersh
 * @author ddf.isgs@lmco.com
 *
 */
public class SourceMetricsImpl implements SourceMetrics {

    private static final Logger LOGGER = LoggerFactory.getLogger(SourceMetricsImpl.class);

    public static final String JMX_COLLECTOR_FACTORY_PID = "MetricsJmxCollector";

    private static final String ALPHA_NUMERIC_REGEX = "[^a-zA-Z0-9]";

    private static final String RRD_FILENAME_EXTENSION = ".rrd";

    private final MetricRegistry metricsRegistry = new MetricRegistry(MBEAN_PACKAGE_NAME);

    private final JmxReporter reporter = JmxReporter.forRegistry(metricsRegistry).build();

    // The types of Yammer Metrics supported
    private enum MetricType {
        HISTOGRAM, METER
    };

    private ConfigurationAdmin configurationAdmin;

    // Injected list of CatalogProviders and FederatedSources 
    // that is kept updated by container, e.g., with latest sourceIds
    private List<CatalogProvider> catalogProviders = new ArrayList<CatalogProvider>();
    private List<FederatedSource> federatedSources = new ArrayList<FederatedSource>();

    // Map of Source to sourceId - used to detect if sourceId has been changed since last metric update
    private Map<Source, String> sourceToSourceIdMap = new HashMap<Source, String>();

    // Map of sourceId to Source's metric data
    protected Map<String, SourceMetric> metrics = new HashMap<String, SourceMetric>();

    /**
     * 
     * @param configurationAdmin
     */
    public SourceMetricsImpl(ConfigurationAdmin configurationAdmin) {

        LOGGER.trace("INSIDE: SourceTracker constructor");

        this.configurationAdmin = configurationAdmin;
    }

    public List<CatalogProvider> getCatalogProviders() {
        return catalogProviders;
    }

    public void setCatalogProviders(List<CatalogProvider> catalogProviders) {
        this.catalogProviders = catalogProviders;
    }

    public List<FederatedSource> getFederatedSources() {
        return federatedSources;
    }

    public void setFederatedSources(List<FederatedSource> federatedSources) {
        this.federatedSources = federatedSources;
    }

    public void init() {
        LOGGER.trace("INSIDE: init");

        reporter.start();
    }

    public void destroy() {
        LOGGER.trace("INSIDE: destroy");

        reporter.stop();
    }

    @Override
    public void updateMetric(String sourceId, String name, int incrementAmount) {

        LOGGER.debug("sourceId = {},   name = {}", sourceId, name);

        if (StringUtils.isBlank(sourceId) || StringUtils.isBlank(name)) {
            return;
        }

        String mapKey = sourceId + "." + name;
        SourceMetric sourceMetric = metrics.get(mapKey);

        if (sourceMetric == null) {
            // Loop through list of all sources until find the sourceId whose metric is being updated
            boolean created = createMetric(catalogProviders, sourceId);
            if (!created) {
                createMetric(federatedSources, sourceId);
            }
            sourceMetric = metrics.get(mapKey);
        }

        // If this metric already exists, then just update its MBean
        if (sourceMetric != null) {
            LOGGER.debug("CASE 1: Metric already exists for " + mapKey);
            if (sourceMetric.isHistogram()) {
                Histogram metric = (Histogram) sourceMetric.getMetric();
                LOGGER.debug("Updating histogram metric " + name + " by amount of " + incrementAmount);
                metric.update(incrementAmount);
            } else {
                Meter metric = (Meter) sourceMetric.getMetric();
                LOGGER.debug("Updating metric " + name + " by amount of " + incrementAmount);
                metric.mark(incrementAmount);
            }
            return;
        }
    }

    private boolean createMetric(List<? extends Source> sources, String sourceId) {
        for (Source source : sources) {
            if (source.getId().equals(sourceId)) {
                LOGGER.debug("Found sourceId = " + sourceId + " in sources list");
                if (sourceToSourceIdMap.containsKey(source)) {
                    // Source's ID must have changed since it is in this map but not in the metrics map
                    // Delete SourceMetrics for Source's "old" sourceId
                    String oldSourceId = sourceToSourceIdMap.get(source);
                    LOGGER.debug("CASE 2: source " + sourceId + " exists but has oldSourceId = " + oldSourceId);
                    deleteMetric(oldSourceId, QUERIES_TOTAL_RESULTS_SCOPE);
                    deleteMetric(oldSourceId, QUERIES_SCOPE);
                    deleteMetric(oldSourceId, EXCEPTIONS_SCOPE);

                    // Create metrics for Source with new sourceId
                    createMetric(sourceId, QUERIES_TOTAL_RESULTS_SCOPE, MetricType.HISTOGRAM);
                    createMetric(sourceId, QUERIES_SCOPE, MetricType.METER);
                    createMetric(sourceId, EXCEPTIONS_SCOPE, MetricType.METER);

                    // Add Source to map with its new sourceId
                    sourceToSourceIdMap.put(source, sourceId);
                } else {
                    // This is a brand new Source - create metrics for it
                    // (Should rarely happen since Sources typically have their metrics created
                    // when the Source itself is created via the addingSource() method. This could
                    // happen if sourceId = null when Source originally created and then its metric
                    // needs updating because client, e.g., SortedFederationStrategy, knows the 
                    // Source exists.)
                    LOGGER.debug("CASE 3: New source " + sourceId + " detected - creating metrics");
                    createMetric(sourceId, QUERIES_TOTAL_RESULTS_SCOPE, MetricType.HISTOGRAM);
                    createMetric(sourceId, QUERIES_SCOPE, MetricType.METER);
                    createMetric(sourceId, EXCEPTIONS_SCOPE, MetricType.METER);

                    sourceToSourceIdMap.put(source, sourceId);
                }
                return true;
            }
        }

        LOGGER.debug("Did not find source " + sourceId + " in Sources - cannot create metrics");

        return false;
    }

    /**
     * Creates metrics for new CatalogProvider or FederatedSource when they
     * are initially created. Metrics creation includes the JMX MBeans and 
     * associated JmxCollector.
     * 
     * @param source
     * @param props
     */
    public void addingSource(Source source, Map props) {
        LOGGER.trace("ENTERING: addingService");

        if (source == null || StringUtils.isBlank(source.getId())) {
            LOGGER.info("Not adding metrics for NULL or blank source");
            return;
        }

        String sourceId = source.getId();

        LOGGER.debug("sourceId = {}", sourceId);

        createMetric(sourceId, QUERIES_TOTAL_RESULTS_SCOPE, MetricType.HISTOGRAM);
        createMetric(sourceId, QUERIES_SCOPE, MetricType.METER);
        createMetric(sourceId, EXCEPTIONS_SCOPE, MetricType.METER);

        // Add new source to internal map used when updating metrics by sourceId
        sourceToSourceIdMap.put(source, sourceId);

        LOGGER.trace("EXITING: addingService");
    }

    /**
     * Deletes metrics for existing CatalogProvider or FederatedSource when they
    * are deleted. Metrics deletion includes the JMX MBeans and 
    * associated JmxCollector.
    * 
     * @param source
     * @param props
     */
    public void deletingSource(Source source, Map props) {
        LOGGER.trace("ENTERING: deletingService");

        if (source == null || StringUtils.isBlank(source.getId())) {
            LOGGER.info("Not deleting metrics for NULL or blank source");
            return;
        }

        String sourceId = source.getId();

        LOGGER.debug("sourceId = {},    props = {}", sourceId, props);

        deleteMetric(sourceId, QUERIES_TOTAL_RESULTS_SCOPE);
        deleteMetric(sourceId, QUERIES_SCOPE);
        deleteMetric(sourceId, EXCEPTIONS_SCOPE);

        // Delete source from internal map used when updating metrics by sourceId
        sourceToSourceIdMap.remove(source);

        LOGGER.trace("EXITING: deletingService");
    }

    private void createMetric(String sourceId, String mbeanName, MetricType type) {

        // Create source-specific metrics for this source
        // (Must be done prior to creating metrics collector so that
        // JMX MBean exists for collector to detect).
        String key = sourceId + "." + mbeanName;

        // Do not create metric and collector if they already exist for this source.
        // (This can happen for ConnectedSources because they have the same sourceId
        // as the local catalog provider).
        if (!metrics.containsKey(key)) {
            if (type == MetricType.HISTOGRAM) {
                Histogram histogram = metricsRegistry.histogram(MetricRegistry.name(sourceId, mbeanName));
                String pid = createMetricsCollector(sourceId, mbeanName);
                metrics.put(key, new SourceMetric(histogram, sourceId, pid, true));
            } else if (type == MetricType.METER) {
                Meter meter = metricsRegistry.meter(MetricRegistry.name(sourceId, mbeanName));
                String pid = createMetricsCollector(sourceId, mbeanName);
                metrics.put(key, new SourceMetric(meter, sourceId, pid));
            } else {
                LOGGER.debug("Metric " + key + " not created because unknown metric type " + type + " specified.");
            }
        } else {
            LOGGER.debug("Metric " + key + " already exists - not creating again");
        }
    }

    /**
     * Creates the JMX Collector for an associated metric's JMX MBean.
     * 
     * @param sourceId
     * @param collectorName
     * @return the PID of the JmxCollector Managed Service Factory created
     */
    private String createMetricsCollector(String sourceId, String collectorName) {

        LOGGER.trace("ENTERING: createMetricsCollector - sourceId = {},   collectorName = {}", sourceId,
                collectorName);

        String pid = null;

        String rrdPath = getRrdFilename(sourceId, collectorName);

        // Create the Managed Service Factory for the JmxCollector, setting its service properties
        // to the MBean it will poll and the RRD file it will populate
        try {
            Configuration config = configurationAdmin.createFactoryConfiguration(JMX_COLLECTOR_FACTORY_PID, null);
            Dictionary<String, String> props = new Hashtable<String, String>();
            props.put("mbeanName", MBEAN_PACKAGE_NAME + ":name=" + sourceId + "." + collectorName);
            props.put("mbeanAttributeName", "Count");
            props.put("rrdPath", rrdPath);
            props.put("rrdDataSourceName", "data");
            props.put("rrdDataSourceType", "COUNTER");
            config.update(props);
            pid = config.getPid();
            LOGGER.debug("JmxCollector pid = {} for sourceId = {}", pid, sourceId);
        } catch (IOException e) {
            LOGGER.warn("Unable to create " + collectorName + " JmxCollector for source " + sourceId, e);
        }

        LOGGER.trace("EXITING: createMetricsCollector");

        return pid;
    }

    protected String getRrdFilename(String sourceId, String collectorName) {

        // Based on the sourceId and collectorName, generate the name of the RRD file.
        // This RRD file will be of the form "source<sourceId><collectorName>.rrd" with
        // the non-alphanumeric characters stripped out and the next character after any
        // non-alphanumeric capitalized.
        // Example:
        //     Given sourceId = dib30rhel-58 and collectorName = Queries.TotalResults
        //     The resulting RRD filename would be: sourceDib30rhel58QueriesTotalResults.rrd
        String[] sourceIdParts = sourceId.split(ALPHA_NUMERIC_REGEX);
        String newSourceId = "";
        for (String part : sourceIdParts) {
            newSourceId += StringUtils.capitalize(part);
        }
        String rrdPath = "source" + newSourceId + collectorName;
        LOGGER.debug("BEFORE: rrdPath = " + rrdPath);

        // Sterilize RRD path name by removing any non-alphanumeric characters - this would confuse the
        // URL being generated for this RRD path in the Metrics tab of Admin console.
        rrdPath = rrdPath.replaceAll(ALPHA_NUMERIC_REGEX, "");
        rrdPath += RRD_FILENAME_EXTENSION;
        LOGGER.debug("AFTER: rrdPath = " + rrdPath);

        return rrdPath;
    }

    /**
     * Delete the metric's MBean for the specified Source.
     * 
     * @param sourceId
     * @param mbeanName
     */
    private void deleteMetric(String sourceId, String mbeanName) {

        String key = sourceId + "." + mbeanName;
        if (metrics.containsKey(key)) {
            metricsRegistry.remove(MetricRegistry.name(sourceId, mbeanName));
            deleteCollector(sourceId, mbeanName);
            metrics.remove(key);
        } else {
            LOGGER.debug("Did not remove metric " + key + " because it was not in metrics map");
        }
    }

    /**
     * Delete the JmxCollector Managed Service Factory for the specified Source and
     * MBean.
     * 
     * @param sourceId
     * @param metricName
     */
    private void deleteCollector(String sourceId, String metricName) {
        String mapKey = sourceId + "." + metricName;
        SourceMetric sourceMetric = metrics.get(mapKey);
        try {
            Configuration config = configurationAdmin.getConfiguration(sourceMetric.getPid());
            if (config != null) {
                LOGGER.debug("Deleting " + metricName + " JmxCollector for source " + sourceId);
                config.delete();
            }
        } catch (IOException e) {
            LOGGER.warn("Unable to delete " + metricName + " JmxCollector for source " + sourceId, e);
        }
        metrics.remove(mapKey);
    }

    /**
     * Inner class POJO to maintain details of each metric for each Source.
     * 
     * @author rodgersh
     *
     */
    public class SourceMetric {

        // The Yammer Metric
        private Metric metric;

        // The ID of the Source (CatalogProvider or Federated Source) this metric
        // is affiliated with
        private String sourceId;

        // The PID of the JmxCollector Managed Service Factory polling this metric's
        // MBean
        private String pid;

        // Whether this metric is a Histogram or Meter
        private boolean isHistogram = false;

        public SourceMetric(Metric metric, String sourceId, String pid) {
            this(metric, sourceId, pid, false);
        }

        public SourceMetric(Metric metric, String sourceId, String pid, boolean isHistogram) {
            this.metric = metric;
            this.sourceId = sourceId;
            this.pid = pid;
            this.isHistogram = isHistogram;
        }

        public Metric getMetric() {
            return metric;
        }

        public String getPid() {
            return pid;
        }

        public boolean isHistogram() {
            return isHistogram;
        }

    }

}