/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2012, Helios Development Group and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*/
package org.helios.apmrouter.server.monitor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import org.helios.apmrouter.deployer.SpringHotDeployer;
import org.helios.apmrouter.deployer.event.HotDeployedContextClosedEvent;
import org.helios.apmrouter.deployer.event.HotDeployedContextEvent;
import org.helios.apmrouter.deployer.event.HotDeployedContextRefreshedEvent;
import org.helios.apmrouter.jmx.JMXHelper;
import org.helios.apmrouter.monitor.jvm.JVMMonitor;
import org.helios.apmrouter.monitor.nativex.NativeMonitor;
import org.helios.apmrouter.server.ServerComponentBean;
import org.helios.apmrouter.server.tracing.ServerTracerFactory;
import org.helios.apmrouter.trace.ITracer;
import org.helios.apmrouter.util.SystemClock;
import org.helios.apmrouter.util.SystemClock.ElapsedTime;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.support.MetricType;
import org.springframework.util.ReflectionUtils;
/**
* <p>Title: ServerMonitor</p>
* <p>Description: A service to execute background localStats collection and submission</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.server.monitor.ServerMonitor</code></p>
*/
public class ServerMonitor extends ServerComponentBean implements Runnable {
/** The internal server tracer */
protected final ITracer tracer = ServerTracerFactory.getInstance().getTracer();
/** The MBeanServer to retrieve the metric values from */
protected final MBeanServer server = JMXHelper.getHeliosMBeanServer();
/** A map of maps of GAUGE metric providing operation names for a bean keyed by the JMX ObjectName */
protected final Map<ObjectName, Map<String[], String[]>> allGaugeMetrics = new HashMap<ObjectName, Map<String[], String[]>>();
/** A map of maps of COUNTER metric providing operation names for a bean keyed by the JMX ObjectName */
protected final Map<ObjectName, Map<String[], String[]>> allCounterMetrics = new HashMap<ObjectName, Map<String[], String[]>>();
/** A map of ObjectNames located from hot deployed contexts keyed by context id */
protected final Map<String, Set<ObjectName>> hotDeployedObjectNames = new ConcurrentHashMap<String, Set<ObjectName>>();
private final boolean ONE_METRIC_ONLY = false;
/**
* Creates a new ServerMonitor
*/
public ServerMonitor() {
super();
// supportedEventTypes.add(HotDeployedContextEvent.class);
// supportedEventSourceTypes.add(ApplicationContext.class);
supportedEventTypes.clear();
supportedEventSourceTypes.clear();
}
public boolean supportsSourceType(Class<?> sourceType) {
return true;
}
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return true;
}
/**
* Handles hot deployed context events.
* @param event hot deployed context event
*/
public void onApplicationEvent(ApplicationEvent event) {
if(HotDeployedContextRefreshedEvent.class.isInstance(event)) {
ApplicationContext appCtx = ((HotDeployedContextRefreshedEvent)event).getAppCtx();
info("Collecting instances of ServerComponentBean from [", appCtx.getDisplayName(), "]");
Map<ObjectName, Map<String[], String[]>> gaugeMetrics = new HashMap<ObjectName, Map<String[], String[]>>();
Map<ObjectName, Map<String[], String[]>> counterMetrics = new HashMap<ObjectName, Map<String[], String[]>>();
locateMonitorBeans(appCtx, gaugeMetrics, counterMetrics);
Set<ObjectName> hotObjects = new HashSet<ObjectName>(counterMetrics.size() + gaugeMetrics.size());
hotObjects.addAll(counterMetrics.keySet());
hotObjects.addAll(gaugeMetrics.keySet());
hotDeployedObjectNames.put(appCtx.getId(), hotObjects);
allCounterMetrics.putAll(counterMetrics);
allGaugeMetrics.putAll(gaugeMetrics);
} else if(HotDeployedContextClosedEvent.class.isInstance(event)) {
ApplicationContext appCtx = ((HotDeployedContextClosedEvent)event).getAppCtx();
info("Clearing instances of ServerComponentBean from [", appCtx.getDisplayName(), "]");
Set<ObjectName> hotObjects = hotDeployedObjectNames.remove(appCtx.getId());
if(hotObjects!=null) {
for(ObjectName on: hotObjects) {
allCounterMetrics.remove(on);
allGaugeMetrics.remove(on);
}
}
info("Cleared [", hotObjects==null ? 0 : hotObjects.size(), "] from closed app context [", appCtx.getDisplayName(), "]");
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.server.ServerComponentBean#onApplicationContextRefresh(org.springframework.context.event.ContextRefreshedEvent)
*/
@Override
public void onApplicationContextRefresh(ContextRefreshedEvent event) {
if(!ONE_METRIC_ONLY) {
new JVMMonitor().startMonitor();
new NativeMonitor().startMonitor();
}
info("Collecting instances of ServerComponentBean from [", event.getApplicationContext().getDisplayName(), "]");
Map<ObjectName, Map<String[], String[]>> gaugeMetrics = new HashMap<ObjectName, Map<String[], String[]>>();
Map<ObjectName, Map<String[], String[]>> counterMetrics = new HashMap<ObjectName, Map<String[], String[]>>();
locateMonitorBeans(event.getApplicationContext(), gaugeMetrics, counterMetrics);
allCounterMetrics.putAll(counterMetrics);
allGaugeMetrics.putAll(gaugeMetrics);
Thread t = new Thread(this, "ServerMonitorThread");
t.setDaemon(true);
t.start();
}
/**
* Scans an application context for {link ServerComponentBean}s and extracts their {@link ManagedMetric} meta-data
* @param appCtx The application context to scan
*/
private void locateMonitorBeans(ApplicationContext appCtx, final Map<ObjectName, Map<String[], String[]>> gaugeMetrics, final Map<ObjectName, Map<String[], String[]>> counterMetrics) {
final Set<String> invalids = new HashSet<String>();
Map<String, ServerComponentBean> beans = appCtx.getBeansOfType(ServerComponentBean.class, false, false);
info("Located [", beans.size(), "] instances of ServerComponentBean....");
for(Map.Entry<String, ServerComponentBean> bean: beans.entrySet()) {
ObjectName on = bean.getValue().getComponentObjectName();
final Set<String> attrNames = getMBeanAttributeNames(on);
//Set<String> jmxOps = new HashSet<String>();
try {
// for(MBeanOperationInfo info: server.getMBeanInfo(on).getOperations()) {
// jmxOps.add(info.getName());
// }
Map<String[], String[]> gauges = new HashMap<String[], String[]>();
Map<String[], String[]> counters = new HashMap<String[], String[]>();
String beanName = bean.getKey();
for(Method m: ReflectionUtils.getAllDeclaredMethods(bean.getValue().getClass())) {
ManagedMetric managedMetric = m.getAnnotation(ManagedMetric.class);
if(managedMetric!=null) {
String category = managedMetric.category();
String attributeName = attributizeMethodName(m);
String displayName = managedMetric.displayName();
if(category!=null && category.trim().isEmpty()) category=null;
if(displayName!=null && displayName.trim().isEmpty()) displayName=null;
String metricName = (displayName==null) ? attributeName : displayName;
if(metricName==null || metricName.trim().isEmpty()) {
invalids.add(on + "-" + m.getName() + " evaluated null metricName");
continue;
}
metricName = metricName.trim();
if(!attrNames.contains(attributeName)) {
invalids.add("No attribute match for " + on + "-" + m.getName() + " evaluated null metricName");
continue;
}
String[] namespace = new String[category==null ? 2 : 3];
String[] names = new String[]{metricName, attributeName};
namespace[0] = "platform=APMRouter";
namespace[1] = "component=" + beanName;
if(category!=null) {
namespace[2] = "category=" + category;
}
if(managedMetric.metricType()==MetricType.COUNTER) {
counters.put(namespace, names);
} else {
gauges.put(namespace, names);
}
}
}
if(!counters.isEmpty()) counterMetrics.put(on, counters);
if(!gauges.isEmpty()) gaugeMetrics.put(on, gauges);
} catch (Exception ex) {
warn("Failed to collect meta-data for ObjectName [", on, "]", ex);
}
}
if(!invalids.isEmpty()) {
StringBuilder b = new StringBuilder("\n\tInvalid ManagedMetrics for Server Monitor from app ctx [" + appCtx.getDisplayName() + "]");
for(String s: invalids) {
b.append("\n\t").append(s);
}
warn(b);
}
info("Completed scan of app context [", appCtx.getDisplayName(), "] \n\tCreated [", gaugeMetrics.size(), "] GAUGE metrics and [", counterMetrics.size(), "] COUNTER metrics");
}
/**
* Returns a set of attribute names for the passed object name
* @param on the target object name
* @return a set of attribute names
*/
protected Set<String> getMBeanAttributeNames(ObjectName on) {
try {
MBeanInfo info = JMXHelper.getHeliosMBeanServer().getMBeanInfo(on);
MBeanAttributeInfo[] attrInfos = info.getAttributes();
Set<String> attributeNames = new HashSet<String>(attrInfos.length);
for(MBeanAttributeInfo attrInfo: attrInfos) {
attributeNames.add(attrInfo.getName());
}
return Collections.unmodifiableSet(attributeNames);
} catch (Exception ex) {
return Collections.emptySet();
}
}
/**
* Returns the attribute name for the passed method if it is a traditional getter.
* i.e. it is in the form <b><code>getXXX</code></b> with no parameters.
* Otherwise, returns null.
* @param method The method to convert
* @return the attribute name or null
*/
protected String attributizeMethodName(Method method) {
if(method==null) return null;
String name = method.getName();
if(name.startsWith("get") && method.getParameterTypes().length==0) {
return name.substring(3);
}
return null;
}
/** Null argument const */
private static final Object[] NULL_ARGS = {};
/** Null signature const */
private static final String[] NULL_SIG = {};
/**
* Executes the traces
*/
protected void trace() {
MBeanServer server = JMXHelper.getHeliosMBeanServer();
final Set<Map.Entry<ObjectName, Map<String[], String[]>>> _allCounterMetrics = new HashSet<Map.Entry<ObjectName, Map<String[], String[]>>>(allCounterMetrics.entrySet());
for(Map.Entry<ObjectName, Map<String[], String[]>> metrics: _allCounterMetrics) {
final ObjectName on = metrics.getKey();
if(!server.isRegistered(on)) {
warn("Target MBean not registered [", on, "]");
continue;
}
for(Map.Entry<String[], String[]> ops: metrics.getValue().entrySet()) {
String[] namespace = ops.getKey();
String metricName = ops.getValue()[0];
String attributeName = ops.getValue()[1];
try {
final long value = ((Number)server.getAttribute(on, attributeName)).longValue();
tracer.traceDeltaCounter(value, metricName, namespace);
} catch (Exception ex) {
error("Failed to collect counter localStats for ObjectName [", on, "] on namespace: ", Arrays.toString(namespace) , " metricName:[", metricName, "]", ex);
}
}
}
final Set<Map.Entry<ObjectName, Map<String[], String[]>>> _allGaugeMetrics = new HashSet<Map.Entry<ObjectName, Map<String[], String[]>>>(allGaugeMetrics.entrySet());
for(Map.Entry<ObjectName, Map<String[], String[]>> metrics: _allGaugeMetrics) {
final ObjectName on = metrics.getKey();
if(!server.isRegistered(on)) {
warn("Target MBean not registered [", on, "]");
continue;
}
for(Map.Entry<String[], String[]> ops: metrics.getValue().entrySet()) {
String[] namespace = ops.getKey();
String metricName = ops.getValue()[0];
String attributeName = ops.getValue()[1];
try {
final long value = ((Number)server.getAttribute(on, attributeName)).longValue();
tracer.traceGauge(value, metricName, namespace);
} catch (Exception ex) {
error("Failed to collect gauge localStats for ObjectName [", on, "] on namespace: ", Arrays.toString(namespace) , " metricName:[", metricName, "]", ex);
}
}
}
}
/**
* {@inheritDoc}
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
while(true) {
if(!ONE_METRIC_ONLY) {
SystemClock.startTimer();
try {
debug("Collecting Server Metrics");
trace();
} finally {
ElapsedTime et = SystemClock.endTimer();
tracer.traceGauge(et.elapsedMs, "ElapsedTimeMs", "platform=APMRouter", "category=ServerMonitor");
}
} else {
tracer.traceGauge(SystemClock.time()%5000, "ElapsedTimeMs", "platform=APMRouter", "category=ServerMonitor");
}
SystemClock.sleep(5000);
}
}
}