package rocks.inspectit.agent.java.sensor.jmx; import java.lang.management.ManagementFactory; import java.lang.reflect.Array; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.management.AttributeNotFoundException; import javax.management.InstanceNotFoundException; import javax.management.IntrospectionException; import javax.management.MBeanAttributeInfo; import javax.management.MBeanException; import javax.management.MBeanServer; import javax.management.MBeanServerNotification; import javax.management.Notification; import javax.management.NotificationFilter; import javax.management.NotificationListener; import javax.management.ObjectName; import javax.management.ReflectionException; import javax.management.RuntimeMBeanException; import org.apache.commons.collections.MapUtils; import org.slf4j.Logger; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import com.google.common.base.Objects; import rocks.inspectit.agent.java.config.IConfigurationStorage; import rocks.inspectit.agent.java.connection.IConnection; import rocks.inspectit.agent.java.connection.ServerUnavailableException; import rocks.inspectit.agent.java.core.ICoreService; import rocks.inspectit.agent.java.core.IPlatformManager; import rocks.inspectit.shared.all.communication.data.JmxSensorValueData; import rocks.inspectit.shared.all.instrumentation.config.impl.JmxAttributeDescriptor; import rocks.inspectit.shared.all.instrumentation.config.impl.JmxSensorTypeConfig; import rocks.inspectit.shared.all.spring.logger.Log; /** * The implementation of the JmxSensor. * * @author Alfred Krauss * @author Marius Oehler * @author Ivan Senic * */ public class JmxSensor implements IJmxSensor, InitializingBean, DisposableBean { /** * Name of the MBeanServerDelegate to register as listener for the {@link NotificationListener}. */ private static final String MBEAN_SERVER_DELEGATE_NAME = "JMImplementation:type=MBeanServerDelegate"; /** * Defines the interval of the maximum call rate of the * {@link JmxSensor#collectData(ICoreService, long)} method. */ private static final int DATA_COLLECT_INTERVAL = 5000; /** * Notification filter that listeners only to the MBeanServerNotification events. */ private static final NotificationFilter NOTIFICATION_FILTER = new NotificationFilter() { /** * Generated UID. */ private static final long serialVersionUID = 3300812282583960575L; @Override public boolean isNotificationEnabled(Notification notification) { String type = notification.getType(); return MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(type) || MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(type); } }; /** * The logger of the class. */ @Log Logger log; /** * The instance of the configuration storage. */ @Autowired private IConfigurationStorage configurationStorage; /** * The Platform manager used to get the correct platform ID. */ @Autowired private IPlatformManager platformManager; /** * Connection. */ @Autowired private IConnection connection; /** * Sensor configuration. */ private JmxSensorTypeConfig sensorTypeConfig; /** * Set of active MBeanServerHolders. */ private final Map<MBeanServer, MBeanServerHolder> activeServerMap = new ConcurrentHashMap<MBeanServer, MBeanServerHolder>(); /** * The timestamp of the last {@link #collectData(ICoreService, long)} method invocation. */ long lastDataCollectionTimestamp = 0; /** * {@inheritDoc} */ public void init(JmxSensorTypeConfig sensorTypeConfig) { this.sensorTypeConfig = sensorTypeConfig; // check for forcing server creation Map<String, Object> parameters = sensorTypeConfig.getParameters(); if (MapUtils.isNotEmpty(parameters)) { if (Boolean.TRUE.equals(parameters.get("forceMBeanServer"))) { // create only, get it via hook MBeanServer server = ManagementFactory.getPlatformMBeanServer(); mbeanServerAdded(server); } } } /** * {@inheritDoc} */ @Override public void mbeanServerAdded(MBeanServer server) { // if null or already registered do nothing if ((null == server) || activeServerMap.containsKey(server)) { return; } try { // create holder MBeanServerHolder holder = new MBeanServerHolder(server); // register listener try { server.addNotificationListener(new ObjectName(MBEAN_SERVER_DELEGATE_NAME), holder, NOTIFICATION_FILTER, null); } catch (Exception e) { log.warn("Failed to add notification listener to the MBean server " + server.toString() + ". New added beans/attributes will not be monitored.", e); } // register already existing beans registerMBeans(holder, null); // add to map if it all works activeServerMap.put(server, holder); } catch (Throwable t) { // NOPMD // catching throwable if anything goes wrong log.warn("Unable to add the MBean server " + server.toString() + ".", t); } } /** * {@inheritDoc} */ @Override public void mbeanServerRemoved(MBeanServer server) { // if null do nothing if (null == server) { return; } try { // remove holder MBeanServerHolder holder = activeServerMap.remove(server); if (null == holder) { return; } // un-register listener try { server.removeNotificationListener(new ObjectName(MBEAN_SERVER_DELEGATE_NAME), holder, NOTIFICATION_FILTER, null); } catch (Exception e) { log.warn("Failed to remove notification listener to the MBean server " + server.toString() + ".", e); } } catch (Throwable t) { // NOPMD // catching throwable if anything goes wrong log.warn("Unable to remove the MBean server " + server.toString() + ".", t); } } /** * {@inheritDoc} */ @Override public JmxSensorTypeConfig getSensorTypeConfig() { return sensorTypeConfig; } /** * {@inheritDoc} */ @Override public void update(ICoreService coreService) { long sensorTypeIdent = sensorTypeConfig.getId(); long currentTime = System.currentTimeMillis(); // Check if the collectData method should be invoked if (MapUtils.isNotEmpty(activeServerMap) && ((currentTime - lastDataCollectionTimestamp) > DATA_COLLECT_INTERVAL)) { // store the invocation timestamp lastDataCollectionTimestamp = System.currentTimeMillis(); for (MBeanServerHolder holder : activeServerMap.values()) { collectData(holder, coreService, sensorTypeIdent); } } } /** * Collects the data from the MBean server in the holder and sends it to the CMR. * * @param holder * {@link MBeanServerHolder} to collect data from * @param coreService * The core service which is needed to store the measurements to. * @param sensorTypeIdent * The ID of the sensor type so that old data can be found. (for aggregating etc.) */ private void collectData(MBeanServerHolder holder, ICoreService coreService, long sensorTypeIdent) { MBeanServer mBeanServer = holder.mBeanServer; Map<JmxAttributeDescriptor, Boolean> activeAttributes = holder.activeAttributes; Map<String, ObjectName> nameStringToObjectName = holder.nameStringToObjectName; Timestamp timestamp = new Timestamp(Calendar.getInstance().getTime().getTime()); for (Iterator<JmxAttributeDescriptor> iterator = activeAttributes.keySet().iterator(); iterator.hasNext();) { JmxAttributeDescriptor descriptor = iterator.next(); try { // Retrieving the value of the, in the JmxAttributeDescriptor specified, // MBeanAttribute ObjectName objectName = nameStringToObjectName.get(descriptor.getmBeanObjectName()); Object collectedValue = mBeanServer.getAttribute(objectName, descriptor.getAttributeName()); String value; if (null == collectedValue) { value = "null"; } else if (collectedValue.getClass().isArray()) { value = getArrayValue(collectedValue); } else { value = collectedValue.toString(); } // Create a new JmxSensorValueData to be saved into the database long platformid = platformManager.getPlatformId(); JmxSensorValueData jsvd = new JmxSensorValueData(descriptor.getId(), value, timestamp, platformid, sensorTypeIdent); coreService.addJmxSensorValueData(sensorTypeIdent, descriptor.getmBeanObjectName(), descriptor.getAttributeName(), jsvd); } catch (AttributeNotFoundException e) { iterator.remove(); log.warn("JMX::AttributeNotFound. Attribute was not found. Maybe currently not available on the server. Attribute removed from the actively read list.", e); } catch (InstanceNotFoundException e) { iterator.remove(); log.warn("JMX::Instance not found. MBean may not be registered on the Server. Attribute removed from the actively read list.", e); } catch (MBeanException e) { iterator.remove(); log.warn("JMX::MBean. Undefined problem with the MBean. Attribute removed from the actively read list.", e); } catch (ReflectionException e) { iterator.remove(); log.warn("JMX::Reflection error. MBean may not be registered on the Server. Attribute removed from the actively read list.", e); } catch (RuntimeMBeanException e) { iterator.remove(); log.warn("JMX::Runtime error reading the attribute " + descriptor.getAttributeName() + " from the MBean " + descriptor.getmBeanObjectName() + ". Attribute removed from the actively read list.", e); } } } /** * Registers all attributes of all object names that are returned as the result of querying with * the given mBeanName on the server in the given holder. * * @param holder * {@link MBeanServerHolder} instance to register beans for * @param mBeanName * Object name to be used in the query. Use <code>null</code> to include all MBeans. */ private void registerMBeans(MBeanServerHolder holder, ObjectName mBeanName) { MBeanServer mBeanServer = holder.mBeanServer; Map<JmxAttributeDescriptor, Boolean> activeAttributes = holder.activeAttributes; Map<String, ObjectName> nameStringToObjectName = holder.nameStringToObjectName; // do nothing if connection is not there if (!connection.isConnected()) { return; } List<JmxAttributeDescriptor> descriptors = new ArrayList<JmxAttributeDescriptor>(); // query for all names using null null Set<ObjectName> allNames = mBeanServer.queryNames(mBeanName, null); for (ObjectName objectName : allNames) { // collect all attributes and send to the server try { MBeanAttributeInfo[] attributeInfos = mBeanServer.getMBeanInfo(objectName).getAttributes(); for (MBeanAttributeInfo mBeanAttributeInfo : attributeInfos) { JmxAttributeDescriptor descriptor = new JmxAttributeDescriptor(); descriptor.setmBeanObjectName(objectName.toString()); descriptor.setAttributeName(mBeanAttributeInfo.getName()); descriptor.setmBeanAttributeDescription(mBeanAttributeInfo.getDescription()); descriptor.setmBeanAttributeIsIs(mBeanAttributeInfo.isIs()); descriptor.setmBeanAttributeIsReadable(mBeanAttributeInfo.isReadable()); descriptor.setmBeanAttributeIsWritable(mBeanAttributeInfo.isWritable()); descriptor.setmBeanAttributeType(mBeanAttributeInfo.getType()); descriptors.add(descriptor); } } catch (IntrospectionException e) { continue; } catch (InstanceNotFoundException e) { continue; } catch (ReflectionException e) { continue; } } try { Collection<JmxAttributeDescriptor> toMonitor = connection.analyzeJmxAttributes(platformManager.getPlatformId(), descriptors); // add to active attributes for (JmxAttributeDescriptor descriptor : toMonitor) { activeAttributes.put(descriptor, Boolean.FALSE); } // if call is working add object names to the map for (ObjectName name : allNames) { nameStringToObjectName.put(name.toString(), name); } } catch (ServerUnavailableException e) { if (log.isWarnEnabled()) { log.warn("Error registering JMX attributes on the server.", e); } } } /** * {@inheritDoc} */ @Override public void afterPropertiesSet() throws Exception { for (JmxSensorTypeConfig config : configurationStorage.getJmxSensorTypes()) { if (config.getClassName().equals(this.getClass().getName())) { this.init(config); break; } } } /** * {@inheritDoc} */ @Override public void destroy() throws Exception { // remove listeners if (MapUtils.isNotEmpty(activeServerMap)) { for (MBeanServerHolder holder : activeServerMap.values()) { holder.mBeanServer.removeNotificationListener(new ObjectName(MBEAN_SERVER_DELEGATE_NAME), holder, NOTIFICATION_FILTER, null); } } } /** * Correctly handles printing of the array no matter if the array class is primitive or not. * * @param collectedArray * collected array * @return Value for the array or empty string if given object is <code>null</code> or not an * array. */ private String getArrayValue(Object collectedArray) { if ((null != collectedArray) && collectedArray.getClass().isArray()) { StringBuilder sb = new StringBuilder("["); int length = Array.getLength(collectedArray); for (int i = 0; i < length; i++) { if (i > 0) { sb.append(", "); } sb.append(Array.get(collectedArray, i)); } sb.append(']'); return sb.toString(); } return ""; } /** * Holder to keep needed information per server. * * @author Ivan Senic * */ final class MBeanServerHolder implements NotificationListener { /** * The MBeanServer providing information about registered MBeans. */ final MBeanServer mBeanServer; /** * Map used to connect the ObjectName of a MBean with the string-representation of the same * MBean. Recreation of the ObjectName is no longer necessary for the update-method. */ final Map<String, ObjectName> nameStringToObjectName = new ConcurrentHashMap<String, ObjectName>(); /** * Set of active attributes (represented as Map). */ final Map<JmxAttributeDescriptor, Boolean> activeAttributes = new ConcurrentHashMap<JmxAttributeDescriptor, Boolean>(); /** * Default constructor. * * @param mBeanServer * Server to store in this holder. Can not be <code>null</code>. */ MBeanServerHolder(MBeanServer mBeanServer) { if (mBeanServer == null) { throw new IllegalArgumentException("MBean server can not be null."); } this.mBeanServer = mBeanServer; } /** * {@inheritDoc} */ @Override public void handleNotification(Notification notification, Object handback) { if (notification instanceof MBeanServerNotification) { MBeanServerNotification serverNotification = (MBeanServerNotification) notification; ObjectName mBeanName = serverNotification.getMBeanName(); if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(serverNotification.getType())) { // if we have registration pick up the attributes registerMBeans(MBeanServerHolder.this, mBeanName); } else if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(serverNotification.getType())) { // get maps from holder // if we have un-registration remove from maps String mBeanNameString = mBeanName.toString(); for (Iterator<JmxAttributeDescriptor> it = activeAttributes.keySet().iterator(); it.hasNext();) { JmxAttributeDescriptor descriptor = it.next(); if (Objects.equal(descriptor.getmBeanObjectName(), mBeanNameString)) { it.remove(); } } nameStringToObjectName.remove(mBeanNameString); } } } } }