diff --git a/plugins/java/jmx2munin/.gitignore b/plugins/java/jmx2munin/.gitignore new file mode 100644 index 00000000..0a99f329 --- /dev/null +++ b/plugins/java/jmx2munin/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +.classpath +.project +.fatjar +target +eclipse +old +bin diff --git a/plugins/java/jmx2munin/README.md b/plugins/java/jmx2munin/README.md new file mode 100644 index 00000000..d1899db0 --- /dev/null +++ b/plugins/java/jmx2munin/README.md @@ -0,0 +1,79 @@ +# jmx2munin + +The [jmx2munin](http://github.com/tcurdt/jmx2munin) project exposes JMX MBean attributes to [Munin](http://munin-monitoring.org/). +Some of it's features: + + * strictly complies to the plugin format + * exposes composite types like Lists, Maps, Set as useful as possible + * String values can be mapped to numbers + +# How to use + +This is what the Munin script will call. So you should test this first. Of course with your parameters. This example expose all Cassandra information to Munin. + + java -jar jmx2munin.jar \ + -url service:jmx:rmi:///jndi/rmi://localhost:8080/jmxrmi \ + -query "org.apache.cassandra.*:*" + +The "url" parameters specifies the JMX URL, the query selects the MBeans (and optionally also the attributes) to expose. + + java -jar jmx2munin.jar \ + -url service:jmx:rmi:///jndi/rmi://localhost:8080/jmxrmi \ + -query "org.apache.cassandra.*:*" \ + -attribute org_apache_cassandra_db_storageservice_livenodes_size + +The script that does the actual interaction with munin you can find in the contrib section. It's the one you should link in the your Munin plugin directory. + + :/etc/munin/plugins$ ls -la cassandra_* + lrwxrwxrwx 1 root root 37 2011-04-07 19:58 cassandra_nodes_in_cluster -> /usr/share/munin/plugins/jmx2munin.sh + +In the plugin conf you point to the correct configuration + + [cassandra_*] + env.query org.apache.cassandra.*:* + + [cassandra_nodes_in_cluster] + env.config cassandra/nodes_in_cluster + +A possible configuration could look like this + + graph_title Number of Nodes in Cluster + graph_vlabel org_apache_cassandra_db_storageservice_livenodes_size + org_apache_cassandra_db_storageservice_livenodes_size.label number of nodes + +The script will extract the attributes from the config and caches the JMX results to reduce the load when showing many values. + +# More advanced + +Sometimes it can be useful to track String values by mapping them into an enum as they really describe states. To find this possible candidates you can call: + + java -jar jmx2munin.jar \ + -url service:jmx:rmi:///jndi/rmi://localhost:8080/jmxrmi \ + -query "org.apache.cassandra.*:*" \ + list + +It should output a list of possible candidates. This can now be turned into a enum configuration file: + + [org.apache.cassandra.db.StorageService:OperationMode] + 0 = ^Normal + 1 = ^Client + 2 = ^Joining + 3 = ^Bootstrapping + 4 = ^Leaving + 5 = ^Decommissioned + 6 = ^Starting drain + 7 = ^Node is drained + +Which you then can provide: + + java -jar jmx2munin.jar \ + -url service:jmx:rmi:///jndi/rmi://localhost:8080/jmxrmi \ + -query "org.apache.cassandra.*:*" \ + -enums /path/to/enums.cfg + +Now matching values get replaced by their numerical representation. On the left needs to be a unique number on the right side is a regular expression. If a string cannot be matched according to the spec "U" for "undefined" will be returned. + +# License + +Licensed under the Apache License, Version 2.0 (the "License") +You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/plugins/java/jmx2munin/contrib/jmx2munin.cfg/cassandra/nodes_in_cluster b/plugins/java/jmx2munin/contrib/jmx2munin.cfg/cassandra/nodes_in_cluster new file mode 100644 index 00000000..7fe323e3 --- /dev/null +++ b/plugins/java/jmx2munin/contrib/jmx2munin.cfg/cassandra/nodes_in_cluster @@ -0,0 +1,3 @@ +graph_title Number of Nodes in Cluster +graph_vlabel org_apache_cassandra_db_storageservice_livenodes_size +org_apache_cassandra_db_storageservice_livenodes_size.label number of nodes diff --git a/plugins/java/jmx2munin/contrib/jmx2munin.sh b/plugins/java/jmx2munin/contrib/jmx2munin.sh new file mode 100644 index 00000000..2ccb1841 --- /dev/null +++ b/plugins/java/jmx2munin/contrib/jmx2munin.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# [cassandra_nodes_in_cluster] +# env.config cassandra/nodes_in_cluster +# env.query org.apache.cassandra.*:* + +if [ -z "$MUNIN_LIBDIR" ]; then + MUNIN_LIBDIR="`dirname $(dirname "$0")`" +fi + +if [ -f "$MUNIN_LIBDIR/plugins/plugin.sh" ]; then + . $MUNIN_LIBDIR/plugins/plugin.sh +fi + +if [ "$1" = "autoconf" ]; then + echo yes + exit 0 +fi + +if [ -z "$url" ]; then + # this is very common so make it a default + url="service:jmx:rmi:///jndi/rmi://127.0.0.1:8080/jmxrmi" +fi + +if [ -z "$config" -o -z "$query" -o -z "$url" ]; then + echo "Configuration needs attributes config, query and optinally url" + exit 1 +fi + +JMX2MUNIN_DIR="$MUNIN_LIBDIR/plugins" +CONFIG="$JMX2MUNIN_DIR/jmx2munin.cfg/$config" + +if [ "$1" = "config" ]; then + cat "$CONFIG" + exit 0 +fi + +JAR="$JMX2MUNIN_DIR/jmx2munin.jar" +CACHED="/tmp/jmx2munin" + +if test ! -f $CACHED || test `find "$CACHED" -mmin +2`; then + + java -jar "$JAR" \ + -url "$url" \ + -query "$query" \ + $ATTRIBUTES \ + > $CACHED + + echo "cached.value `date +%s`" >> $CACHED +fi + +ATTRIBUTES=`awk '/\.label/ { gsub(/\.label/,""); print $1 }' $CONFIG` + +for ATTRIBUTE in $ATTRIBUTES; do + grep $ATTRIBUTE $CACHED +done \ No newline at end of file diff --git a/plugins/java/jmx2munin/pom.xml b/plugins/java/jmx2munin/pom.xml new file mode 100644 index 00000000..2bbbd026 --- /dev/null +++ b/plugins/java/jmx2munin/pom.xml @@ -0,0 +1,121 @@ + + + 4.0.0 + + org.vafer + jmx2munin + jmx2munin + 1.0 + + Munin plugin to access JMX information + + http://github.com/tcurdt/jmx2munin + + + + tcurdt + Torsten Curdt + tcurdt at vafer.org + +1 + + + + + + Apache License 2 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + scm:git:git://github.com:tcurdt/jmx2munin.git + scm:git:git://github.com:tcurdt/jmx2munin.git + http://github.com/tcurdt/jmx2munin/tree/master + + + + + com.beust + jcommander + 1.17 + + + + junit + junit + 4.5 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + UTF-8 + + + + org.apache.maven.plugins + maven-surefire-plugin + + never + + **/*TestCase.java + + + **/Abstract* + + true + false + + + + org.apache.maven.plugins + maven-source-plugin + 2.1 + + true + + + + create-source-jar + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-shade-plugin + 1.4 + + + package + + shade + + + false + + + com.beust:jcommander + + + + + org.vafer.jmx.munin.Munin + + + + + + + + + diff --git a/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Enums.java b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Enums.java new file mode 100644 index 00000000..ab5b4831 --- /dev/null +++ b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Enums.java @@ -0,0 +1,77 @@ +package org.vafer.jmx; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Pattern; + +import javax.management.ObjectName; + +public final class Enums { + + private TreeMap> sections = new TreeMap>(); + + public boolean load(String filePath) throws IOException { + BufferedReader input = null; + LinkedHashMap section = new LinkedHashMap(); + try { + input = new BufferedReader(new InputStreamReader(new FileInputStream(filePath))); + String line; + int linenr = 0; + while((line = input.readLine()) != null) { + linenr += 1; + line = line.trim(); + if (line.startsWith("#")) { + continue; + } + if (line.startsWith("[") && line.endsWith("]")) { + // new section + String id = line.substring(1, line.length() - 1); + section = new LinkedHashMap(); + sections.put(id, section); + } else { + String[] pair = line.split("="); + if (pair.length == 2) { + Integer number = Integer.parseInt(pair[0].trim()); + Pattern pattern = Pattern.compile(pair[1].trim()); + if (section.put(number, pattern) != null) { + System.err.println("Line " + linenr + ": previous definitions of " + number); + } + } + } + } + } finally { + if (input != null) { + input.close(); + } + } + return false; + } + + public static String id(ObjectName beanName, String attributeName) { + StringBuilder sb = new StringBuilder(); + sb.append(beanName.getDomain()); + sb.append('.'); + sb.append(beanName.getKeyProperty("type")); + sb.append(':'); + sb.append(attributeName); + return sb.toString(); + } + + public Number resolve(String id, String value) { + LinkedHashMap section = sections.get(id); + if (section == null) { + return null; + } + for(Map.Entry entry : section.entrySet()) { + if (entry.getValue().matcher(value).matches()) { + return entry.getKey(); + } + } + return null; + } +} diff --git a/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Filter.java b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Filter.java new file mode 100644 index 00000000..e7d67a8a --- /dev/null +++ b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Filter.java @@ -0,0 +1,9 @@ +package org.vafer.jmx; + +import javax.management.ObjectName; + +public interface Filter { + + public boolean include(ObjectName bean, String attribute); + +} diff --git a/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/ListOutput.java b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/ListOutput.java new file mode 100644 index 00000000..4e050faf --- /dev/null +++ b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/ListOutput.java @@ -0,0 +1,26 @@ +package org.vafer.jmx; + +import java.util.HashSet; +import java.util.Set; + +import javax.management.ObjectName; + +public final class ListOutput implements Output { + + private final Set seen = new HashSet(); + + public void output(ObjectName beanName, String attributeName, Object value) { + Value.flatten(beanName, attributeName, value, new Value.Listener() { + public void value(ObjectName beanName, String attributeName, String value) { + final String id = Enums.id(beanName, attributeName); + if (!seen.contains(id)) { + System.out.println("[" + id + "]"); + seen.add(id); + } + } + public void value(ObjectName beanName, String attributeName, Number value) { + } + }); + } + +} diff --git a/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/NoFilter.java b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/NoFilter.java new file mode 100644 index 00000000..6188d5c7 --- /dev/null +++ b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/NoFilter.java @@ -0,0 +1,10 @@ +package org.vafer.jmx; + +import javax.management.ObjectName; + +public final class NoFilter implements Filter { + + public boolean include(ObjectName bean, String attribute) { + return true; + } +} diff --git a/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Output.java b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Output.java new file mode 100644 index 00000000..eb9e6ca2 --- /dev/null +++ b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Output.java @@ -0,0 +1,9 @@ +package org.vafer.jmx; + +import javax.management.ObjectName; + +public interface Output { + + public void output(ObjectName beanName, String attributeName, Object value); + +} diff --git a/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Query.java b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Query.java new file mode 100644 index 00000000..e27bc4f5 --- /dev/null +++ b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Query.java @@ -0,0 +1,52 @@ +package org.vafer.jmx; + +import java.io.IOException; +import java.util.Collection; + +import javax.management.AttributeNotFoundException; +import javax.management.InstanceNotFoundException; +import javax.management.IntrospectionException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanServerConnection; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import javax.management.ReflectionException; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + +public final class Query { + + public void run(String url, String expression, Filter filter, Output output) throws IOException, MalformedObjectNameException, InstanceNotFoundException, ReflectionException, IntrospectionException, AttributeNotFoundException, MBeanException { + JMXConnector connector = JMXConnectorFactory.connect(new JMXServiceURL(url)); + MBeanServerConnection connection = connector.getMBeanServerConnection(); + final Collection mbeans = connection.queryMBeans(new ObjectName(expression), null); + + for(ObjectInstance mbean : mbeans) { + final ObjectName mbeanName = mbean.getObjectName(); + final MBeanInfo mbeanInfo = connection.getMBeanInfo(mbeanName); + final MBeanAttributeInfo[] attributes = mbeanInfo.getAttributes(); + for (final MBeanAttributeInfo attribute : attributes) { + if (attribute.isReadable()) { + if (filter.include(mbeanName, attribute.getName())) { + final String attributeName = attribute.getName(); + try { + output.output( + mbean.getObjectName(), + attributeName, + connection.getAttribute(mbeanName, attributeName) + ); + } catch(Exception e) { + // System.err.println("Failed to read " + mbeanName + "." + attributeName); + } + } + } + } + + } + connector.close(); + } +} diff --git a/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Value.java b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Value.java new file mode 100644 index 00000000..87af5f8a --- /dev/null +++ b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/Value.java @@ -0,0 +1,52 @@ +package org.vafer.jmx; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.management.ObjectName; + +public final class Value { + + public interface Listener { + public void value(ObjectName beanName, String attributeName, String value); + public void value(ObjectName beanName, String attributeName, Number value); + } + + public static void flatten(ObjectName beanName, String attributeName, Object value, Listener listener) { + if (value instanceof Number) { + + listener.value(beanName, attributeName, (Number) value); + + } else if (value instanceof String) { + + listener.value(beanName, attributeName, (String) value); + + } else if (value instanceof Set) { + + final Set set = (Set) value; + flatten(beanName, attributeName + ".size", set.size(), listener); + for(Object entry : set) { + flatten(beanName, attributeName + "[" + entry + "]", 1, listener); + } + + } else if (value instanceof List) { + + final List list = (List)value; + listener.value(beanName, attributeName + ".size", list.size()); + for(int i = 0; i map = (Map) value; + listener.value(beanName, attributeName + ".size", map.size()); + for(Map.Entry entry : map.entrySet()) { + flatten(beanName, attributeName + "[" + entry.getKey() + "]", entry.getValue(), listener); + } + + } else { + // System.err.println("Failed to convert " + beanName + "." + attributeName); + } + } +} diff --git a/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/munin/Munin.java b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/munin/Munin.java new file mode 100644 index 00000000..9f1ffdc7 --- /dev/null +++ b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/munin/Munin.java @@ -0,0 +1,67 @@ +package org.vafer.jmx.munin; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.vafer.jmx.Enums; +import org.vafer.jmx.Filter; +import org.vafer.jmx.ListOutput; +import org.vafer.jmx.NoFilter; +import org.vafer.jmx.Query; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; + +public final class Munin { + + @Parameter(description = "") + private List args = new ArrayList(); + + @Parameter(names = "-url", description = "jmx url", required = true) + private String url; + + @Parameter(names = "-query", description = "query expression", required = true) + private String query; + + @Parameter(names = "-enums", description = "file string to enum config") + private String enumsPath; + + @Parameter(names = "-attribute", description = "attributes to return") + private List attributes = new ArrayList(); + + private void run() throws Exception { + final Filter filter; + if (attributes == null || attributes.isEmpty()) { + filter = new NoFilter(); + } else { + filter = new MuninAttributesFilter(attributes); + } + + final Enums enums = new Enums(); + if (enumsPath != null) { + enums.load(enumsPath); + } + + final String cmd = args.toString().toLowerCase(Locale.US); + if ("[list]".equals(cmd)) { + new Query().run(url, query, filter, new ListOutput()); + } else { + new Query().run(url, query, filter, new MuninOutput(enums)); + } + } + + public static void main(String[] args) throws Exception { + Munin m = new Munin(); + + JCommander cli = new JCommander(m); + try { + cli.parse(args); + } catch(Exception e) { + cli.usage(); + System.exit(1); + } + + m.run(); + } +} diff --git a/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/munin/MuninAttributesFilter.java b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/munin/MuninAttributesFilter.java new file mode 100644 index 00000000..e1a49e83 --- /dev/null +++ b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/munin/MuninAttributesFilter.java @@ -0,0 +1,24 @@ +package org.vafer.jmx.munin; + +import java.util.HashSet; +import java.util.List; + +import javax.management.ObjectName; + +import org.vafer.jmx.Filter; + +public final class MuninAttributesFilter implements Filter { + + private final HashSet attributes = new HashSet(); + + public MuninAttributesFilter(List pAttributes) { + for (String attribute : pAttributes) { + attributes.add(attribute.trim().replaceAll("_size$", "")); + } + } + + public boolean include(ObjectName bean, String attribute) { + return attributes.contains(MuninOutput.attributeName(bean, attribute)); + } + +} diff --git a/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/munin/MuninOutput.java b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/munin/MuninOutput.java new file mode 100644 index 00000000..9fb50b12 --- /dev/null +++ b/plugins/java/jmx2munin/src/main/java/org/vafer/jmx/munin/MuninOutput.java @@ -0,0 +1,93 @@ +package org.vafer.jmx.munin; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Hashtable; +import java.util.Locale; + +import javax.management.ObjectName; + +import org.vafer.jmx.Enums; +import org.vafer.jmx.Output; +import org.vafer.jmx.Value; + +public final class MuninOutput implements Output { + + private final Enums enums; + + public MuninOutput(Enums enums) { + this.enums = enums; + } + + public static String attributeName(ObjectName bean, String attribute) { + StringBuilder sb = new StringBuilder(); + sb.append(fieldname(beanString(bean))); + sb.append('_'); + sb.append(fieldname(attribute)); + return sb.toString().toLowerCase(Locale.US); + } + + private static String fieldname(String s) { + return s.replaceAll("[^A-Za-z0-9]", "_"); + } + + private static String beanString(ObjectName beanName) { + StringBuilder sb = new StringBuilder(); + sb.append(beanName.getDomain()); + + Hashtable properties = beanName.getKeyPropertyList(); + + String keyspace = "keyspace"; + if (properties.containsKey(keyspace)) { + sb.append('.'); + sb.append(properties.get(keyspace)); + properties.remove(keyspace); + } + + String type = "type"; + if (properties.containsKey(type)) { + sb.append('.'); + sb.append(properties.get(type)); + properties.remove(type); + } + + ArrayList keys = new ArrayList(properties.keySet()); + Collections.sort(keys); + + for(String key : keys) { + sb.append('.'); + sb.append(properties.get(key)); + } + + return sb.toString(); + // return beanName.getCanonicalName(); + } + + public void output(ObjectName beanName, String attributeName, Object value) { + Value.flatten(beanName, attributeName, value, new Value.Listener() { + public void value(ObjectName beanName, String attributeName, String value) { + final Number v = enums.resolve(Enums.id(beanName, attributeName), value); + if (v != null) { + value(beanName, attributeName, v); + } else { + value(beanName, attributeName, Double.NaN); + } + } + public void value(ObjectName beanName, String attributeName, Number value) { + final String v; + + if (Double.isNaN(value.doubleValue())) { + v = "U"; + } else { + final NumberFormat f = NumberFormat.getInstance(); + f.setMaximumFractionDigits(2); + f.setGroupingUsed(false); + v = f.format(value); + } + + System.out.println(attributeName(beanName, attributeName) + ".value " + v); + } + }); + } +} \ No newline at end of file diff --git a/plugins/jvm/jstat__gccount b/plugins/java/jstat__gccount similarity index 100% rename from plugins/jvm/jstat__gccount rename to plugins/java/jstat__gccount diff --git a/plugins/jvm/jstat__gctime b/plugins/java/jstat__gctime similarity index 100% rename from plugins/jvm/jstat__gctime rename to plugins/java/jstat__gctime diff --git a/plugins/jvm/jstat__heap b/plugins/java/jstat__heap similarity index 100% rename from plugins/jvm/jstat__heap rename to plugins/java/jstat__heap diff --git a/plugins/jvm/jvm_sun_memory b/plugins/java/jvm_sun_memory similarity index 100% rename from plugins/jvm/jvm_sun_memory rename to plugins/java/jvm_sun_memory diff --git a/plugins/jvm/jvm_sun_minorgcs b/plugins/java/jvm_sun_minorgcs similarity index 100% rename from plugins/jvm/jvm_sun_minorgcs rename to plugins/java/jvm_sun_minorgcs diff --git a/plugins/jvm/jvm_sun_tenuredgcs b/plugins/java/jvm_sun_tenuredgcs similarity index 100% rename from plugins/jvm/jvm_sun_tenuredgcs rename to plugins/java/jvm_sun_tenuredgcs diff --git a/plugins/other/jmx b/plugins/other/jmx deleted file mode 100755 index 0f4820b2..00000000 Binary files a/plugins/other/jmx and /dev/null differ diff --git a/plugins/other/jmx2munin b/plugins/other/jmx2munin deleted file mode 100755 index ccae5568..00000000 Binary files a/plugins/other/jmx2munin and /dev/null differ