/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2007, 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.jagent;
import org.helios.apmrouter.instrumentation.Trace;
import org.helios.apmrouter.instrumentation.TraceClassFileTransformer;
import org.helios.apmrouter.instrumentation.publifier.ClassPublifier;
import org.helios.apmrouter.jmx.JMXHelper;
import org.helios.apmrouter.jmx.XMLHelper;
import org.helios.apmrouter.jmx.threadinfo.ExtendedThreadManager;
import org.helios.apmrouter.monitor.Monitor;
import org.helios.apmrouter.sender.SenderFactory;
import org.helios.apmrouter.util.SimpleLogger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import java.io.File;
import java.io.StringReader;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import static org.helios.apmrouter.util.SimpleLogger.*;
//import org.helios.apmrouter.byteman.sockets.impl.TrackingSocketImplFactory;
/**
* <p>Title: AgentBoot</p>
* <p>Description: A core module bootstrap entry point for the java-agent to initialize through</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.jagent.AgentBoot</code></p>
*/
public class AgentBoot {
/** The provided instrumentation instance */
protected static Instrumentation instrumentation = null;
/** The provided agent argument string */
protected static String agentArgs = null;
/** The classloader passed by the bootstrap agent */
protected static URLClassLoader classLoader;
/** The agent boot class for codahale */
protected static final String CODAHALE_BOOT_CLASS = "org.helios.apmrouter.codahale.agent.Agent";
/** The target method name for the agent boot class for codahale */
protected static final String CODAHALE_BOOT_METHOD = "heliosBoot";
/** The target method signature for the agent boot class for codahale */
protected static final Class<?>[] CODAHALE_BOOT_SIG = new Class[]{
String.class, Instrumentation.class, Node.class
};
/** The reflective method for {@link URLClassLoader}'s addURL method */
protected static final Method addUrlMethod;
static {
try {
addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
addUrlMethod.setAccessible(true);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/**
* Returns the instrumentation instance
* @return the instrumentation instance
*/
public static Instrumentation getInstrumentation() {
return instrumentation;
}
/**
* The core module boot hook when installing though the java-agent
* @param classLoader The classloader passed by the bootstrap agent
* @param agentArgs The agent arguments
* @param instrumentation The instrumentation which may be null
*/
public static void boot(URLClassLoader classLoader, String agentArgs, Instrumentation instrumentation) {
org.helios.apmrouter.jagent.Instrumentation.install(instrumentation);
AgentBoot.agentArgs = agentArgs;
AgentBoot.instrumentation = instrumentation;
AgentBoot.classLoader = classLoader;
configure();
ExtendedThreadManager.install();
Thread t = new Thread("HotspotInternalLoaderThread") {
@Override
public void run() {
try {
Thread.sleep(15000);
Class<?> clazz = Class.forName("sun.management.HotspotInternal");
Object obj = clazz.newInstance();
JMXHelper.getHeliosMBeanServer().registerMBean(obj, new javax.management.ObjectName("sun.management:type=HotspotInternal"));
} catch (Exception ex) {
SimpleLogger.warn("Failed to install HotspotInternal:", ex);
}
}
};
t.setDaemon(true);
t.start();
}
/**
* Configures the agent from the supplied agent args.
*/
protected static void configure() {
if(agentArgs!=null && !agentArgs.trim().isEmpty()) {
URL xmlUrl = null;
Node configNode = null;
try {
xmlUrl = new URL(agentArgs);
configNode = getXMLConf(xmlUrl);
} catch (Exception e) {
System.err.println("Failed to parse XML configuration defined at [" + agentArgs + "]");
e.printStackTrace(System.err);
return;
}
loadProps(XMLHelper.getChildNodeByName(configNode, "props", false));
loadInternalByteMan();
publify(XMLHelper.getChildNodeByName(configNode, "publify", false));
loadJavaAgents(XMLHelper.getChildNodeByName(configNode, "javaagents", false));
SenderFactory.getInstance();
Node aopNode = XMLHelper.getChildNodeByName(configNode, "aop", false);
if(aopNode!=null) {
loadTraceAnnotated(aopNode);
loadCodahale(XMLHelper.getChildNodeByName(aopNode, "codahale", false));
}
loadJmxConnectors(XMLHelper.getChildNodeByName(configNode, "jmx-connector", false));
loadMonitors(XMLHelper.getChildNodeByName(configNode, "monitors", false));
}
}
/**
* The passed node contains the comma separated names of classes that need to be publified
* @param publifyNode The publify config node
*/
private static void publify(Node publifyNode) {
SimpleLogger.info("\n\tExecuting publifier");
if(publifyNode==null) return;
String[] classes = XMLHelper.getNodeTextValue(publifyNode).split(",");
SimpleLogger.debug("Publifying Classes ", Arrays.toString(classes));
try {
ClassPublifier.getInstance().publify(classes);
StringBuilder b = new StringBuilder("\n\t===============================================\n\tThe following classes will be transformed to make them public\n\t===============================================");
for(String className: classes) {
if(className==null || className.trim().isEmpty()) continue;
b.append("\n\t").append(className.trim());
}
b.append("\n\t===============================================\n");
SimpleLogger.info(b.toString());
} catch (Exception ex) {
/* No Op */
}
}
/*
<jmx-connector>
<connectorclass>javax.management.remote.jmxmp.JMXMPConnectorServer</connectorclass>
<serviceurl>service:jmx:jmxmp://0.0.0.0:8006</serviceurl>
<env>
<prop name="" value=""/>
<prop name="" value=""/>
</env>
<register-domains>
<domain></domain>
</register-domains>
</jmx-connector>
<javaagents>
<javaagent jar="/home/nwhitehead/.m2/repository/org/jboss/byteman/byteman/2.1.1-SNAPSHOT/byteman-2.1.1-SNAPSHOT.jar">
<args>
</args>
</javaagent>
</javaagents>
*/
/** The pre-main manifest key */
public static final String PRE_MAIN = "Premain-Class";
/** The agent-main manifest key */
public static final String AGENT_MAIN = "Agent-Class";
/** The pre-main method name */
public static final String PRE_MAIN_METHOD = "premain";
/** The agent-main method name */
public static final String AGENT_MAIN_METHOD = "agentmain";
/** The method signature when providing instrumentation */
protected static final Class<?>[] INSTR_SIG = new Class[]{String.class, Instrumentation.class};
/** The method signature when not providing instrumentation */
protected static final Class<?>[] NO_INSTR_SIG = new Class[]{String.class};
/**
* Loads the internal helios byteman agent.
* Should be done after props are loaded.
*/
private static void loadInternalByteMan() {
try {
Class<?> byteManClazz = Class.forName("org.jboss.byteman.agent.Main");
byteManClazz.getDeclaredMethod("premain", String.class, Instrumentation.class)
.invoke(null, (String)null, instrumentation);
SimpleLogger.info("Loaded internal helios byteman agent");
} catch (Exception ex) {
SimpleLogger.error("Failed to load internal helios byteman agent", ex);
}
}
/**
* Attempts to initialize the java-agent in the passed agent jar file
* @param file The jar file that the java agent is in
* @param agentArgs The configured agent arguments
* @param supportJars An optional array of supporting jar files to be appended to the bootstrap classpath
* @return The name of the java agent class
* @throws Except)ion thrown on any error
*/
protected static String initAgent(File file, String agentArgs, File...supportJars) throws Exception {
JarFile jarFile = new JarFile(file);
Manifest manifest = jarFile.getManifest();
Attributes attrs = manifest.getMainAttributes();
String className = attrs.getValue(PRE_MAIN);
if(className==null) {
className = attrs.getValue(AGENT_MAIN);
}
if(className==null) {
throw new Exception("Could not find pre-main or agent-main classname in the jar manifest", new Throwable());
}
URLClassLoader classLoader = new URLClassLoader(new URL[]{file.toURI().toURL()});
Class<?> clazz = Class.forName(className, true, classLoader);
LinkedHashMap<String, Class<?>[]> map = new LinkedHashMap<String, Class<?>[]> (4);
map.put(PRE_MAIN_METHOD, INSTR_SIG);
map.put(AGENT_MAIN_METHOD, INSTR_SIG);
map.put(PRE_MAIN_METHOD.toUpperCase(), NO_INSTR_SIG);
map.put(AGENT_MAIN_METHOD.toUpperCase(), NO_INSTR_SIG);
Method method = null;
for(Map.Entry<String, Class<?>[]> entry: map.entrySet()) {
method = getMethod(clazz, entry.getKey().toLowerCase(), entry.getValue());
if(method!=null) break;
}
if(method==null) {
throw new Exception("Could not find premain or agentmain methods in the class [" + clazz.getName() + "]", new Throwable());
}
//instrumentation.appendToSystemClassLoaderSearch(jarFile);
for(File f: supportJars) {
if(!f.canRead()) {
SimpleLogger.warn("Cannot read support jar file [", f, "]. NOT adding to boot classpath");
continue;
}
instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(f));
}
instrumentation.appendToBootstrapClassLoaderSearch(jarFile);
if(method.getParameterTypes().length==2) {
method.invoke(null, agentArgs, instrumentation);
} else {
method.invoke(null, agentArgs);
}
return clazz.getName();
}
protected static Method getMethod(Class<?> clazz, String methodName, Class<?>...sig) {
try {
return clazz.getDeclaredMethod(methodName, sig);
} catch (Exception ex) {
return null;
}
}
/**
* Loads the configured chained java-agents
* @param agentsNode The java-agent configuration node
*/
protected static void loadJavaAgents(Node agentsNode) {
if(agentsNode==null) return;
SimpleLogger.info("Loading Chanined Java Agents");
for(Node agentNode: XMLHelper.getChildNodesByName(agentsNode, "javaagent", false)) {
String jarName = XMLHelper.getAttributeByName(agentNode, "jar", "null");
Node argNode = XMLHelper.getChildNodeByName(agentNode, "args", false);
String args = argNode==null ? null : XMLHelper.getNodeTextValue(argNode);
if(args!=null) {
args = args.trim();
if(args.isEmpty()) {
args = null;
}
}
try {
File file = new File(jarName.trim());
if(!file.canRead()) {
throw new Exception("Cannot read the file [" + file + "]");
}
Set<File> supportJars = new HashSet<File>();
for(Node jarNode: XMLHelper.getChildNodesByName(agentNode, "jar", false)) {
File f = new File(XMLHelper.getAttributeByName(jarNode, "name", ""));
if(file.canRead()) {
supportJars.add(f);
}
}
SimpleLogger.info("Adding support jars ", supportJars);
String className = initAgent(file, args, supportJars.toArray(new File[supportJars.size()]));
SimpleLogger.info("Loaded Java Agent from [", className, "]");
} catch (Throwable ex) {
SimpleLogger.warn("Failed to load and configure java agent [", jarName, "]", ex);
}
}
}
/*
<jars>
<jar name="c:/users/nwhitehe/.m2/repository/org/jboss/byteman/byteman-sample/2.1.1-SNAPSHOT/byteman-sample-2.1.1-SNAPSHOT.jar"/>
</jars>
*/
/**
* Loads the configured JMX Connector Server
* @param jmxNode The jmx connector configuration node
*/
protected static void loadJmxConnectors(Node jmxNode) {
if(jmxNode==null) return;
SimpleLogger.info("Loading JMXConnectorServer");
String connectorClass = XMLHelper.getNodeTextValue(XMLHelper.getChildNodeByName(jmxNode, "connectorclass", false));
String serviceUrl = XMLHelper.getNodeTextValue(XMLHelper.getChildNodeByName(jmxNode, "serviceurl", false));
try {
JMXServiceURL xurl = new JMXServiceURL(serviceUrl);
final ObjectName objectName = new ObjectName(String.format("org.helios.jmx:service=JMXConnectorServer,protocol=%s", xurl.getProtocol()));
final JMXConnectorServer server = JMXConnectorServerFactory.newJMXConnectorServer(xurl, null, JMXHelper.getHeliosMBeanServer());
JMXHelper.registerMBean(objectName, server);
Thread shutdown = new Thread("JMXConnectorServerShutdownThread") {
@Override
public void run() {
SimpleLogger.info("Stopping JMXConnectorServer [", objectName, "]");
try {
server.stop();
} catch (Exception ex) {
SimpleLogger.warn("Failed to stop JMXConnectorServer [", objectName, "]", ex);
}
}
};
Runtime.getRuntime().addShutdownHook(shutdown);
server.start();
} catch (Exception ex) {
SimpleLogger.warn("Failed to load JMX Connector", ex);
}
}
/**
* Loads and processes the codahale node
* @param codahaleNode the codahale AOP node
* <p><b>Example:</b>
* <pre>
* <aop>
* <codahale jar="<helios codahale jar URL>">
* <annotations/>
* <packages>org.helios.test,org.helios.test2</packages>
* </codahale>
* </aop>
* </pre></p>
*
*/
protected static void loadCodahale(Node codahaleNode) {
if(codahaleNode==null) return;
String jarUrl = XMLHelper.getAttributeByName(codahaleNode, "jar", null);
if(jarUrl==null) {
System.err.println("No jar URL defined for codeahale");
return;
}
try {
URL url = new URL(jarUrl);
info("Codahale jar:[", url ,"]");
//addURLToClassLoader(url);
// URL thirdParty = new URL("file:/C:/users/nwhitehe/.m2/repository/com/yammer/metrics/metrics-core/3.0.0-SNAPSHOT/metrics-core-3.0.0-SNAPSHOT.jar");
// addURLToClassLoader(thirdParty);
instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(url.getFile()));
//instrumentation.appendToSystemClassLoaderSearch(new JarFile(url.getFile()));
//Class<?> bootClazz = classLoader.loadClass(CODAHALE_BOOT_CLASS);
Class<?> bootClazz = ClassLoader.getSystemClassLoader().loadClass(CODAHALE_BOOT_CLASS);
Method bootMethod = bootClazz.getDeclaredMethod(CODAHALE_BOOT_METHOD, CODAHALE_BOOT_SIG);
bootMethod.invoke(null, agentArgs, instrumentation, codahaleNode);
} catch (Exception ex) {
String xml = null;
try {
xml = XMLHelper.renderNode(codahaleNode);
} catch (Exception e) {
xml = "== Failed to render ==";
}
warn("Failed to process codahale AOP configuration. XML was [\n", xml, "\n] Stack Trace follows.", ex);
}
}
/**
* Adds a URL to the classloader
* @param url the URL to add
*/
protected static void addURLToClassLoader(URL url) {
try {
addUrlMethod.invoke(classLoader, url);
} catch (Exception ex) {
error("Failed to add URL [", url, "] to classloader", ex);
throw new RuntimeException("Failed to add URL [" + url + "] to classloader", ex);
}
}
/**
* Loads the {@link TraceClassFileTransformer} that will instrument {@link Trace} annotated methods.
* @param aopNode The config node for aop
*/
protected static void loadTraceAnnotated(Node aopNode) {
if(aopNode==null) return;
Set<String> packages = new HashSet<String>();
Node traceAnnot = XMLHelper.getChildNodeByName(aopNode, "trace-annotated", false);
if(traceAnnot!=null) {
Node packageNode = XMLHelper.getChildNodeByName(traceAnnot, "packages", false);
if(packageNode!=null) {
String pnames = XMLHelper.getNodeTextValue(packageNode);
if(pnames!=null && !pnames.trim().isEmpty()) {
String[] frags = pnames.trim().split(",");
for(String s: frags) {
if(s.trim().isEmpty()) continue;
packages.add(s.trim());
}
}
}
}
if(!packages.isEmpty()) {
TraceClassFileTransformer tcf = new TraceClassFileTransformer(packages);
instrumentation.addTransformer(tcf, true);
debug("Added TraceClassFileTransformer for packages ", packages);
}
}
/**
* Configures XML defined monitors
* @param monitorsNode The monitors node from the XML config
*/
protected static void loadMonitors(Node monitorsNode) {
if(monitorsNode==null) return;
for(Node mNode : XMLHelper.getChildNodesByName(monitorsNode, "monitor", false)) {
try {
String name = XMLHelper.getAttributeByName(mNode, "name", null);
long period = XMLHelper.getLongAttributeByName(mNode, "period", 15000);
long startDelay = XMLHelper.getLongAttributeByName(mNode, "startDelay", -1L);
Class<Monitor> monClass = (Class<Monitor>) Class.forName(name);
Monitor monitor = monClass.newInstance();
monitor.setCollectPeriod(period);
Node propertyNode = XMLHelper.getChildNodeByName(mNode, "properties", false);
if(propertyNode!=null) {
String props = XMLHelper.getNodeTextValue(propertyNode);
Properties p = new Properties();
p.load(new StringReader(props.trim()));
Properties cleanedProperties = new Properties();
for(String key: p.stringPropertyNames()) {
String value = p.getProperty(key).trim();
key = key.trim();
cleanedProperties.setProperty(key, value);
}
monitor.setProperties(cleanedProperties);
}
if(startDelay<1) {
monitor.startMonitor();
} else {
monitor.startMonitor(startDelay);
}
} catch (Exception e) {
String xml = null;
try {
xml = XMLHelper.renderNode(mNode);
} catch (Exception ex) {
xml = "== Failed to render ==";
}
warn("Failed to process configured monitor. XML was [\n", xml, "\n] Stack Trace follows.", e);
}
}
}
/**
* Loads XML defined properties
* @param propsNode The properties node from the XML config
*/
protected static void loadProps(Node propsNode) {
if(propsNode!=null) {
for(Node pNode : XMLHelper.getChildNodesByName(propsNode, "prop", false)) {
try {
System.setProperty(XMLHelper.getAttributeByName(pNode, "name", null), XMLHelper.getAttributeByName(pNode, "value", null));
} catch (Exception e) {
System.err.println("Failed to process agent defined property in node [" + XMLHelper.renderNode(pNode) + "]");
}
}
}
}
/**
* Parses the XML input from the passed URL and returns the root node
* @param xmlUrl The URL of the XML config
* @return the parsed root node
*/
protected static Node getXMLConf(URL xmlUrl) {
Document doc = XMLHelper.parseXML(xmlUrl);
return doc.getDocumentElement();
}
}