package org.datadog.jmxfetch; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.management.AttributeNotFoundException; import javax.management.InstanceNotFoundException; import javax.management.MBeanAttributeInfo; import javax.management.MBeanException; import javax.management.ObjectName; import javax.management.ReflectionException; import org.apache.log4j.Logger; public abstract class JMXAttribute { protected static final String ALIAS = "alias"; protected static final String METRIC_TYPE = "metric_type"; protected final static Logger LOGGER = Logger.getLogger(JMXAttribute.class.getName()); private static final List<String> EXCLUDED_BEAN_PARAMS = Arrays.asList("domain", "domain_regex", "bean_name", "bean", "bean_regex", "attribute", "exclude_tags", "tags"); private static final String FIRST_CAP_PATTERN = "(.)([A-Z][a-z]+)"; private static final String ALL_CAP_PATTERN = "([a-z0-9])([A-Z])"; private static final String METRIC_REPLACEMENT = "([^a-zA-Z0-9_.]+)|(^[^a-zA-Z]+)"; private static final String DOT_UNDERSCORE = "_*\\._*"; protected static final String CASSANDRA_DOMAIN = "org.apache.cassandra.metrics"; private MBeanAttributeInfo attribute; private Connection connection; private ObjectName beanName; private String domain; private String beanStringName; private HashMap<String, String> beanParameters; private String attributeName; private LinkedHashMap<Object, Object> valueConversions; protected String[] tags; private Configuration matchingConf; private LinkedList<String> defaultTagsList; private Boolean cassandraAliasing; JMXAttribute(MBeanAttributeInfo attribute, ObjectName beanName, String instanceName, Connection connection, HashMap<String, String> instanceTags, Boolean cassandraAliasing) { this.attribute = attribute; this.beanName = beanName; this.matchingConf = null; this.connection = connection; this.attributeName = attribute.getName(); this.beanStringName = beanName.toString(); this.cassandraAliasing = cassandraAliasing; // A bean name is formatted like that: org.apache.cassandra.db:type=Caches,keyspace=system,cache=HintsColumnFamilyKeyCache // i.e. : domain:bean_parameter1,bean_parameter2 //Note: some beans have a ':' in the name. Example: some.domain:name="some.bean.0.0.0.0:80.some-metric" int splitPosition = beanStringName.indexOf(':'); String domain = beanStringName.substring(0, splitPosition); String beanParameters = beanStringName.substring(splitPosition+1); this.domain = domain; HashMap<String, String> beanParametersHash = getBeanParametersHash(beanParameters); LinkedList<String> beanParametersList = getBeanParametersList(instanceName, beanParametersHash, instanceTags); this.beanParameters = beanParametersHash; this.defaultTagsList = sanitizeParameters(beanParametersList); } /** * Remove tags listed in the 'exclude_tags' list from configuration. */ private void applyTagsBlackList() { Filter include = this.matchingConf.getInclude(); if (include != null) { for (String excludedTagName : include.getExcludeTags()) { for (Iterator<String> it = this.defaultTagsList.iterator(); it.hasNext();) { if (it.next().startsWith(excludedTagName + ":")) { it.remove(); } } } } } /** * Add alias tag from the 'tag_alias' configuration list */ private void addAdditionalTags() { Filter include = this.matchingConf.getInclude(); if (include != null) { for (Map.Entry<String, String> tag : include.getAdditionalTags().entrySet()) { this.defaultTagsList.add(tag.getKey() + ":" + this.replaceByAlias(tag.getValue())); } } } public static HashMap<String, String> getBeanParametersHash(String beanParametersString) { String[] beanParameters = beanParametersString.split(","); HashMap<String, String> beanParamsMap = new HashMap<String, String>(beanParameters.length); for (String param : beanParameters) { String[] paramSplit = param.split("="); if (paramSplit.length > 1) { beanParamsMap.put(new String(paramSplit[0]), new String(paramSplit[1])); } else { beanParamsMap.put(new String(paramSplit[0]), ""); } } return beanParamsMap; } private LinkedList<String> getBeanParametersList(String instanceName, Map<String, String> beanParameters, HashMap<String, String> instanceTags) { LinkedList<String> beanTags = new LinkedList<String>(); beanTags.add("instance:" + instanceName); beanTags.add("jmx_domain:" + domain); if (renameCassandraMetrics()) { beanTags.addAll(getCassandraBeanTags(beanParameters)); } else { for (Map.Entry<String, String> param : beanParameters.entrySet()) { beanTags.add(param.getKey() + ":" + param.getValue()); } } if (instanceTags != null) { for (Map.Entry<String, String> tag : instanceTags.entrySet()) { if (tag.getValue() != null) { beanTags.add(tag.getKey() + ":" + tag.getValue()); } else { beanTags.add(tag.getKey()); } } } return beanTags; } /** * Sanitize MBean parameter names and values, i.e. * - Rename parameter names conflicting with existing tags * - Remove illegal characters */ private static LinkedList<String> sanitizeParameters(LinkedList<String> beanParametersList) { LinkedList<String> defaultTagsList = new LinkedList<String>(); for (String rawBeanParameter: beanParametersList) { // Remove `|` characters String beanParameter = rawBeanParameter.replace("|", ""); // 'host' parameter is renamed to 'bean_host' if (beanParameter.startsWith("host:")) { defaultTagsList.add("bean_host:" + beanParameter.substring("host:".length())); } else if (beanParameter.endsWith(":")) { // If the parameter's value is empty, remove the colon in the tag defaultTagsList.add(beanParameter.substring(0, beanParameter.length() - 1)); } else { defaultTagsList.add(beanParameter); } } return defaultTagsList; } protected Boolean renameCassandraMetrics(){ return cassandraAliasing && domain.equals(CASSANDRA_DOMAIN); } private static Collection<String> getCassandraBeanTags(Map<String, String> beanParameters) { Collection<String> tags = new LinkedList<String>(); for (Map.Entry<String, String> param : beanParameters.entrySet()) { if (param.getKey().equals("name")) { //This is already in the alias continue; } else if (param.getKey().equals("scope")) { String type = beanParameters.get("type"); tags.add(type + ":" + param.getValue()); } else { tags.add(param.getKey() + ":" + param.getValue()); } } return tags; } static String convertMetricName(String metricName) { metricName = metricName.replaceAll(FIRST_CAP_PATTERN, "$1_$2"); metricName = metricName.replaceAll(ALL_CAP_PATTERN, "$1_$2").toLowerCase(); metricName = metricName.replaceAll(METRIC_REPLACEMENT, "_"); metricName = metricName.replaceAll(DOT_UNDERSCORE, ".").trim(); return metricName; } @Override public String toString() { return "Bean name: " + beanStringName + " - Attribute name: " + attributeName + " - Attribute type: " + attribute.getType(); } public abstract LinkedList<HashMap<String, Object>> getMetrics() throws AttributeNotFoundException, InstanceNotFoundException, MBeanException, ReflectionException, IOException; /** * An abstract function implemented in the inherited classes JMXSimpleAttribute and JMXComplexAttribute * * @param conf Configuration a Configuration object that will be used to check if the JMX Attribute match this configuration * @return a boolean that tells if the attribute matches the configuration or not */ public abstract boolean match(Configuration conf); public int getMetricsCount() { try { return this.getMetrics().size(); } catch (Exception e) { LOGGER.warn("Unable to get metrics from " + beanStringName + " - " + attributeName, e); return 0; } } Object getJmxValue() throws AttributeNotFoundException, InstanceNotFoundException, MBeanException, ReflectionException, IOException { return this.connection.getAttribute(this.beanName, this.attribute.getName()); } boolean matchDomain(Configuration conf) { String includeDomain = conf.getInclude().getDomain(); Pattern includeDomainRegex = conf.getInclude().getDomainRegex(); return (includeDomain == null || includeDomain.equals(domain)) && (includeDomainRegex == null || includeDomainRegex.matcher(domain).matches()); } boolean excludeMatchDomain(Configuration conf) { String excludeDomain = conf.getExclude().getDomain(); Pattern excludeDomainRegex = conf.getExclude().getDomainRegex(); return excludeDomain != null && excludeDomain.equals(domain) || excludeDomainRegex != null && excludeDomainRegex.matcher(domain).matches(); } Object convertMetricValue(Object metricValue) { Object converted = metricValue; if (!getValueConversions().isEmpty()) { converted = getValueConversions().get(metricValue); if (converted == null && getValueConversions().get("default") != null) { converted = getValueConversions().get("default"); } } return converted; } double castToDouble(Object metricValue) { Object value = convertMetricValue(metricValue); if (value instanceof String) { return Double.parseDouble((String) value); } else if (value instanceof Integer) { return new Double((Integer) (value)); } else if (value instanceof AtomicInteger) { return new Double(((AtomicInteger) (value)).get()); } else if (value instanceof AtomicLong) { Long l = ((AtomicLong) (value)).get(); return l.doubleValue(); } else if (value instanceof Double) { return (Double) value; } else if (value instanceof Boolean) { return ((Boolean) value ? 1.0 : 0.0); } else if (value instanceof Long) { Long l = new Long((Long) value); return l.doubleValue(); } else if (value instanceof Number) { return ((Number) value).doubleValue(); } else { try { return new Double((Double) value); } catch (Exception e) { throw new NumberFormatException(); } } } private boolean matchBeanRegex(Filter filter, boolean matchIfNoRegex) { ArrayList<Pattern> beanRegexes = filter.getBeanRegexes(); if (beanRegexes.isEmpty()) { return matchIfNoRegex; } for (Pattern beanRegex : beanRegexes) { if(beanRegex.matcher(beanStringName).matches()) { return true; } } return false; } private boolean matchBeanName(Configuration configuration) { Filter include = configuration.getInclude(); if (!include.isEmptyBeanName() && !include.getBeanNames().contains(beanStringName)) { return false; } for (String bean_attr : include.keySet()) { if (EXCLUDED_BEAN_PARAMS.contains(bean_attr)) { continue; } ArrayList<String> beanValues = include.getParameterValues(bean_attr); if (beanParameters.get(bean_attr) == null || !(beanValues.contains(beanParameters.get(bean_attr)))){ return false; } } return true; } private boolean excludeMatchBeanName(Configuration conf) { Filter exclude = conf.getExclude(); ArrayList<String> beanNames = exclude.getBeanNames(); if(beanNames.contains(beanStringName)){ return true; } for (String bean_attr : exclude.keySet()) { if (EXCLUDED_BEAN_PARAMS.contains(bean_attr)) { continue; } if (beanParameters.get(bean_attr) == null) { continue; } ArrayList<String> beanValues = exclude.getParameterValues(bean_attr); for (String beanVal : beanValues) { if (beanParameters.get(bean_attr).equals(beanVal)) { return true; } } } return false; } boolean matchBean(Configuration configuration) { return matchBeanName(configuration) && matchBeanRegex(configuration.getInclude(), true); } boolean excludeMatchBean(Configuration configuration) { return excludeMatchBeanName(configuration) || matchBeanRegex(configuration.getExclude(), false); } @SuppressWarnings("unchecked") HashMap<Object, Object> getValueConversions() { if (valueConversions == null) { Object includedAttribute = matchingConf.getInclude().getAttribute(); if (includedAttribute instanceof LinkedHashMap<?, ?>) { String attributeName = this.attribute.getName(); LinkedHashMap<String, LinkedHashMap<Object, Object>> attribute = ((LinkedHashMap<String, LinkedHashMap<String, LinkedHashMap<Object, Object>>>) includedAttribute).get(attributeName); if (attribute != null) { valueConversions = attribute.get("values"); } } if (valueConversions == null) { valueConversions = new LinkedHashMap<Object, Object>(); } } return valueConversions; } public Configuration getMatchingConf() { return matchingConf; } public void setMatchingConf(Configuration matchingConf) { this.matchingConf = matchingConf; // Now that we have the matchingConf we can: // - add additional tags this.addAdditionalTags(); // - filter out excluded tags this.applyTagsBlackList(); } MBeanAttributeInfo getAttribute() { return attribute; } public ObjectName getBeanName() { return beanName; } /** * Get attribute alias. * * In order, tries to: * * Use `alias_match` to generate an alias with a regular expression * * Use `alias` directly * * Create an generic alias prefixed with user's `metric_prefix` preference or default to `jmx` * * Argument(s): * * (Optional) `field` * `Null` for `JMXSimpleAttribute`. */ protected String getAlias(String field) { String alias = null; Filter include = getMatchingConf().getInclude(); LinkedHashMap<String, Object> conf = getMatchingConf().getConf(); String fullAttributeName =(field!=null)?(getAttribute().getName() + "." + field):(getAttribute().getName()); if (include.getAttribute() instanceof LinkedHashMap<?, ?>) { LinkedHashMap<String, LinkedHashMap<String, String>> attribute = (LinkedHashMap<String, LinkedHashMap<String, String>>) (include.getAttribute()); alias = getUserAlias(attribute, fullAttributeName); } if (alias == null) { if (conf.get("metric_prefix") != null) { alias = conf.get("metric_prefix") + "." + getDomain() + "." + fullAttributeName; } else if (getDomain().startsWith("org.apache.cassandra")) { alias = getCassandraAlias(); } } //If still null - generate generic alias if (alias == null) { alias = "jmx." + getDomain() + "." + fullAttributeName; } alias = convertMetricName(alias); return alias; } /** * Metric name aliasing specific to Cassandra. * * * (Default) `cassandra_aliasing` == False. * Legacy aliasing: drop `org.apache` prefix. * * `cassandra_aliasing` == True * Comply with CASSANDRA-4009 * * More information: https://issues.apache.org/jira/browse/CASSANDRA-4009 */ private String getCassandraAlias() { if (renameCassandraMetrics()) { Map<String, String> beanParameters = getBeanParameters(); String metricName = beanParameters.get("name"); String attributeName = getAttributeName(); if (attributeName.equals("Value")) { return "cassandra." + metricName; } return "cassandra." + metricName + "." + attributeName; } //Deprecated Cassandra metric. Remove domain prefix. return getDomain().replace("org.apache.", "") + "." + getAttributeName(); } /** * Retrieve user defined alias. Substitute regular expression named groups. * * Example: * ``` * bean: org.datadog.jmxfetch.test:foo=Bar,qux=Baz * attribute: * toto: * alias: my.metric.$foo.$attribute * ``` * returns a metric name `my.metric.bar.toto` */ private String getUserAlias(LinkedHashMap<String, LinkedHashMap<String, String>> attribute, String fullAttributeName){ String alias = attribute.get(fullAttributeName).get(ALIAS); if (alias == null) { return null; } alias = this.replaceByAlias(alias); // Attribute & domain alias = alias.replace("$attribute", fullAttributeName); alias = alias.replace("$domain", domain); return alias; } private String replaceByAlias(String alias){ // Bean parameters for (Map.Entry<String, String> param : beanParameters.entrySet()) { alias = alias.replace("$" + param.getKey(), param.getValue()); } return alias; } /** * Overload `getAlias` method. * * Note: used for `JMXSimpleAttribute` only, as `field` is null. */ protected String getAlias(){ return getAlias(null); } @SuppressWarnings("unchecked") protected String[] getTags() { if(tags != null) { return tags; } Filter include = matchingConf.getInclude(); if (include != null) { Object includeAttribute = include.getAttribute(); if (includeAttribute instanceof LinkedHashMap<?, ?>) { LinkedHashMap<String, ArrayList<String>> attributeParams = ((LinkedHashMap<String, LinkedHashMap<String, ArrayList<String>>>)includeAttribute).get(attributeName); if (attributeParams != null) { ArrayList<String> yamlTags = attributeParams.get("tags"); if ( yamlTags != null) { defaultTagsList.addAll(yamlTags); } } } } tags = new String[defaultTagsList.size()]; tags = defaultTagsList.toArray(tags); return tags; } String getBeanStringName() { return beanStringName; } String getAttributeName() { return attributeName; } public static List<String> getExcludedBeanParams(){ return EXCLUDED_BEAN_PARAMS; } protected String getDomain() { return domain; } protected HashMap<String, String> getBeanParameters() { return beanParameters; } }