diff --git a/CHANGES.txt b/CHANGES.txt
index bacc371582..83fdd65753 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -38,6 +38,9 @@ Trunk (unreleased changes)
HADOOP-6832. Add an authentication plugin using a configurable static user
for the web UI. (Owen O'Malley and Todd Lipcon via cdouglas)
+ HADOOP-7144. Expose JMX metrics via JSON servlet. (Robert Joseph Evans via
+ cdouglas)
+
IMPROVEMENTS
HADOOP-7042. Updates to test-patch.sh to include failed test names and
diff --git a/src/java/org/apache/hadoop/http/HttpServer.java b/src/java/org/apache/hadoop/http/HttpServer.java
index e482a1faab..75b3bbac34 100644
--- a/src/java/org/apache/hadoop/http/HttpServer.java
+++ b/src/java/org/apache/hadoop/http/HttpServer.java
@@ -52,6 +52,7 @@
import org.apache.hadoop.http.FilterContainer;
import org.apache.hadoop.http.FilterInitializer;
import org.apache.hadoop.http.HtmlQuoting;
+import org.apache.hadoop.jmx.JMXJsonServlet;
import org.apache.hadoop.log.LogLevel;
import org.apache.hadoop.metrics.MetricsServlet;
import org.apache.hadoop.security.Krb5AndCertsSslSocketConnector;
@@ -287,6 +288,7 @@ protected void addDefaultServlets() {
addServlet("stacks", "/stacks", StackServlet.class);
addServlet("logLevel", "/logLevel", LogLevel.Servlet.class);
addServlet("metrics", "/metrics", MetricsServlet.class);
+ addServlet("jmx", "/jmx", JMXJsonServlet.class);
addServlet("conf", "/conf", ConfServlet.class);
}
diff --git a/src/java/org/apache/hadoop/jmx/JMXJsonServlet.java b/src/java/org/apache/hadoop/jmx/JMXJsonServlet.java
new file mode 100644
index 0000000000..be59f5a301
--- /dev/null
+++ b/src/java/org/apache/hadoop/jmx/JMXJsonServlet.java
@@ -0,0 +1,327 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hadoop.jmx;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.Array;
+import java.util.Iterator;
+import java.util.Set;
+
+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.MBeanServer;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+import javax.management.ReflectionException;
+import javax.management.openmbean.CompositeData;
+import javax.management.openmbean.CompositeType;
+import javax.management.openmbean.TabularData;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.http.HttpServer;
+import org.codehaus.jackson.JsonFactory;
+import org.codehaus.jackson.JsonGenerator;
+
+/*
+ * This servlet is based off of the JMXProxyServlet from Tomcat 7.0.14. It has
+ * been rewritten to be read only and to output in a JSON format so it is not
+ * really that close to the original.
+ */
+/**
+ * Provides Read only web access to JMX.
+ *
+ * This servlet generally will be placed under the /jmx URL for each
+ * HttpServer. It provides read only
+ * access to JMX metrics. The optional qry
parameter
+ * may be used to query only a subset of the JMX Beans. This query
+ * functionality is provided through the
+ * {@link MBeanServer#queryNames(ObjectName, javax.management.QueryExp)}
+ * method.
+ *
+ * For example http://.../jmx?qry=Hadoop:*
will return
+ * all hadoop metrics exposed through JMX.
+ *
+ * If the qry
parameter is not formatted correctly then a
+ * 400 BAD REQUEST http response code will be returned.
+ *
+ * The return format is JSON and in the form
+ *
+ *
+ * {
+ * "beans" : [
+ * {
+ * "name":"bean-name"
+ * ...
+ * }
+ * ]
+ * }
+ *
+ *
+ * The servlet attempts to convert the the JMXBeans into JSON. Each
+ * bean's attributes will be converted to a JSON object member.
+ *
+ * If the attribute is a boolean, a number, a string, or an array
+ * it will be converted to the JSON equivalent.
+ *
+ * If the value is a {@link CompositeData} then it will be converted
+ * to a JSON object with the keys as the name of the JSON member and
+ * the value is converted following these same rules.
+ *
+ * If the value is a {@link TabularData} then it will be converted
+ * to an array of the {@link CompositeData} elements that it contains.
+ *
+ * All other objects will be converted to a string and output as such.
+ *
+ * The bean's name and modelerType will be returned for all beans.
+ */
+public class JMXJsonServlet extends HttpServlet {
+ private static final Log LOG = LogFactory.getLog(JMXJsonServlet.class);
+
+ private static final long serialVersionUID = 1L;
+
+ // ----------------------------------------------------- Instance Variables
+ /**
+ * MBean server.
+ */
+ protected transient MBeanServer mBeanServer = null;
+
+ // --------------------------------------------------------- Public Methods
+ /**
+ * Initialize this servlet.
+ */
+ @Override
+ public void init() throws ServletException {
+ // Retrieve the MBean server
+ mBeanServer = ManagementFactory.getPlatformMBeanServer();
+ }
+
+ /**
+ * Process a GET request for the specified resource.
+ *
+ * @param request
+ * The servlet request we are processing
+ * @param response
+ * The servlet response we are creating
+ */
+ @Override
+ public void doGet(HttpServletRequest request, HttpServletResponse response) {
+ try {
+ // Do the authorization
+ if (!HttpServer.hasAdministratorAccess(getServletContext(), request,
+ response)) {
+ return;
+ }
+
+ response.setContentType("application/json; charset=utf8");
+
+ PrintWriter writer = response.getWriter();
+
+ JsonFactory jsonFactory = new JsonFactory();
+ JsonGenerator jg = jsonFactory.createJsonGenerator(writer);
+ jg.useDefaultPrettyPrinter();
+ jg.writeStartObject();
+ if (mBeanServer == null) {
+ jg.writeStringField("result", "ERROR");
+ jg.writeStringField("message", "No MBeanServer could be found");
+ jg.close();
+ return;
+ }
+ String qry = request.getParameter("qry");
+ if (qry == null) {
+ qry = "*:*";
+ }
+ listBeans(jg, new ObjectName(qry));
+ jg.close();
+ } catch (IOException e) {
+ LOG.error("Caught an exception while processing JMX request", e);
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ } catch (MalformedObjectNameException e) {
+ LOG.error("Caught an exception while processing JMX request", e);
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ }
+ }
+
+ // --------------------------------------------------------- Private Methods
+ private void listBeans(JsonGenerator jg, ObjectName qry) throws IOException {
+ LOG.debug("Listing beans for "+qry);
+ Set names = null;
+ names = mBeanServer.queryNames(qry, null);
+
+ jg.writeArrayFieldStart("beans");
+ Iterator it = names.iterator();
+ while (it.hasNext()) {
+ ObjectName oname = it.next();
+ MBeanInfo minfo;
+ String code;
+ try {
+ minfo = mBeanServer.getMBeanInfo(oname);
+ code = minfo.getClassName();
+ try {
+ if ("org.apache.commons.modeler.BaseModelMBean".equals(code)) {
+ code = (String) mBeanServer.getAttribute(oname, "modelerType");
+ }
+ } catch (AttributeNotFoundException e) {
+ //Ignored the modelerType attribute was not found, so use the class name instead.
+ } catch (MBeanException e) {
+ //The code inside the attribute getter threw an exception so log it, and
+ // fall back on the class name
+ LOG.error("getting attribute modelerType of "+oname+" threw an exception", e);
+ } catch (RuntimeException e) {
+ //For some reason even with an MBeanException available to them Runtime exceptions
+ //can still find their way through, so treat them the same as MBeanException
+ LOG.error("getting attribute modelerType of "+oname+" threw an exception", e);
+ } catch (ReflectionException e) {
+ //This happens when the code inside the JMX bean (setter?? from the java docs)
+ //threw an exception, so log it and fall back on the class name
+ LOG.error("getting attribute modelerType of "+oname+" threw an exception", e);
+ }
+ } catch (InstanceNotFoundException e) {
+ //Ignored for some reason the bean was not found so don't output it
+ continue;
+ } catch (IntrospectionException e) {
+ //This is an internal error, something odd happened with reflection so log it and
+ //don't output the bean.
+ LOG.error("Problem while trying to process JMX query: "+qry+" with MBean "+oname, e);
+ continue;
+ } catch (ReflectionException e) {
+ //This happens when the code inside the JMX bean threw an exception, so log it and
+ //don't output the bean.
+ LOG.error("Problem while trying to process JMX query: "+qry+" with MBean "+oname, e);
+ continue;
+ }
+
+ jg.writeStartObject();
+ jg.writeStringField("name", oname.toString());
+ // can't be null - I think
+
+ jg.writeStringField("modelerType", code);
+
+ MBeanAttributeInfo attrs[] = minfo.getAttributes();
+ for (int i = 0; i < attrs.length; i++) {
+ writeAttribute(jg, oname, attrs[i]);
+ }
+ // LOG.error("Caught Error writing value ",t);
+ // ExceptionUtils.handleThrowable(t);
+ //}
+ jg.writeEndObject();
+ }
+ jg.writeEndArray();
+ }
+
+ private void writeAttribute(JsonGenerator jg, ObjectName oname, MBeanAttributeInfo attr) throws IOException {
+ if (!attr.isReadable()) {
+ return;
+ }
+ String attName = attr.getName();
+ if ("modelerType".equals(attName)) {
+ return;
+ }
+ if (attName.indexOf("=") >= 0 || attName.indexOf(":") >= 0
+ || attName.indexOf(" ") >= 0) {
+ return;
+ }
+ Object value = null;
+ try {
+ value = mBeanServer.getAttribute(oname, attName);
+ } catch (AttributeNotFoundException e) {
+ //Ignored the attribute was not found, which should never happen because the bean
+ //just told us that it has this attribute, but if this happens just don't output
+ //the attribute.
+ return;
+ } catch (MBeanException e) {
+ //The code inside the attribute getter threw an exception so log it, and
+ // skip outputting the attribute
+ LOG.error("getting attribute "+attName+" of "+oname+" threw an exception", e);
+ return;
+ } catch (RuntimeException e) {
+ //For some reason even with an MBeanException available to them Runtime exceptions
+ //can still find their way through, so treat them the same as MBeanException
+ LOG.error("getting attribute "+attName+" of "+oname+" threw an exception", e);
+ return;
+ } catch (ReflectionException e) {
+ //This happens when the code inside the JMX bean (setter?? from the java docs)
+ //threw an exception, so log it and skip outputting the attribute
+ LOG.error("getting attribute "+attName+" of "+oname+" threw an exception", e);
+ return;
+ } catch (InstanceNotFoundException e) {
+ //Ignored the mbean itself was not found, which should never happen because we
+ //just accessed it (perhaps something unregistered in-between) but if this
+ //happens just don't output the attribute.
+ return;
+ }
+
+ writeAttribute(jg, attName, value);
+ }
+
+ private void writeAttribute(JsonGenerator jg, String attName, Object value) throws IOException {
+ jg.writeFieldName(attName);
+ writeObject(jg, value);
+ }
+
+ private void writeObject(JsonGenerator jg, Object value) throws IOException {
+ if(value == null) {
+ jg.writeNull();
+ } else {
+ Class> c = value.getClass();
+ if (c.isArray()) {
+ jg.writeStartArray();
+ int len = Array.getLength(value);
+ for (int j = 0; j < len; j++) {
+ Object item = Array.get(value, j);
+ writeObject(jg, item);
+ }
+ jg.writeEndArray();
+ } else if(value instanceof Number) {
+ Number n = (Number)value;
+ jg.writeNumber(n.toString());
+ } else if(value instanceof Boolean) {
+ Boolean b = (Boolean)value;
+ jg.writeBoolean(b);
+ } else if(value instanceof CompositeData) {
+ CompositeData cds = (CompositeData)value;
+ CompositeType comp = cds.getCompositeType();
+ Set keys = comp.keySet();
+ jg.writeStartObject();
+ for(String key: keys) {
+ writeAttribute(jg, key, cds.get(key));
+ }
+ jg.writeEndObject();
+ } else if(value instanceof TabularData) {
+ TabularData tds = (TabularData)value;
+ jg.writeStartArray();
+ for(Object entry : tds.values()) {
+ writeObject(jg, entry);
+ }
+ jg.writeEndArray();
+ } else {
+ jg.writeString(value.toString());
+ }
+ }
+ }
+}
diff --git a/src/java/org/apache/hadoop/jmx/package-info.java b/src/java/org/apache/hadoop/jmx/package-info.java
new file mode 100644
index 0000000000..e09d9938cd
--- /dev/null
+++ b/src/java/org/apache/hadoop/jmx/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/**
+ * This package provides access to JMX primarily through the
+ * {@link org.apache.hadoop.jmx.JMXJsonServlet} class.
+ */
+package org.apache.hadoop.jmx;
\ No newline at end of file
diff --git a/src/test/core/org/apache/hadoop/jmx/TestJMXJsonServlet.java b/src/test/core/org/apache/hadoop/jmx/TestJMXJsonServlet.java
new file mode 100644
index 0000000000..b1feaf3ae8
--- /dev/null
+++ b/src/test/core/org/apache/hadoop/jmx/TestJMXJsonServlet.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hadoop.jmx;
+
+
+import java.net.URL;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.http.HttpServer;
+import org.apache.hadoop.http.HttpServerFunctionalTest;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestJMXJsonServlet extends HttpServerFunctionalTest {
+ private static final Log LOG = LogFactory.getLog(TestJMXJsonServlet.class);
+ private static HttpServer server;
+ private static URL baseUrl;
+
+ @BeforeClass public static void setup() throws Exception {
+ server = createTestServer();
+ server.start();
+ baseUrl = getServerURL(server);
+ }
+
+ @AfterClass public static void cleanup() throws Exception {
+ server.stop();
+ }
+
+ public static void assertReFind(String re, String value) {
+ Pattern p = Pattern.compile(re);
+ Matcher m = p.matcher(value);
+ assertTrue("'"+p+"' does not match "+value, m.find());
+ }
+
+ @Test public void testQury() throws Exception {
+ String result = readOutput(new URL(baseUrl, "/jmx?qry=java.lang:type=Runtime"));
+ LOG.info("/jmx?qry=java.lang:type=Runtime RESULT: "+result);
+ assertReFind("\"name\"\\s*:\\s*\"java.lang:type=Runtime\"", result);
+ assertReFind("\"modelerType\"", result);
+
+ result = readOutput(new URL(baseUrl, "/jmx?qry=java.lang:type=Memory"));
+ LOG.info("/jmx?qry=java.lang:type=Memory RESULT: "+result);
+ assertReFind("\"name\"\\s*:\\s*\"java.lang:type=Memory\"", result);
+ assertReFind("\"modelerType\"", result);
+
+ result = readOutput(new URL(baseUrl, "/jmx"));
+ LOG.info("/jmx RESULT: "+result);
+ assertReFind("\"name\"\\s*:\\s*\"java.lang:type=Memory\"", result);
+ }
+}