/* * Copyright 2015-2017 JKOOL, LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jkoolcloud.tnt4j.stream.jmx; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.lang.instrument.Instrumentation; import java.lang.reflect.Method; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import javax.management.*; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import org.apache.commons.lang3.StringUtils; import com.jkoolcloud.tnt4j.stream.jmx.core.Sampler; import com.jkoolcloud.tnt4j.stream.jmx.factory.DefaultSamplerFactory; import com.jkoolcloud.tnt4j.stream.jmx.factory.SamplerFactory; import com.jkoolcloud.tnt4j.utils.Utils; /** * <p> * This class provides java agent implementation {@link #premain(String, Instrumentation)}, * {@link #agentmain(String, Instrumentation)} as well as {@link #main(String[])} entry point to run as a standalone * application. * </p> * * @version $Revision: 1 $ * * @see SamplerFactory */ public class SamplingAgent { protected static Sampler platformJmx; protected static ConcurrentHashMap<MBeanServerConnection, Sampler> STREAM_AGENTS = new ConcurrentHashMap<MBeanServerConnection, Sampler>(89); protected static boolean TRACE = Boolean.getBoolean("com.jkoolcloud.tnt4j.stream.jmx.agent.trace"); private static final String PARAM_VM_DESCRIPTOR = "-vm:"; private static final String PARAM_AGENT_LIB_PATH = "-ap:"; private static final String PARAM_AGENT_OPTIONS = "-ao:"; private static final String AGENT_MODE_AGENT = "-agent"; private static final String AGENT_MODE_ATTACH = "-attach"; private static final String AGENT_MODE_CONNECT = "-connect"; private static final String AGENT_ARG_MODE = "agent.mode"; private static final String AGENT_ARG_VM = "vm.descriptor"; private static final String AGENT_ARG_LIB_PATH = "agent.lib.path"; private static final String AGENT_ARG_OPTIONS = "agent.options"; private static final String AGENT_ARG_I_FILTER = "beans.include.filter"; private static final String AGENT_ARG_E_FILTER = "beans.exclude.filter"; private static final String AGENT_ARG_S_TIME = "agent.sample.time"; private static final String AGENT_ARG_W_TIME = "agent.wait.time"; private static final String DEFAULT_AGENT_OPTIONS = Sampler.JMX_FILTER_ALL + "!" + Sampler.JMX_FILTER_NONE + "!" + Sampler.JMX_SAMPLE_PERIOD; /** * Entry point to be loaded as {@code -javaagent:jarpath="mbean-filter!sample.ms"} command line. * Example: {@code -javaagent:tnt4j-sample-jmx.jar="*:*!30000"} * * @param options '!' separated list of options mbean-filter!sample.ms, where mbean-filter is semicolon separated list of mbean filters * @param inst instrumentation handle */ public static void premain(String options, Instrumentation inst) throws IOException { String incFilter = System.getProperty("com.jkoolcloud.tnt4j.stream.jmx.include.filter", Sampler.JMX_FILTER_ALL); String excFilter = System.getProperty("com.jkoolcloud.tnt4j.stream.jmx.exclude.filter", Sampler.JMX_FILTER_NONE); int period = Integer.getInteger("com.jkoolcloud.tnt4j.stream.jmx.period", Sampler.JMX_SAMPLE_PERIOD); if (options != null) { String[] args = options.split("!"); if (args.length >= 2) { incFilter = args[0]; period = Integer.parseInt(args[1]); } } sample(incFilter, excFilter, period, TimeUnit.MILLISECONDS); System.out.println("SamplingAgent.premain: inlcude.filter=" + incFilter + ", exclude.filter=" + excFilter + ", sample.ms=" + period + ", trace=" + TRACE + ", tnt4j.config=" + System.getProperty("tnt4j.config") + ", jmx.sample.list=" + STREAM_AGENTS); } /** * Entry point to bea loaded as JVM agent. Does same as {@link #premain(String, Instrumentation)}. * * @param agentArgs '!' separated list of options mbean-filter!sample.ms, where mbean-filter is semicolon separated list of mbean filters * @param inst instrumentation handle * @throws IOException if any I/O exception occurs while starting agent * * @see #premain(String, Instrumentation) */ public static void agentmain(String agentArgs, Instrumentation inst) throws Exception { String agentParams = ""; String tnt4jProp = System.getProperty("tnt4j.config"); String agentLibPath = ""; if (!Utils.isEmpty(agentArgs)) { String[] args = agentArgs.split("!"); if (args.length >= 2) { agentParams = args[0] + "!" + args[1]; } for (String arg : args) { if (arg.startsWith("-Dtnt4j.config")) { if (Utils.isEmpty(tnt4jProp)) { String[] prop = arg.split("="); tnt4jProp = prop.length > 1 ? prop[1] : null; System.setProperty("tnt4j.config", tnt4jProp); } } else if (arg.startsWith("-DSamplingAgent.path")) { String[] prop = arg.split("="); agentLibPath = prop.length > 1 ? prop[1] : null; } else if (arg.startsWith("trace=")) { String[] prop = arg.split("="); TRACE = prop.length > 1 ? Boolean.parseBoolean(prop[1]) : TRACE; } } } System.out.println("SamplingAgent.agentmain: agent.params=" + agentParams + ", agent.lib.path=" + agentLibPath + ", trace=" + TRACE + ", tnt4j.config=" + System.getProperty("tnt4j.config")); File agentPath = new File(agentLibPath); final String[] classPathEntries = agentPath.list(new JarFilter()); if (classPathEntries != null) { File pathFile; for (String classPathEntry : classPathEntries) { pathFile = new File(classPathEntry); System.out.println("SamplingAgent.agentmain: extending classpath with: " + pathFile.getAbsolutePath()); extendClasspath(pathFile.toURI().toURL()); } } premain(agentParams, inst); } /** * Loads required classpath entries to running JVM. * * @param classPathEntriesURL classpath entries URLs to attach to JVM * @throws Exception if exception occurs while extending system class loader's classpath */ private static void extendClasspath(URL... classPathEntriesURL) throws Exception { URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader(); Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); method.setAccessible(true); for (URL classPathEntryURL : classPathEntriesURL) { try { method.invoke(classLoader, classPathEntryURL); } catch (Exception e) { System.err.println("SamplingAgent.extendClasspath: Could not load lib " + classPathEntryURL); System.err.println(" Exception: " + e.getLocalizedMessage()); // e.printStackTrace (); } } } /** * Main entry point for running as a standalone application (test only). * * @param args * argument list: [sampling-mode vm-descriptor] [mbean-filter sample_time_ms] */ public static void main(String[] args) throws Exception { Properties props = new Properties(); boolean argsValid = parseArgs(props, args); if (argsValid) { String am = props.getProperty(AGENT_ARG_MODE); if (AGENT_MODE_CONNECT.equalsIgnoreCase(am)) { String vmDescr = props.getProperty(AGENT_ARG_VM); String agentOptions = props.getProperty(AGENT_ARG_OPTIONS, DEFAULT_AGENT_OPTIONS); connect(vmDescr, agentOptions); } else if (AGENT_MODE_ATTACH.equalsIgnoreCase(am)) { String vmDescr = props.getProperty(AGENT_ARG_VM); String jarPath = props.getProperty(AGENT_ARG_LIB_PATH); String agentOptions = props.getProperty(AGENT_ARG_OPTIONS, DEFAULT_AGENT_OPTIONS); attach(vmDescr, jarPath, agentOptions); } else { try { String inclF = props.getProperty(AGENT_ARG_I_FILTER, Sampler.JMX_FILTER_ALL); String exclF = props.getProperty(AGENT_ARG_E_FILTER, Sampler.JMX_FILTER_NONE); long sample_time = Integer .parseInt(props.getProperty(AGENT_ARG_S_TIME, String.valueOf(Sampler.JMX_SAMPLE_PERIOD))); long wait_time = Integer.parseInt(props.getProperty(AGENT_ARG_W_TIME, "0")); sample(inclF, exclF, sample_time, TimeUnit.MILLISECONDS); System.out.println("SamplingAgent.main: inlcude.filter=" + inclF + ", exclude.filter=" + exclF + ", sample.ms=" + sample_time + ", wait.ms=" + wait_time + ", trace=" + TRACE + ", tnt4j.config=" + System.getProperty("tnt4j.config") + ", jmx.sample.list=" + STREAM_AGENTS); synchronized (platformJmx) { platformJmx.wait(wait_time); } } catch (Throwable ex) { ex.printStackTrace(); } } } else { System.out.println("Usage: mbean-filter exclude-filter sample-ms [wait-ms] (e.g \"*:*\" \"\" 10000 60000)"); System.out.println(" or: -attach -vm:vmName/vmId -ap:agentJarPath -ao:agentOptions (e.g -attach -vm:activemq -ap:[ENV_PATH]/tnt-stream-jmx.jar -ao:*:*!!10000)"); System.out.println(" or: -connect -vm:vmName/vmId/JMX_URL -ao:agentOptions (e.g -connect -vm:activemq -ao:*:*!!10000"); System.out.println(); System.out.println("Parameters definition:"); System.out.println(" -ao: - agent options string using '!' symbol as delimiter. Options format: mbean-filter!exclude-filter!sample-ms"); System.out.println(" mbean-filter - MBean include name filter defined using object name pattern: domainName:keysSet"); System.out.println(" exclude-filter - MBean exclude name filter defined using object name pattern: domainName:keysSet"); System.out.println(" sample-ms - MBeans sampling rate in milliseconds"); System.exit(1); } } private static boolean parseArgs(Properties props, String... args) { boolean ac = AGENT_MODE_ATTACH.equalsIgnoreCase(args[0]) || AGENT_MODE_CONNECT.equalsIgnoreCase(args[0]); if (ac) { props.setProperty(AGENT_ARG_MODE, args[0]); for (int ai = 1; ai < args.length; ai++) { String arg = args[ai]; if (StringUtils.isEmpty(arg)) { continue; } if (arg.startsWith(PARAM_VM_DESCRIPTOR)) { if (StringUtils.isNotEmpty(props.getProperty(AGENT_ARG_VM))) { System.out.println("JVM descriptor aready defined. Can not use argument [" + PARAM_VM_DESCRIPTOR + "] multiple times."); return false; } String pValue = arg.substring(PARAM_VM_DESCRIPTOR.length()); if (StringUtils.isEmpty(pValue)) { System.out.println("Missing argument '" + PARAM_VM_DESCRIPTOR + "' value"); return false; } props.setProperty(AGENT_ARG_VM, pValue); } else if (arg.startsWith(PARAM_AGENT_LIB_PATH)) { if (StringUtils.isNotEmpty(props.getProperty(AGENT_ARG_LIB_PATH))) { System.out.println("Agent library path aready defined. Can not use argument [" + PARAM_AGENT_LIB_PATH + "] multiple times."); return false; } String pValue = arg.substring(PARAM_AGENT_LIB_PATH.length()); if (StringUtils.isEmpty(pValue)) { System.out.println("Missing argument '" + PARAM_AGENT_LIB_PATH + "' value"); return false; } props.setProperty(AGENT_ARG_LIB_PATH, pValue); } else if (arg.startsWith(PARAM_AGENT_OPTIONS)) { if (StringUtils.isNotEmpty(props.getProperty(AGENT_ARG_OPTIONS))) { System.out.println("Agent options aready defined. Can not use argument [" + PARAM_AGENT_OPTIONS + "] multiple times."); return false; } String pValue = arg.substring(PARAM_AGENT_OPTIONS.length()); if (StringUtils.isEmpty(pValue)) { System.out.println("Missing argument '" + PARAM_AGENT_OPTIONS + "' value"); return false; } props.setProperty(AGENT_ARG_OPTIONS, pValue); } else { System.out.println("Invalid argument: " + arg); return false; } } if (StringUtils.isEmpty(props.getProperty(AGENT_ARG_VM))) { System.out.println("Missing mandatory argument '" + PARAM_VM_DESCRIPTOR + "' defining JVM descriptor."); return false; } // if (AGENT_MODE_ATTACH.equalsIgnoreCase(props.getProperty(AGENT_ARG_MODE)) // && StringUtils.isEmpty(props.getProperty(AGENT_ARG_LIB_PATH))) { // System.out.println( // "Missing mandatory argument '" + PARAM_AGENT_LIB_PATH + "' defining agent library path."); // return false; // } } else { props.setProperty(AGENT_ARG_MODE, AGENT_MODE_AGENT); props.setProperty(AGENT_ARG_I_FILTER, args[0]); props.setProperty(AGENT_ARG_E_FILTER, args[1]); props.setProperty(AGENT_ARG_S_TIME, args[2]); if (args.length > 3) { props.setProperty(AGENT_ARG_W_TIME, args[3]); } } return true; } /** * Schedule sample with default MBean server instance as well as all registered MBean servers within the JVM. */ public static void sample() throws IOException { String incFilter = System.getProperty("com.jkoolcloud.tnt4j.stream.jmx.include.filter", Sampler.JMX_FILTER_ALL); String excFilter = System.getProperty("com.jkoolcloud.tnt4j.stream.jmx.exclude.filter", Sampler.JMX_FILTER_NONE); int period = Integer.getInteger("com.jkoolcloud.tnt4j.stream.jmx.period", Sampler.JMX_SAMPLE_PERIOD); sample(incFilter, excFilter, period); } /** * Schedule sample with default MBean server instance as well as all registered MBean servers within the JVM. * * @param incFilter semicolon separated include filter list * @param period sampling in milliseconds. */ public static void sample(String incFilter, long period) throws IOException { sample(incFilter, null, period); } /** * Schedule sample with default MBean server instance as well as all registered MBean servers within the JVM. * * @param incFilter semicolon separated include filter list * @param excFilter semicolon separated exclude filter list (null if empty) * @param period sampling in milliseconds. */ public static void sample(String incFilter, String excFilter, long period) throws IOException { sample(incFilter, excFilter, period, TimeUnit.MILLISECONDS); } /** * Schedule sample with default MBean server instance as well as all registered MBean servers within the JVM. * * @param incFilter semicolon separated include filter list * @param excFilter semicolon separated exclude filter list (null if empty) * @param period sampling time * @param tunit time units for sampling period */ public static void sample(String incFilter, String excFilter, long period, TimeUnit tunit) throws IOException { SamplerFactory pFactory = initPlatformJMX(incFilter, excFilter, period, tunit, null); // find other registered MBean servers and initiate sampling for all ArrayList<MBeanServer> mlist = MBeanServerFactory.findMBeanServer(null); for (MBeanServer server : mlist) { Sampler jmxp = STREAM_AGENTS.get(server); if (jmxp == null) { jmxp = pFactory.newInstance(server); jmxp.setSchedule(incFilter, excFilter, period, tunit) .addListener(new DefaultSampleListener(System.out, TRACE)).run(); STREAM_AGENTS.put(jmxp.getMBeanServer(), jmxp); } } } /** * Schedule sample using defined JMX connector to get MBean server connection instance to monitored JVM. * * @param incFilter semicolon separated include filter list * @param excFilter semicolon separated exclude filter list (null if empty) * @param period sampling time * @param tunit time units for sampling period * @param conn JMX connector to get MBean server connection instance */ public static void sample(String incFilter, String excFilter, long period, TimeUnit tunit, JMXConnector conn) throws IOException { // get MBeanServerConnection from JMX RMI connector MBeanServerConnection mbSrvConn = conn.getMBeanServerConnection(); SamplerFactory pFactory = initPlatformJMX(incFilter, excFilter, period, tunit, mbSrvConn); } private static SamplerFactory initPlatformJMX(String incFilter, String excFilter, long period, TimeUnit tunit, MBeanServerConnection mbSrvConn) throws IOException { // obtain a default sample factory SamplerFactory pFactory = DefaultSamplerFactory.getInstance(); if (platformJmx == null) { // create new sampler with default MBeanServer instance platformJmx = mbSrvConn == null ? pFactory.newInstance() : pFactory.newInstance(mbSrvConn); // schedule sample with a given filter and sampling period platformJmx.setSchedule(incFilter, excFilter, period, tunit) .addListener(new DefaultSampleListener(System.out, TRACE)).run(); STREAM_AGENTS.put(platformJmx.getMBeanServer(), platformJmx); } return pFactory; } /** * Attaches to {@code vmDescr} defined JVM as agent. * * @param vmDescr JVM descriptor: name fragment or pid * @param agentJarPath agent JAR path * @param agentOptions agent options * @throws Exception if any exception occurs while attaching to JVM */ public static void attach(String vmDescr, String agentJarPath, String agentOptions) throws Exception { if (Utils.isEmpty(vmDescr)) { throw new RuntimeException("Java VM descriptor must be not empty!.."); } File pathFile; if (StringUtils.isEmpty(agentJarPath)) { System.out.println("SamplingAgent.attach: no agent jar defined"); pathFile = getSAPath(); } else { pathFile = new File(agentJarPath); if (!pathFile.exists()) { System.out.println("SamplingAgent.attach: non-existing argument defined agent jar: " + agentJarPath); System.out.println(" absolute agent jar path: " + pathFile.getAbsolutePath()); pathFile = getSAPath(); } } String agentPath = pathFile.getAbsolutePath(); agentOptions += "!trace=" + TRACE; agentOptions += "!-DSamplingAgent.path=" + agentPath; String tnt4jConf = System.getProperty("tnt4j.config"); if (!Utils.isEmpty(tnt4jConf)) { String tnt4jPropPath = new File(tnt4jConf).getAbsolutePath(); agentOptions += "!-Dtnt4j.config=" + tnt4jPropPath; } VMUtils.attachVM(vmDescr, agentPath, agentOptions); } private static File getSAPath() throws URISyntaxException { String saPath = SamplingAgent.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath(); System.out.println("SamplingAgent.attach: using SamplingAgent class referenced jar path: " + saPath); File agentFile = new File(saPath); if (!agentFile.exists()) { throw new RuntimeException("Could not find agent jar: " + agentFile.getAbsolutePath()); } return agentFile; } /** * Connects to {@code vmDescr} defined JVM over {@link JMXConnector} an uses {@link MBeanServerConnection} to * collect samples. * * @param vmDescr JVM descriptor: JMX service URI, local JVM name fragment or pid * @param options '!' separated list of options mbean-filter!sample.ms, where mbean-filter is semicolon separated list of mbean filters * @throws Exception if any exception occurs while connecting to JVM */ public static void connect(String vmDescr, String options) throws Exception { if (Utils.isEmpty(vmDescr)) { throw new RuntimeException("Java VM descriptor must be not empty!.."); } String connectorAddress; if (vmDescr.startsWith("service:jmx:")) { connectorAddress = vmDescr; } else { connectorAddress = VMUtils.getVMConnAddress(vmDescr); } String incFilter = System.getProperty("com.jkoolcloud.tnt4j.stream.jmx.include.filter", Sampler.JMX_FILTER_ALL); String excFilter = System.getProperty("com.jkoolcloud.tnt4j.stream.jmx.exclude.filter", Sampler.JMX_FILTER_NONE); int period = Integer.getInteger("com.jkoolcloud.tnt4j.stream.jmx.period", Sampler.JMX_SAMPLE_PERIOD); if (options != null) { String[] args = options.split("!"); if (args.length > 0) { incFilter = args[0]; } if (args.length > 1) { excFilter = args.length > 2 ? args[1] : Sampler.JMX_FILTER_NONE; period = Integer.parseInt(args.length > 2 ? args[2] : args[1]); } } System.out.println("SamplingAgent.connect: making JMX service URL from address=" + connectorAddress); JMXServiceURL url = new JMXServiceURL(connectorAddress); System.out.println("SamplingAgent.connect: connecting JMX service using URL=" + url); JMXConnector connector = JMXConnectorFactory.connect(url); try { sample(incFilter, excFilter, period, TimeUnit.MILLISECONDS, connector); System.out.println("SamplingAgent.connect: inlcude.filter=" + incFilter + ", exclude.filter=" + excFilter + ", sample.ms=" + period + ", trace=" + TRACE + ", tnt4j.config=" + System.getProperty("tnt4j.config") + ", jmx.sample.list=" + STREAM_AGENTS); NotificationListener cnl = new NotificationListener() { @Override public void handleNotification(Notification notification, Object key) { if (notification.getType().contains("closed") || notification.getType().contains("failed") || notification.getType().contains("lost")) { System.out.println( "SamplingAgent.connect: JMX connection status change: " + notification.getType()); stopConnection(); } } }; connector.addConnectionNotificationListener(cnl, null, null); Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { stopConnection(); } })); synchronized (platformJmx) { platformJmx.wait(); } connector.removeConnectionNotificationListener(cnl); System.out.println("SamplingAgent.connect: Stopping Stream-JMX..."); } finally { try { connector.close(); } catch (IOException exc) { } } } private static void stopConnection() { if (platformJmx != null) { synchronized (platformJmx) { platformJmx.notifyAll(); } } } /** * Obtain a map of all scheduled MBeanServerConnections and associated sample references. * * @return map of all scheduled MBeanServerConnections and associated sample references. */ public static Map<MBeanServerConnection, Sampler> getSamplers() { HashMap<MBeanServerConnection, Sampler> copy = new HashMap<MBeanServerConnection, Sampler>(89); copy.putAll(STREAM_AGENTS); return copy; } /** * Cancel and close all outstanding {@link Sampler} instances and stop all sampling for all {@code MBeanServer} * instances. */ public static void cancel() { for (Sampler sampler : STREAM_AGENTS.values()) { sampler.cancel(); } STREAM_AGENTS.clear(); } /** * Cancel and close all sampling for a given {@code MBeanServer} instance. * * @param mServerConn MBeanServerConnection instance */ public static void cancel(MBeanServerConnection mServerConn) { Sampler sampler = STREAM_AGENTS.remove(mServerConn); if (sampler != null) { sampler.cancel(); } } private static class JarFilter implements FilenameFilter { JarFilter() { } @Override public boolean accept(File dir, String name) { String nfn = name.toLowerCase(); return nfn.endsWith(".jar") || nfn.endsWith(".zip"); } } }