All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.servo.publish.JmxMetricPoller Maven / Gradle / Ivy

There is a newer version: 0.40.13
Show newest version
/**
 * Copyright 2013 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;

import com.netflix.servo.Metric;
import com.netflix.servo.annotations.DataSourceType;
import com.netflix.servo.monitor.MonitorConfig;
import com.netflix.servo.tag.BasicTagList;
import com.netflix.servo.tag.SmallTagMap;
import com.netflix.servo.tag.StandardTagKeys;
import com.netflix.servo.tag.Tag;
import com.netflix.servo.tag.TagList;
import com.netflix.servo.tag.Tags;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.management.Attribute;
import javax.management.InstanceNotFoundException;
import javax.management.JMException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.TabularData;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Generic poller for fetching simple data from JMX.
 */
public final class JmxMetricPoller implements MetricPoller {

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

    private static final Tag CLASS_TAG = Tags.newTag(
        StandardTagKeys.CLASS_NAME.getKeyName(),
        JmxMetricPoller.class.getCanonicalName());

    private static final String DOMAIN_KEY = "JmxDomain";
    private static final String COMPOSITE_PATH_KEY = "JmxCompositePath";
    private static final String PROP_KEY_PREFIX = "Jmx";

    private final JmxConnector connector;
    private final List queries;
    private final MetricFilter counters;
    private final boolean onlyNumericMetrics;
    private final List defaultTags;

    /**
     * Creates a new instance that polls mbeans matching the provided object
     * name pattern.
     *
     * @param connector  used to get a connection to an MBeanServer
     * @param query      object name pattern for selecting mbeans
     * @param counters   metrics matching this filter will be treated as
     *                   counters, all others will be gauges
     */
    public JmxMetricPoller(
            JmxConnector connector, ObjectName query, MetricFilter counters) {
        this(connector, Collections.singletonList(query), counters, true, null);
    }

    /**
     * Creates a new instance that polls mbeans matching the provided object
     * name patterns.
     *
     * @param connector  used to get a connection to an MBeanServer
     * @param queries    object name patterns for selecting mbeans
     * @param counters   metrics matching this filter will be treated as
     *                   counters, all others will be gauges
     */
    public JmxMetricPoller(
            JmxConnector connector, List queries, MetricFilter counters) {
        this(connector, queries, counters, true, null);
    }

    /**
     * Creates a new instance that polls mbeans matching the provided object
     * name pattern.
     *
     * @param connector  used to get a connection to an MBeanServer
     * @param queries    object name patterns for selecting mbeans
     * @param counters   metrics matching this filter will be treated as
     *                   counters, all others will be gauges
     * @param onlyNumericMetrics only produce metrics that can be converted to a Number
     *                           (filter out all strings, etc)
     * @param defaultTags a list of tags to attach to all metrics, usually
     *                    useful to identify all metrics from a given application or hostname
     */
    public JmxMetricPoller(
            JmxConnector connector, List queries, MetricFilter counters,
            boolean onlyNumericMetrics, List defaultTags) {
        this.connector = connector;
        this.queries = queries;
        this.counters = counters;
        this.onlyNumericMetrics = onlyNumericMetrics;
        this.defaultTags = defaultTags;
    }

    /**
     * Creates a tag list from an object name.
     */
    private TagList createTagList(ObjectName name) {
        Map props = name.getKeyPropertyList();
        SmallTagMap.Builder tagsBuilder = SmallTagMap.builder();
        for (Map.Entry e : props.entrySet()) {
            String key = PROP_KEY_PREFIX + "." + e.getKey();
            tagsBuilder.add(Tags.newTag(key, e.getValue()));
        }
        tagsBuilder.add(Tags.newTag(DOMAIN_KEY, name.getDomain()));
        tagsBuilder.add(CLASS_TAG);
        if (defaultTags != null) {
            for (Tag defaultTag : defaultTags) {
                tagsBuilder.add(defaultTag);
            }
        }
        return new BasicTagList(tagsBuilder.result());
    }

    private static TagList getTagListWithAdditionalTag(TagList tags, Tag extra) {
        return new BasicTagList(SmallTagMap.builder().addAll(tags).add(extra).result());
    }
    /**
     * Create a new metric object and add it to the list.
     */
    private void addMetric(
            List metrics,
            String name,
            TagList tags,
            Object value) {
        long now = System.currentTimeMillis();
        if (onlyNumericMetrics) {
            value = asNumber(value);
        }
        if (value != null) {

            TagList newTags = counters.matches(MonitorConfig.builder(name).withTags(tags).build())
                ? getTagListWithAdditionalTag(tags, DataSourceType.COUNTER)
                : getTagListWithAdditionalTag(tags, DataSourceType.GAUGE);
            Metric m = new Metric(name, newTags, now, value);
            metrics.add(m);
        }
    }

    /**
     * Recursively extracts simple numeric values from composite data objects.
     * The map {@code values} will be populated with a path to the value as
     * the key and the simple object as the value.
     */
    private void extractValues(String path, Map values, CompositeData obj) {
        for (String key : obj.getCompositeType().keySet()) {
            String newPath = (path == null) ? key : path + "." + key;
            Object value = obj.get(key);
            if (value instanceof CompositeData) {
                extractValues(newPath, values, (CompositeData) value);
            } else if (value != null) {
                values.put(newPath, value);
            }
        }
    }

    /**
     * Query the mbean connection and add all metrics that satisfy the filter
     * to the list {@code metrics}.
     */
    private void getMetrics(
            MBeanServerConnection con,
            MetricFilter filter,
            List metrics,
            ObjectName name)
            throws JMException, IOException {
        // Create tags from the object name
        TagList tags = createTagList(name);
        MBeanInfo info = con.getMBeanInfo(name);
        MBeanAttributeInfo[] attrInfos = info.getAttributes();

        // Restrict to attributes that match the filter
        List matchingNames = new ArrayList();
        for (MBeanAttributeInfo attrInfo : attrInfos) {
            String attrName = attrInfo.getName();
            if (filter.matches(new MonitorConfig.Builder(attrName).withTags(tags).build())) {
                matchingNames.add(attrName);
            }
        }
        List attributeList = safelyLoadAttributes(con, name, matchingNames);

        for (Attribute attr : attributeList) {
            String attrName = attr.getName();
            Object obj = attr.getValue();
            if (obj instanceof TabularData) {
                for (Object key : ((TabularData) obj).values()) {
                    if (key instanceof CompositeData) {
                        addTabularMetrics(filter, metrics, tags, attrName, (CompositeData) key);
                    }
                }
            } else if (obj instanceof CompositeData) {
                addCompositeMetrics(filter, metrics, tags, attrName, (CompositeData) obj);
            } else {
                addMetric(metrics, attrName, tags, obj);
            }
        }
    }

    private void addCompositeMetrics(MetricFilter filter, List metrics, TagList tags,
                                     String attrName, CompositeData obj) {
        Map values = new HashMap();
        extractValues(null, values, obj);
        for (Map.Entry e : values.entrySet()) {
            final Tag compositeTag = Tags.newTag(COMPOSITE_PATH_KEY, e.getKey());
            final TagList newTags = getTagListWithAdditionalTag(tags, compositeTag);
            if (filter.matches(MonitorConfig.builder(attrName).withTags(newTags).build())) {
                addMetric(metrics, attrName, newTags, e.getValue());
            }
        }
    }

    private void addTabularMetrics(MetricFilter filter, List metrics, TagList tags,
                                   String attrName, CompositeData obj) {
        Map values = new HashMap();
        // tabular composite data has a value called key and one called value
        values.put(obj.get("key").toString(), obj.get("value"));
        for (Map.Entry e : values.entrySet()) {
            final Tag compositeTag = Tags.newTag(COMPOSITE_PATH_KEY, e.getKey());
            final TagList newTags = getTagListWithAdditionalTag(tags, compositeTag);
            if (filter.matches(MonitorConfig.builder(attrName).withTags(newTags).build())) {
                addMetric(metrics, attrName, newTags, e.getValue());
            }
        }
    }

    /**
     * Try to convert an object into a number. Boolean values will return 1 if
     * true and 0 if false. If the value is null or an unknown data type null
     * will be returned.
     */
    private static Number asNumber(Object value) {
        Number num = null;
        if (value == null) {
            num = null;
        } else if (value instanceof Number) {
            num = (Number) value;
        } else if (value instanceof Boolean) {
            num = ((Boolean) value) ? 1 : 0;
        }
        return num;
    }

    /** {@inheritDoc} */
    public List poll(MetricFilter filter) {
        return poll(filter, false);
    }

    /** {@inheritDoc} */
    public List poll(MetricFilter filter, boolean reset) {
        List metrics = new ArrayList();
        try {
            MBeanServerConnection con = connector.getConnection();
            for (ObjectName query : queries) {
                Set names = con.queryNames(query, null);
                if (names.isEmpty()) {
                    LOGGER.warn("no mbeans matched query: {}", query);
                } else {
                    for (ObjectName name : names) {
                        try {
                            getMetrics(con, filter, metrics, name);
                        } catch (Exception e) {
                            LOGGER.warn("failed to get metrics for: " + name, e);
                        }
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.warn("failed to collect jmx metrics.", e);
        }
        return metrics;
    }

    /**
     * There are issues loading some JMX attributes on some systems. This protects us from a
     * single bad attribute stopping us reading any metrics (or just a random sampling) out of
     * the system.
     */
    private static List safelyLoadAttributes(
            MBeanServerConnection server, ObjectName objectName, List matchingNames) {
        try {
            // first try batch loading all attributes as this is faster
            return batchLoadAttributes(server, objectName, matchingNames);
        } catch (Exception e) {
            // JBOSS ticket: https://issues.jboss.org/browse/AS7-4404

            LOGGER.info("Error batch loading attributes for {} : {}", objectName, e.getMessage());
            // some containers (jboss I am looking at you) fail the entire getAttributes request
            // if one is broken we can get the working attributes if we ask for them individually
            return individuallyLoadAttributes(server, objectName, matchingNames);
        }
    }

    private static List batchLoadAttributes(
            MBeanServerConnection server, ObjectName objectName,  List matchingNames)
            throws InstanceNotFoundException, ReflectionException, IOException {
        final String[] namesArray = matchingNames.toArray(new String[matchingNames.size()]);
        return server.getAttributes(objectName, namesArray).asList();
    }

    private static List individuallyLoadAttributes(
            MBeanServerConnection server, ObjectName objectName, List matchingNames) {
        List attributes = new ArrayList();
        for (String attrName : matchingNames) {
            try {
                Object value = server.getAttribute(objectName, attrName);
                attributes.add(new Attribute(attrName, value));
            } catch (Exception e) {
                LOGGER.info("Couldn't load attribute {} for {} : {}",
                    new Object[] {attrName, objectName, e.getMessage()}, e);
            }
        }
        return attributes;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy