package de.is24.util.monitoring.jmx;
import de.is24.util.monitoring.MultiValueProvider;
import de.is24.util.monitoring.ReportVisitor;
import de.is24.util.monitoring.State;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.LineIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.openmbean.CompositeData;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* JMXExporter exports all numeric attributes of a given JMX Domain.
* You can use it to send metrics from other JMX beans to graphite.
* You do not need it, and should not use it, to send appmon4j metrics, this should be done by
* Statsd or Graphite plugins instead.
*/
public class JMXExporter implements MultiValueProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(JMXExporter.class);
private static final String JMXEXPORTER = "JMXExporter";
private final MBeanServer platformMBeanServer = ManagementFactory.getPlatformMBeanServer();
private final Set<ObjectName> objectPatterns;
/**
* Initialize Exporter
*
*/
public JMXExporter() {
objectPatterns = Collections.synchronizedSet(new HashSet<ObjectName>());
}
/**
* Initialize Exporter for a given ObjectName pattern. The pattern must be a valid object name.
*
* @param pattern The JMX domain.
* @throws MalformedObjectNameException in case of an invalid pattern
*/
public JMXExporter(String pattern) throws MalformedObjectNameException {
this();
addPattern(pattern);
}
public void addPattern(String pattern) throws MalformedObjectNameException {
objectPatterns.add(new ObjectName(pattern));
}
public List<ObjectName> listPatterns() {
return new ArrayList<ObjectName>(objectPatterns);
}
public boolean removePattern(String pattern) throws MalformedObjectNameException {
return objectPatterns.remove(new ObjectName(pattern));
}
@Override
public Collection<State> getValues() {
List<State> result = new ArrayList<State>();
searchAndLogNumericAttributes(result);
return result;
}
@Override
public String getName() {
return JMXEXPORTER;
}
@Override
public void accept(ReportVisitor visitor) {
visitor.reportMultiValue(this);
}
protected void searchAndLogNumericAttributes(List<State> result) {
Map<ObjectName, MBeanInfo> mBeanInfoMap = getMBeanInfos();
for (Map.Entry<ObjectName, MBeanInfo> entry : mBeanInfoMap.entrySet()) {
ObjectName name = entry.getKey();
MBeanInfo mBeanInfo = entry.getValue();
MBeanAttributeInfo[] attributes = mBeanInfo.getAttributes();
for (MBeanAttributeInfo info : attributes) {
try {
Object valueObject = platformMBeanServer.getAttribute(name, info.getName());
String attributeName = getBaseName(name);
handleObject(attributeName, info.getName(), valueObject, result);
} catch (Exception e) {
// Some special treatment if running in jsvc for https://issues.apache.org/jira/browse/DAEMON-120
// or for some attributes not supported by all JVM implementations, but still in the mbean
if (isUnsupportedOperation(e) || isJsvcSpecificProcSelfFdProblem(e)) {
LOGGER.debug("ignoring unsupported numeric MBean Attribute {} {} {}", name, info);
} else {
LOGGER.info("Error accessing numeric MBean Attribute {} {} {}", name, info, e.getMessage());
}
}
}
}
}
private boolean isJsvcSpecificProcSelfFdProblem(Exception e) {
String message = e.getMessage();
return (message != null) && message.contains("Unable to open directory /proc/self/fd");
}
private boolean isUnsupportedOperation(Exception e) {
return (e.getCause() != null) && e.getCause().getClass().equals(UnsupportedOperationException.class);
}
private void handleObject(String baseName, String path, Object valueObject, List<State> result) {
LOGGER.debug("handling {}", baseName);
Long value = null;
if (valueObject instanceof Long) {
value = ((Long) valueObject).longValue();
} else if (valueObject instanceof Boolean) {
value = ((Boolean) valueObject) ? 1L : 0L;
} else if (valueObject instanceof Integer) {
value = ((Integer) valueObject).longValue();
} else if (valueObject instanceof Short) {
value = ((Short) valueObject).longValue();
} else if (valueObject instanceof Double) {
value = ((Double) valueObject).longValue();
} else if (valueObject instanceof Float) {
value = ((Float) valueObject).longValue();
} else if (valueObject instanceof CompositeData) {
logComposite(baseName, path, (CompositeData) valueObject, result);
}
if (value != null) {
result.add(createState(baseName, path, value));
}
}
private void logComposite(String attributeName, String path, CompositeData obj, List<State> result) {
for (String key : obj.getCompositeType().keySet()) {
String additionalPath = (path == null) ? key : (path + "." + key);
Object valueObject = obj.get(key);
if (valueObject instanceof CompositeData) {
logComposite(attributeName, additionalPath, (CompositeData) valueObject, result);
} else if (valueObject != null) {
handleObject(attributeName, additionalPath, valueObject, result);
}
}
}
private State createState(String baseName, String path, Long value) {
return new State(baseName, path, value);
}
private String getBaseName(ObjectName name) {
StringBuilder builder = new StringBuilder();
builder.append(name.getDomain()).append(".").append(name.getCanonicalKeyPropertyListString());
return builder.toString();
}
protected Map<ObjectName, MBeanInfo> getMBeanInfos() {
try {
Map<ObjectName, MBeanInfo> result = new HashMap<ObjectName, MBeanInfo>();
for (ObjectName objectPattern : objectPatterns) {
Set<ObjectName> objectNames = platformMBeanServer.queryNames(objectPattern, null);
for (ObjectName name : objectNames) {
result.put(name, platformMBeanServer.getMBeanInfo(name));
}
}
LOGGER.debug("searching for MBeans using {} patterns found {} matching Bean Infos",
objectPatterns.size(),
result.size());
return result;
} catch (Exception e) {
LOGGER.warn("oops", e);
throw new RuntimeException(e);
}
}
public void readFromDirectory(String path) {
LOGGER.info("reading JMXExporter Patterns from directory {}", path);
File dir = new File(path);
if (dir.exists() && dir.isDirectory()) {
Collection<File> files = FileUtils.listFiles(dir, null, false);
if (files.size() == 0) {
LOGGER.warn("no files found in readFromDirectory {}", path);
}
for (File file : files) {
readFromFile(file);
}
} else {
LOGGER.warn("JMXExporter config dir {} does not exist.", dir.getAbsolutePath());
}
}
public void readFromFile(String filename) {
readFromFile(new File(filename));
}
public void readFromFile(File file) {
LOGGER.info("reading JMXExporter Patterns from file {}", file.getAbsolutePath());
LineIterator it;
try {
it = FileUtils.lineIterator(file, "UTF-8");
} catch (IOException e) {
LOGGER.warn("Error while reading patterns from file " + file.getAbsolutePath(), e);
throw new RuntimeException(e);
}
try {
while (it.hasNext()) {
String pattern = it.nextLine();
// an empty pattern would be translated to *:*, we do not want this default behaviour of ObjectName here.
if (pattern.length() > 0) {
try {
addPattern(pattern);
} catch (MalformedObjectNameException e) {
LOGGER.warn("Ignoring malformed ObjectName pattern while applying pattern {} from file {} {} ",
pattern,
file.getAbsolutePath(),
e.getMessage());
}
}
}
} finally {
LineIterator.closeQuietly(it);
}
}
}