package org.datadog.jmxfetch;
import java.io.IOException;
import java.lang.ClassCastException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import javax.management.MBeanAttributeInfo;
import javax.management.ObjectName;
import javax.security.auth.login.FailedLoginException;
import org.apache.log4j.Logger;
import org.datadog.jmxfetch.reporter.Reporter;
public class Instance {
private final static Logger LOGGER = Logger.getLogger(Instance.class.getName());
private final static List<String> SIMPLE_TYPES = Arrays.asList("long",
"java.lang.String", "int", "float", "double", "java.lang.Double","java.lang.Float", "java.lang.Integer", "java.lang.Long",
"java.util.concurrent.atomic.AtomicInteger", "java.util.concurrent.atomic.AtomicLong",
"java.lang.Object", "java.lang.Boolean", "boolean", "java.lang.Number");
private final static List<String> COMPOSED_TYPES = Arrays.asList("javax.management.openmbean.CompositeData", "java.util.HashMap", "java.util.Map");
private final static List<String> MULTI_TYPES = Arrays.asList("javax.management.openmbean.TabularData");
private final static int MAX_RETURNED_METRICS = 350;
private final static int DEFAULT_REFRESH_BEANS_PERIOD = 600;
public static final String PROCESS_NAME_REGEX = "process_name_regex";
public static final String ATTRIBUTE = "Attribute: ";
private Set<ObjectName> beans;
private LinkedList<String> beanScopes;
private LinkedList<Configuration> configurationList = new LinkedList<Configuration>();
private LinkedList<JMXAttribute> matchingAttributes;
private HashSet<JMXAttribute> failingAttributes;
private Integer refreshBeansPeriod;
private long lastCollectionTime;
private Integer minCollectionPeriod;
private long lastRefreshTime;
private LinkedHashMap<String, Object> yaml;
private LinkedHashMap<String, Object> initConfig;
private String instanceName;
private LinkedHashMap<String, String> tags;
private String checkName;
private int maxReturnedMetrics;
private boolean limitReached;
private Connection connection;
private AppConfig appConfig;
private Boolean cassandraAliasing;
public Instance(Instance instance, AppConfig appConfig) {
this(instance.getYaml() != null
? new LinkedHashMap<String, Object>(instance.getYaml())
: null,
instance.getInitConfig() != null
? new LinkedHashMap<String, Object>(instance.getInitConfig())
: null,
instance.getCheckName(),
appConfig);
}
@SuppressWarnings("unchecked")
public Instance(LinkedHashMap<String, Object> yamlInstance, LinkedHashMap<String, Object> initConfig,
String checkName, AppConfig appConfig) {
this.appConfig = appConfig;
this.yaml = yamlInstance != null ? new LinkedHashMap<String, Object>(yamlInstance) : null;
this.initConfig = initConfig != null ? new LinkedHashMap<String, Object>(initConfig) : null;
this.instanceName = (String) yaml.get("name");
this.tags = getTagsMap(yaml.get("tags"));
this.checkName = checkName;
this.matchingAttributes = new LinkedList<JMXAttribute>();
this.failingAttributes = new HashSet<JMXAttribute>();
this.refreshBeansPeriod = (Integer) yaml.get("refresh_beans");
if (this.refreshBeansPeriod == null) {
this.refreshBeansPeriod = DEFAULT_REFRESH_BEANS_PERIOD; // Make sure to refresh the beans list every 10 minutes
// Useful because sometimes if the application restarts, jmxfetch might read
// a jmxtree that is not completely initialized and would be missing some attributes
}
this.minCollectionPeriod = (Integer) yaml.get("min_collection_period");
if (this.minCollectionPeriod == null) {
this.minCollectionPeriod = (Integer) initConfig.get("min_collection_period");
}
this.lastCollectionTime = 0;
this.lastRefreshTime = 0;
this.limitReached = false;
Object maxReturnedMetrics = this.yaml.get("max_returned_metrics");
if (maxReturnedMetrics == null) {
this.maxReturnedMetrics = MAX_RETURNED_METRICS;
} else {
this.maxReturnedMetrics = (Integer) maxReturnedMetrics;
}
// Generate an instance name that will be send as a tag with the metrics
if (this.instanceName == null) {
if (this.yaml.get(PROCESS_NAME_REGEX) != null) {
this.instanceName = this.checkName + "-" + this.yaml.get(PROCESS_NAME_REGEX);
} else if (this.yaml.get("host") != null) {
this.instanceName = this.checkName + "-" + this.yaml.get("host") + "-" + this.yaml.get("port");
} else {
LOGGER.warn("Cannot determine a unique instance name. Please define a name in your instance configuration");
this.instanceName = this.checkName;
}
}
// Alternative aliasing for CASSANDRA-4009 metrics
// More information: https://issues.apache.org/jira/browse/CASSANDRA-4009
this.cassandraAliasing = (Boolean) yaml.get("cassandra_aliasing");
if (this.cassandraAliasing == null){
this.cassandraAliasing = false;
}
// In case the configuration to match beans is not specified in the "instance" parameter but in the initConfig one
Object yamlConf = this.yaml.get("conf");
if (yamlConf == null && this.initConfig != null) {
yamlConf = this.initConfig.get("conf");
}
if (yamlConf == null) {
LOGGER.warn("Cannot find a \"conf\" section in " + this.instanceName);
} else {
for (LinkedHashMap<String, Object> conf : (ArrayList<LinkedHashMap<String, Object>>) (yamlConf)) {
configurationList.add(new Configuration(conf));
}
}
// Add the configuration to get the default basic metrics from the JVM
configurationList.add(new Configuration((LinkedHashMap<String, Object>) new YamlParser(this.getClass().getResourceAsStream("/jmx-1.yaml")).getParsedYaml()));
configurationList.add(new Configuration((LinkedHashMap<String, Object>) new YamlParser(this.getClass().getResourceAsStream("/jmx-2.yaml")).getParsedYaml()));
}
/**
* Format the instance tags defined in the YAML configuration file to a `LinkedHashMap`.
* Supported inputs: `List`, `Map`.
*/
private static LinkedHashMap<String, String> getTagsMap(Object yamlTags){
try {
// Input has `Map` format
return (LinkedHashMap<String, String>) yamlTags;
}
catch (ClassCastException e){
// Input has `List` format
LinkedHashMap<String, String> tags = new LinkedHashMap<String, String>();
for (String tag: (List<String>)yamlTags) {
tags.put(tag, null);
}
return tags;
}
}
public Connection getConnection(LinkedHashMap<String, Object> connectionParams, boolean forceNewConnection) throws IOException {
if (connection == null || !connection.isAlive()) {
LOGGER.info("Connection closed or does not exist. Creating a new connection!");
return ConnectionFactory.createConnection(connectionParams);
} else if (forceNewConnection) {
LOGGER.info("Forcing the creation of a new connection");
connection.closeConnector();
return ConnectionFactory.createConnection(connectionParams);
}
return connection;
}
public void init(boolean forceNewConnection) throws IOException, FailedLoginException, SecurityException {
LOGGER.info("Trying to connect to JMX Server at " + this.toString());
connection = getConnection(yaml, forceNewConnection);
LOGGER.info("Connected to JMX Server at " + this.toString());
this.refreshBeansList();
this.getMatchingAttributes();
}
@Override
public String toString() {
if (this.yaml.get(PROCESS_NAME_REGEX) != null) {
return "process_regex: `" + this.yaml.get(PROCESS_NAME_REGEX) + "`";
} else if (this.yaml.get("jmx_url") != null) {
return (String) this.yaml.get("jmx_url");
} else {
return this.yaml.get("host") + ":" + this.yaml.get("port");
}
}
public LinkedList<HashMap<String, Object>> getMetrics() throws IOException {
// We can force to refresh the bean list every x seconds in case of ephemeral beans
// To enable this, a "refresh_beans" parameter must be specified in the yaml config file
if (this.refreshBeansPeriod != null && (System.currentTimeMillis() - this.lastRefreshTime) / 1000 > this.refreshBeansPeriod) {
LOGGER.info("Refreshing bean list");
this.refreshBeansList();
this.getMatchingAttributes();
}
LinkedList<HashMap<String, Object>> metrics = new LinkedList<HashMap<String, Object>>();
Iterator<JMXAttribute> it = matchingAttributes.iterator();
// increment the lastCollectionTime
this.lastCollectionTime = System.currentTimeMillis();
while (it.hasNext()) {
JMXAttribute jmxAttr = it.next();
try {
LinkedList<HashMap<String, Object>> jmxAttrMetrics = jmxAttr.getMetrics();
for (HashMap<String, Object> m : jmxAttrMetrics) {
m.put("check_name", this.checkName);
metrics.add(m);
}
if (this.failingAttributes.contains(jmxAttr)) {
this.failingAttributes.remove(jmxAttr);
}
} catch (IOException e) {
throw e;
} catch (Exception e) {
LOGGER.debug("Cannot get metrics for attribute: " + jmxAttr, e);
if (this.failingAttributes.contains(jmxAttr)) {
LOGGER.debug("Cannot generate metrics for attribute: " + jmxAttr + " twice in a row. Removing it from the attribute list");
it.remove();
} else {
this.failingAttributes.add(jmxAttr);
}
}
}
return metrics;
}
public boolean timeToCollect() {
if (this.minCollectionPeriod == null) {
return true;
} else if ((System.currentTimeMillis() - this.lastCollectionTime) / 1000 < this.minCollectionPeriod) {
return false;
} else {
return true;
}
}
private void getMatchingAttributes() {
limitReached = false;
Reporter reporter = appConfig.getReporter();
String action = appConfig.getAction();
boolean metricReachedDisplayed = false;
this.matchingAttributes.clear();
this.failingAttributes.clear();
int metricsCount = 0;
if (!action.equals(AppConfig.ACTION_COLLECT)) {
reporter.displayInstanceName(this);
}
for (ObjectName beanName : beans) {
if (limitReached) {
LOGGER.debug("Limit reached");
if (action.equals(AppConfig.ACTION_COLLECT)) {
break;
}
}
MBeanAttributeInfo[] attributeInfos;
try {
// Get all the attributes for bean_name
LOGGER.debug("Getting attributes for bean: " + beanName);
attributeInfos = connection.getAttributesForBean(beanName);
} catch (Exception e) {
LOGGER.warn("Cannot get bean attributes " + e.getMessage());
continue;
}
for (MBeanAttributeInfo attributeInfo : attributeInfos) {
if (metricsCount >= maxReturnedMetrics) {
limitReached = true;
if (action.equals(AppConfig.ACTION_COLLECT)) {
LOGGER.warn("Maximum number of metrics reached.");
break;
} else if (!metricReachedDisplayed &&
!action.equals(AppConfig.ACTION_LIST_COLLECTED) &&
!action.equals(AppConfig.ACTION_LIST_NOT_MATCHING)) {
reporter.displayMetricReached();
metricReachedDisplayed = true;
}
}
JMXAttribute jmxAttribute;
String attributeType = attributeInfo.getType();
if (SIMPLE_TYPES.contains(attributeType)) {
LOGGER.debug(ATTRIBUTE + beanName + " : " + attributeInfo + " has attributeInfo simple type");
jmxAttribute = new JMXSimpleAttribute(attributeInfo, beanName, instanceName, connection, tags, cassandraAliasing);
} else if (COMPOSED_TYPES.contains(attributeType)) {
LOGGER.debug(ATTRIBUTE + beanName + " : " + attributeInfo + " has attributeInfo composite type");
jmxAttribute = new JMXComplexAttribute(attributeInfo, beanName, instanceName, connection, tags);
} else if (MULTI_TYPES.contains(attributeType)) {
LOGGER.debug(ATTRIBUTE + beanName + " : " + attributeInfo + " has attributeInfo tabular type");
jmxAttribute = new JMXTabularAttribute(attributeInfo, beanName, instanceName, connection, tags);
} else {
try {
LOGGER.debug(ATTRIBUTE + beanName + " : " + attributeInfo + " has an unsupported type: " + attributeType);
} catch (NullPointerException e) {
LOGGER.warn("Caught unexpected NullPointerException");
}
continue;
}
// For each attribute we try it with each configuration to see if there is one that matches
// If so, we store the attribute so metrics will be collected from it. Otherwise we discard it.
for (Configuration conf : configurationList) {
try {
if (jmxAttribute.match(conf)) {
jmxAttribute.setMatchingConf(conf);
metricsCount += jmxAttribute.getMetricsCount();
this.matchingAttributes.add(jmxAttribute);
if (action.equals(AppConfig.ACTION_LIST_EVERYTHING) ||
action.equals(AppConfig.ACTION_LIST_MATCHING) ||
action.equals(AppConfig.ACTION_LIST_COLLECTED) && !limitReached ||
action.equals(AppConfig.ACTION_LIST_LIMITED) && limitReached) {
reporter.displayMatchingAttributeName(jmxAttribute, metricsCount, maxReturnedMetrics);
}
break;
}
} catch (Exception e) {
LOGGER.error("Error while trying to match attributeInfo configuration with the Attribute: " + beanName + " : " + attributeInfo, e);
}
}
if (jmxAttribute.getMatchingConf() == null
&& (action.equals(AppConfig.ACTION_LIST_EVERYTHING)
|| action.equals(AppConfig.ACTION_LIST_NOT_MATCHING))) {
reporter.displayNonMatchingAttributeName(jmxAttribute);
}
}
}
LOGGER.info("Found " + matchingAttributes.size() + " matching attributes");
}
public LinkedList<String> getBeansScopes(){
if(this.beanScopes == null){
this.beanScopes = Configuration.getGreatestCommonScopes(configurationList);
}
return this.beanScopes;
}
/**
* Query and refresh the instance's list of beans.
* Limit the query scope when possible on certain actions, and fallback if necessary.
*/
private void refreshBeansList() throws IOException {
this.beans = new HashSet<ObjectName>();
String action = appConfig.getAction();
Boolean limitQueryScopes = !action.equals(AppConfig.ACTION_LIST_EVERYTHING) && !action.equals(AppConfig.ACTION_LIST_NOT_MATCHING);
if (limitQueryScopes) {
try {
LinkedList<String> beanScopes = getBeansScopes();
for (String scope : beanScopes) {
ObjectName name = new ObjectName(scope);
this.beans.addAll(connection.queryNames(name));
}
}
catch (Exception e) {
LOGGER.error("Unable to compute a common bean scope, querying all beans as a fallback", e);
}
}
this.beans = (this.beans.isEmpty()) ? connection.queryNames(null): this.beans;
this.lastRefreshTime = System.currentTimeMillis();
}
public String[] getServiceCheckTags() {
List<String> tags = new ArrayList<String>();
if (this.yaml.get("host") != null) {
tags.add("jmx_server:" + this.yaml.get("host"));
}
if (this.tags != null) {
for (Entry<String, String> e : this.tags.entrySet()) {
if (e.getValue()!=null){
tags.add(e.getKey() + ":" + e.getValue());
} else {
tags.add(e.getKey());
}
}
}
tags.add("instance:" + this.instanceName);
return tags.toArray(new String[tags.size()]);
}
public String getName() {
return this.instanceName;
}
LinkedHashMap<String, Object> getYaml() {
return this.yaml;
}
LinkedHashMap<String, Object> getInitConfig() {
return this.initConfig;
}
public String getCheckName() {
return this.checkName;
}
public int getMaxNumberOfMetrics() {
return this.maxReturnedMetrics;
}
public boolean isLimitReached() {
return this.limitReached;
}
public void cleanUp() {
this.appConfig = null;
if (connection != null) {
connection.closeConnector();
}
}
}