/** * 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.monitor.script; import org.helios.apmrouter.jmx.JMXHelper; import org.helios.apmrouter.monitor.script.rhino.INativeObject; import org.helios.apmrouter.monitor.script.rhino.NativeFactory; import org.helios.apmrouter.util.StringHelper.Unformatter; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import javax.management.Attribute; import javax.management.AttributeList; import javax.management.MBeanServerConnection; import javax.management.ObjectName; import javax.management.openmbean.CompositeData; import javax.management.openmbean.TabularData; import javax.script.Bindings; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.util.*; /** * <p>Title: JMXScriptHelper</p> * <p>Description: A JMXHelper for scripting JMX checks and monitors</p> * <p>Company: Helios Development Group LLC</p> * @author Whitehead (nwhitehead AT heliosdev DOT org) * <p><code>org.helios.apmrouter.monitor.script.JMXScriptHelper</code></p> */ public class JMXScriptHelper { public static void main(String[] args) { LOG("Testing JMXRequest Submission"); try { ScriptEngine se = new ScriptEngineManager().getEngineByExtension("js"); Object obj = se.eval("function foo(){var a = {mbs:'jboss', on:'domain:foo=bar', attrs:['aaa', 'bbb']}; return a;}; foo();"); LOG(Arrays.toString(JSONNativeizer.fromNative(obj))); obj = se.eval("function foo(){var a = [{mbs:'jboss', on:'domain:foo=bar', attrs:['aaa', 'bbb']}, {mbs:'jboss', on:'domain:foo=foo', attrs:['xxx', 'yyy']}]; return a;}; foo();"); LOG(Arrays.toString(JSONNativeizer.fromNative(obj))); obj = se.eval("function foo(){var a = [{mbs:'jboss', on:'domain:foo=bar', attrs:['aaa', 'bbb']}, {mbs:'jboss', on:'domain:foo=foo', attrs:'xxx'}]; return a;}; foo();"); LOG(Arrays.toString(JSONNativeizer.fromNative(obj))); // Now, actual tests Bindings b = se.createBindings(); b.put("jmx", new JMXScriptHelper()); se.eval("var composite = {attr : 'HeapMemoryUsage', path : 'used'}"); se.eval("var compositeAll = {attr : 'HeapMemoryUsage', path : '*'}"); se.eval("var lastGC = {attr : 'LastGcInfo', path : ['memoryUsageAfterGc','PS Perm Gen','*']}"); se.getContext().setBindings(b, ScriptContext.GLOBAL_SCOPE); obj = se.eval("function foo(){var a = {on:'java.lang:type=Memory', attrs:['ObjectPendingFinalizationCount']}; jmx.getAttributes(a);}; foo();"); obj = se.eval("function foo(){var a = {on:'java.lang:type=Memory', attrs:composite}; jmx.getAttributes(a);}; foo();"); StringBuilder q = new StringBuilder(); for(int i = 0; i < 750000; i++) { q.append("0"); } System.gc(); obj = se.eval("function foo(){var a = {on:'java.lang:type=GarbageCollector,*', attrs:lastGC}; jmx.getAttributes(a);}; foo();"); } catch (Exception ex) { ex.printStackTrace(System.err); } } public static boolean isNumber(Object obj) { if(obj==null) return false; if(obj instanceof Number) return true; return false; } /** * Converts the passed JSON object to a native javascript object * @param json the JSON to convert * @return a native javascript object */ public static Object toNative(JSONObject json) { return JSONNativeizer.toNative(json); } /** * Converts the passed JSON string value to a native javascript object * @param json the JSON to convert * @return a native javascript object */ public static Object toNative(CharSequence json) { try { return JSONNativeizer.toNative(new JSONObject(json.toString())); } catch (Exception ex) { throw new RuntimeException(ex); } } /** * Converts the passed map to a native javascript object * @param map The map to convert * @return a native javascript object */ public static Object toNative(Map<?, ?> map) { return JSONNativeizer.toNative(new JSONObject(map)); } /** * Runtime exception only map to json converter * @param map The map to convert * @return the JSON object */ public static JSONObject toJSONObject(Map<?, ?> map) { try { return new JSONObject(map); } catch (Exception ex) { throw new RuntimeException(ex); } } /** * Formats the passed objects into a loggable string. * If the last object is a {@link Throwable}, it will be formatted into a stack trace. * @param msgs The objects to log * @return the loggable string */ public static String format(Object...msgs) { if(msgs==null||msgs.length<1) return ""; StringBuilder b = new StringBuilder(); int c = msgs.length-1; for(int i = 0; i <= c; i++) { if(i==c && msgs[i] instanceof Throwable) { b.append(formatStackTrace((Throwable)msgs[i])); } else { b.append(msgs[i]); } } return b.toString(); } /** * Formats a throwable's stack trace * @param t The throwable to format * @return the formatted stack trace */ public static String formatStackTrace(Throwable t) { if(t==null) return ""; ByteArrayOutputStream baos = new ByteArrayOutputStream(); t.printStackTrace(new PrintStream(baos, true)); try { baos.flush(); } catch (IOException e) { /* No Op */ } return baos.toString(); } /** * Low maintenance logger * @param msgs The objects to format and log */ private static void LOG(Object...msgs) { System.out.println(format(msgs)); } /** * Reads a numeric MBean attribute as a long * @param server The MBeanServer to read from * @param objectName The ObjectName of the target MBean * @param attribute The attribute name * @return The long value of the read attribute */ public static long getNumericAttribute(MBeanServerConnection server, CharSequence objectName, String attribute) { if(server==null) throw new UnavailableMBeanServerException(); try { return ((Number)server.getAttribute(JMXHelper.objectName(objectName), attribute)).longValue(); } catch (Exception ex) { LOG("Failed to read numeric attribute [" + attribute + "] from ObjectName [" + objectName + "]", ex); throw new RuntimeException("Failed to read numeric attribute [" + attribute + "] from ObjectName [" + objectName + "]", ex); } } /** * Reads a numeric MBean attribute as a long from the default MBeanServer * @param objectName The ObjectName of the target MBean * @param attribute The attribute name * @return The long value of the read attribute */ public static long getNumericAttribute(CharSequence objectName, String attribute) { return getNumericAttribute(JMXHelper.getHeliosMBeanServer(), objectName, attribute); } /** * Reads a numeric MBean attribute as a long from the named MBeanServer * @param mbeanServerName The default domain of the target MBeanServer * @param objectName The ObjectName of the target MBean * @param attribute The attribute name * @return The long value of the read attribute */ public static long getNumericAttribute(String mbeanServerName, CharSequence objectName, String attribute) { return getNumericAttribute(JMXHelper.getLocalMBeanServer(mbeanServerName), objectName, attribute); } /** * Builds a calculator response key * @param availableKeys The available key/values * @param groupingKeys The requested keys * @return a calculator key or null if any of the grouping keys do not resolve to a value in the available keys. */ protected static String buildCalcKey(Map<String, String> availableKeys, String...groupingKeys) { if(groupingKeys==null || groupingKeys.length<1) return null; StringBuilder b = new StringBuilder(); for(String gkey: groupingKeys) { String val = availableKeys.get(gkey); if(val==null) return null; // TODO: BAD KEY. Can we handle this better ? b.append(val).append("/"); } if(b.length()>0) b.deleteCharAt(b.length()-1); return b.toString(); } /** * Process an array of JMX calculators in the form of rhino native objects * @param calculator the rhino native object * @return The native rhino result object * @throws JSONException thrown on any json exception */ public static Object jmxCalc(Object calculator) throws JSONException { if(calculator==null) { throw new IllegalArgumentException("The passed calculator was null", new Throwable()); } JMXCalculator[] calculators = JSONNativeizer.extractCalculators(NativeFactory.newScriptable(calculator)); if(calculators==null || calculators.length<1) throw new IllegalArgumentException("No calculators found in submitted object", new Throwable()); final JSONArray results = new JSONArray(); for(JMXCalculator calc: calculators) { Map<String, List<Object>> accumulator = new HashMap<String, List<Object>>(); for(JMXScriptRequest req: calc.jmxRequests) { try { MBeanServerConnection conn = req.getMBeanServerConnection(); ObjectName on = JMXHelper.objectName(req.objectName); Set<ObjectName> objectNames = new HashSet<ObjectName>(); if(!on.isPattern()) { objectNames.add(on); } else { objectNames.addAll(conn.queryNames(on, null)); // TODO: Support exprs ? } for(ObjectName objectName: objectNames) { Map<String, String> objectNameKeys = objectName.getKeyPropertyList(); Map<String, String> keys = new HashMap<String, String>(calc.xParams.size() + objectNameKeys.size()); keys.putAll(calc.xParams); keys.putAll(objectNameKeys); String calcKey = buildCalcKey(keys, calc.group); if(calcKey==null) calcKey = calc.aggregateFunction.name(); AttributeList attrs = conn.getAttributes(objectName, req.attributeNames); Map<String, Object> attrMap = new HashMap<String, Object>(attrs.size()); for(Attribute attr: attrs.asList()) { attrMap.put(attr.getName(), attr.getValue()); } for(String attrName: req.attributeNames) { Object value = attrMap.get(attrName); if(value!=null) { List<Object> objList = accumulator.get(calcKey); if(objList==null) { objList = new ArrayList<Object>(); accumulator.put(calcKey, objList); } objList.add(value); } } } } catch (Exception ex) { ex.printStackTrace(System.err); } } // Aggregate the accumulator here. final JSONObject result = new JSONObject(); final JSONObject calcResults = new JSONObject(); result.put(JMXCalculator.KEY_NAME, calc.name); result.put(JMXCalculator.KEY_CALCS, calcResults); for(Map.Entry<String, List<Object>> entry: accumulator.entrySet()) { Object aggregated = calc.aggregateFunction.aggregate(entry.getValue()); calcResults.put(entry.getKey(), aggregated); } results.put(result); } Object nativeResult = JSONNativeizer.toNative(results); //LOG("RESULTS:" + results + "\n\tNative Object Result:" + nativeResult.getClass().getName()); return nativeResult; } /** * Process an array of JMX requests in the form of rhino native objects * @param requests the rhino native object * @return The native rhino result object * @throws JSONException thrown on any json exception */ public static Object getAttributes(Object requests) throws JSONException { if(requests==null) { throw new IllegalArgumentException("The passed object was null", new Throwable()); } try { final JSONObject result = new JSONObject(); final JSONArray entries = new JSONArray(); final JSONArray errors = new JSONArray(); result.put("results", entries); JMXScriptRequest[] jmxRequests = JSONNativeizer.fromNative(requests); for(JMXScriptRequest req : jmxRequests) { if(req.attributeNames.length<1 && req.compositeNames.isEmpty()) continue; try { MBeanServerConnection conn = req.getMBeanServerConnection(); ObjectName on = JMXHelper.objectName(req.objectName); Set<ObjectName> objectNames = new HashSet<ObjectName>(); if(!on.isPattern()) { objectNames.add(on); } else { objectNames.addAll(conn.queryNames(on, null)); // TODO: Support exprs ? } for(ObjectName objectName: objectNames) { JSONObject data = new JSONObject(); Map<String, Object> map = new HashMap<String, Object>(); map.put("d", objectName.getDomain()); map.put("p", new JSONObject(objectName.getKeyPropertyList())); AttributeList attrs = conn.getAttributes(objectName, req.attributeNames); for(Attribute attr: attrs.asList()) { data.put(attr.getName(), attr.getValue()); } attrs = conn.getAttributes(objectName, req.compositeNames.keySet().toArray(new String[0])); for(Attribute attr: attrs.asList()) { if(attr.getValue()==null) continue; String[] path = req.compositeNames.get(attr.getName()); if(attr.getValue() instanceof CompositeData || attr.getValue() instanceof TabularData) { Object value = attr.getValue(); for(int i = 0; i < path.length; i++) { String cKey = path[i].trim(); if(i==path.length-1 && "*".equals(cKey)) { value = getNext(value); } else { if("*".equals(cKey)) { /* TODO: Figure out how to implement this */ } else { value = getNext(value, cKey); } } } insertCompositeResult(data, attr.getName(), path, value); } else { data.put(attr.getName(), attr.getValue()); } } if(data.length()>0) { map.put("data", data); } entries.put(new JSONObject(map)); } } catch (Exception ex) { Map<String, Object> errMap = new HashMap<String, Object>(2); errMap.put("req", req.toJSON()); errMap.put("ex", ex.toString()); JSONObject err = new JSONObject(errMap); errors.put(err); ex.printStackTrace(System.err); } } if(errors.length()>0) { result.put("errs", errors); } //log(result.toString(2)); return JSONNativeizer.toNative(result); } catch (Exception ex) { ex.printStackTrace(System.err); return null; } } protected void processCalculators(final Collection<JMXCalculator> calculators, final JSONObject results, final MBeanServerConnection conn, final ObjectName objectName) { if(calculators.isEmpty()) return; for(JMXCalculator calc: calculators) { try { } catch (Exception ex) { ex.printStackTrace(System.err); } } } /** * Inserts a composite result * @param data The json object to insert into * @param topName The top attribute name * @param keys The composite path * @param value The value to insert */ protected static void insertCompositeResult(final JSONObject data, final String topName, final String[] keys, final Object value) { try { List<String> names = new ArrayList<String>(keys.length + 1); names.add(topName); for(int i = 0; i < keys.length-1; i++) { names.add(keys[i]); } String lastName = keys[keys.length-1]; JSONObject current = data; for(String s: names) { if(!current.has(s)) { current.put(s, new JSONObject()); } current = current.getJSONObject(s); } current.put(lastName, value); } catch (Exception ex) { throw new RuntimeException("Failed to insety composite result", ex); } } /** * Extracts the keyed value from an instance of {@link CompositeData} or {@link TabularData} * @param obj an instance of {@link CompositeData} or {@link TabularData} * @param key The key * @return The extracted value or null if the passed object was not an instance of {@link CompositeData} or {@link TabularData} */ @SuppressWarnings("rawtypes") protected static Object getNext(Object obj, String key) { if(obj==null) throw new IllegalArgumentException("The passed object was null", new Throwable()); if(key==null || key.trim().isEmpty()) throw new IllegalArgumentException("The passed key was empty or null", new Throwable()); if(obj instanceof CompositeData) { return ((CompositeData)obj).get(key); } else if(obj instanceof TabularData) { return ((TabularData)obj).get(new Object[]{key}); // TODO: This will not work most of the time. } else if(obj instanceof Map) { return ((Map)obj).get(obj); } else { return null; } } protected static Map<String, Object> getNext(Object obj) { if(obj==null) return Collections.emptyMap(); Map<String, Object> map = new HashMap<String, Object>(); if(obj instanceof CompositeData) { CompositeData cd = (CompositeData)obj; for(String key: cd.getCompositeType().keySet()) { if(cd.get(key) instanceof CompositeData) { return getNext(cd.get(key)); } map.put(key, cd.get(key)); } return map; } else if(obj instanceof TabularData) { TabularData td = (TabularData)obj; Collection<CompositeData> data = (Collection<CompositeData>) td.values(); int index = 0; for(CompositeData cd: data) { map.put("" + index, getNext(cd)); index++; } return map; } else { return null; } } // /** // * Retrieves the value of the named attribute from all matching MBeans by the supplied ObjectName pattern and computes an aggregate. // * @param aggregateFunction The name of the aggregate function which should be one of the enumerated in {@link AggregateFunction} // * @param server The MBeanServer to fetch from // * @param objectName The JMX ObjectName pattern that defines the group of MBeans that values should be retrieved from // * @param attributeName The name of the attribute to retrieve values from // * @param objectNameKeys The property keys in the located matching ObjectNames that can be used to uniquely identify the MBean // * @return The specified aggregate of all the retrieved values // */ // public static Map<String, Long> getSummaryAggregate(String aggregateFunction, MBeanServerConnection server, CharSequence objectName, String attributeName, String...objectNameKeys) { // AggregateFunction af = AggregateFunction.forName(aggregateFunction); // try { // Set<ObjectName> matches = server.queryNames(JMXHelper.objectName(objectName), null); // if(matches.isEmpty()) return 0; // List<Object> values = new ArrayList<Object>(matches.size()); // for(ObjectName on: matches) { // try { // values.add(server.getAttribute(on, attributeName)); // } catch (Exception ex) {/* No Op */} // } // if(values.isEmpty()) return 0; // Object result = af.aggregate(values); // if(result instanceof Number) { // return ((Number)result).longValue(); // } // throw new Exception("The aggregate function [" + af.name() + "] did not return a number but a [" + result.getClass().getName() + "]", new Throwable()); // } catch (Exception ex) { // LOG.error("Failed to read numeric aggregate for " + attributeName + " from pattern [" + objectName + "]", ex); // throw new RuntimeException("Failed to read numeric aggregate for " + attributeName + " from pattern [" + objectName + "]", ex); // } // } /** * Builds a key by concatenating the the passed ObjectName's key property values whose keys are in the passed keys into one string, comma separated. * @param on The object name to build a key from * @param objectNameKeys The keys of the object name's for which values should be included in the key * @return the created key */ public static String formatKey(ObjectName on, String...objectNameKeys) { if(on==null) throw new IllegalArgumentException("The passed ObjectName was null", new Throwable()); if(objectNameKeys==null || objectNameKeys.length<1) throw new IllegalArgumentException("The passed ObjectNameKeys was null or empty", new Throwable()); StringBuilder b = new StringBuilder(); Hashtable<String, String> props = on.getKeyPropertyList(); for(String key: objectNameKeys) { key = key.trim(); String val = props.get(key); if(val!=null) { b.append(val).append(","); } } if(b.charAt(b.length()-1)==',') b.deleteCharAt(b.length()-1); return b.toString(); } /** * Determines if the passed ObjectName is a registered MBean in the passed server * @param server The MBeanServer * @param objectName The ObjectName to test * @return true if the ObjectName represents at least one registered MBean */ public static boolean isRegistered(MBeanServerConnection server, CharSequence objectName) { try { ObjectName on = JMXHelper.objectName(objectName); if(on.isPattern()) { return !server.queryMBeans(on, null).isEmpty(); } return server.isRegistered(JMXHelper.objectName(objectName)); } catch (Exception ex) { throw new RuntimeException(ex); } } /** * Determines if the passed ObjectName is a registered MBean in the named server * @param mbeanServerName The default domain of the target MBeanServer * @param objectName The ObjectName to test * @return true if the ObjectName represents at least one registered MBean */ public static boolean isRegistered(String mbeanServerName, CharSequence objectName) { return isRegistered(JMXHelper.getLocalMBeanServer(mbeanServerName), objectName); } /** * Determines if the passed ObjectName is a registered MBean in the default server * @param objectName The ObjectName to test * @return true if the ObjectName represents at least one registered MBean */ public static boolean isRegistered(CharSequence objectName) { return isRegistered(JMXHelper.getHeliosMBeanServer(), objectName); } /** * Cretaes a new NativeUnformatter * @param pattern The unformatter's pattern * @param keys The unformatter's keys * @return a new NativeUnformatter */ public static NativeUnformatter unformatter(CharSequence pattern, String...keys) { return new NativeUnformatter(pattern, keys); } /** * Cretaes a new NativeUnformatter * @param pattern The unformatter's pattern * @param options The match flags bit mask * @param keys The unformatter's keys * @return a new NativeUnformatter */ public static NativeUnformatter unformatter(CharSequence pattern, int options, String...keys) { return new NativeUnformatter(pattern, options, keys); } /** * <p>Title: NativeUnformatter</p> * <p>Description: An extension of {@link Unformatter} that return rhino native objects</p> * <p>Company: Helios Development Group LLC</p> * @author Whitehead (nwhitehead AT heliosdev DOT org) * <p><code>org.helios.apmrouter.monitor.script.JMXScriptHelper.NativeUnformatter</code></p> */ public static class NativeUnformatter extends Unformatter { /** * Creates a new NativeUnformatter * @param pattern The regex pattern * @param options The match flags bit mask * @param keys The keys to key the returned map with */ public NativeUnformatter(CharSequence pattern, int options, String[] keys) { super(pattern, options, keys); } /** * Creates a new NativeUnformatter * @param pattern The regex pattern * @param keys The keys to key the returned map with */ public NativeUnformatter(CharSequence pattern, String... keys) { super(pattern, keys); } /** * Unformats the passed string value into a rhino native object * @param str The string value to parse * @return A rhino native object of the extracted values keyed by the positional string keys */ public Object jsunformat(CharSequence str) { INativeObject obj = NativeFactory.newNativeObject(); for(Map.Entry<String, String> entry : super.unformat(str).entrySet()) { obj.putProperty(entry.getKey(), entry.getValue()); } return obj.getUnderlying(); } } }