001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.hadoop.jmx; 019 020import org.apache.commons.logging.Log; 021import org.apache.commons.logging.LogFactory; 022import org.apache.hadoop.http.HttpServer2; 023import org.codehaus.jackson.JsonFactory; 024import org.codehaus.jackson.JsonGenerator; 025 026import javax.management.AttributeNotFoundException; 027import javax.management.InstanceNotFoundException; 028import javax.management.IntrospectionException; 029import javax.management.MBeanAttributeInfo; 030import javax.management.MBeanException; 031import javax.management.MBeanInfo; 032import javax.management.MBeanServer; 033import javax.management.MalformedObjectNameException; 034import javax.management.ObjectName; 035import javax.management.ReflectionException; 036import javax.management.RuntimeErrorException; 037import javax.management.RuntimeMBeanException; 038import javax.management.openmbean.CompositeData; 039import javax.management.openmbean.CompositeType; 040import javax.management.openmbean.TabularData; 041import javax.servlet.ServletException; 042import javax.servlet.http.HttpServlet; 043import javax.servlet.http.HttpServletRequest; 044import javax.servlet.http.HttpServletResponse; 045import java.io.IOException; 046import java.io.PrintWriter; 047import java.lang.management.ManagementFactory; 048import java.lang.reflect.Array; 049import java.util.Iterator; 050import java.util.Set; 051 052/* 053 * This servlet is based off of the JMXProxyServlet from Tomcat 7.0.14. It has 054 * been rewritten to be read only and to output in a JSON format so it is not 055 * really that close to the original. 056 */ 057/** 058 * Provides Read only web access to JMX. 059 * <p> 060 * This servlet generally will be placed under the /jmx URL for each 061 * HttpServer. It provides read only 062 * access to JMX metrics. The optional <code>qry</code> parameter 063 * may be used to query only a subset of the JMX Beans. This query 064 * functionality is provided through the 065 * {@link MBeanServer#queryNames(ObjectName, javax.management.QueryExp)} 066 * method. 067 * <p> 068 * For example <code>http://.../jmx?qry=Hadoop:*</code> will return 069 * all hadoop metrics exposed through JMX. 070 * <p> 071 * The optional <code>get</code> parameter is used to query an specific 072 * attribute of a JMX bean. The format of the URL is 073 * <code>http://.../jmx?get=MXBeanName::AttributeName<code> 074 * <p> 075 * For example 076 * <code> 077 * http://../jmx?get=Hadoop:service=NameNode,name=NameNodeInfo::ClusterId 078 * </code> will return the cluster id of the namenode mxbean. 079 * <p> 080 * If the <code>qry</code> or the <code>get</code> parameter is not formatted 081 * correctly then a 400 BAD REQUEST http response code will be returned. 082 * <p> 083 * If a resouce such as a mbean or attribute can not be found, 084 * a 404 SC_NOT_FOUND http response code will be returned. 085 * <p> 086 * The return format is JSON and in the form 087 * <p> 088 * <code><pre> 089 * { 090 * "beans" : [ 091 * { 092 * "name":"bean-name" 093 * ... 094 * } 095 * ] 096 * } 097 * </pre></code> 098 * <p> 099 * The servlet attempts to convert the the JMXBeans into JSON. Each 100 * bean's attributes will be converted to a JSON object member. 101 * 102 * If the attribute is a boolean, a number, a string, or an array 103 * it will be converted to the JSON equivalent. 104 * 105 * If the value is a {@link CompositeData} then it will be converted 106 * to a JSON object with the keys as the name of the JSON member and 107 * the value is converted following these same rules. 108 * 109 * If the value is a {@link TabularData} then it will be converted 110 * to an array of the {@link CompositeData} elements that it contains. 111 * 112 * All other objects will be converted to a string and output as such. 113 * 114 * The bean's name and modelerType will be returned for all beans. 115 * 116 */ 117public class JMXJsonServlet extends HttpServlet { 118 private static final Log LOG = LogFactory.getLog(JMXJsonServlet.class); 119 static final String ACCESS_CONTROL_ALLOW_METHODS = 120 "Access-Control-Allow-Methods"; 121 static final String ACCESS_CONTROL_ALLOW_ORIGIN = 122 "Access-Control-Allow-Origin"; 123 124 private static final long serialVersionUID = 1L; 125 126 /** 127 * MBean server. 128 */ 129 protected transient MBeanServer mBeanServer = null; 130 131 // --------------------------------------------------------- Public Methods 132 /** 133 * Initialize this servlet. 134 */ 135 @Override 136 public void init() throws ServletException { 137 // Retrieve the MBean server 138 mBeanServer = ManagementFactory.getPlatformMBeanServer(); 139 } 140 141 protected boolean isInstrumentationAccessAllowed(HttpServletRequest request, 142 HttpServletResponse response) throws IOException { 143 return HttpServer2.isInstrumentationAccessAllowed(getServletContext(), 144 request, response); 145 } 146 147 /** 148 * Process a GET request for the specified resource. 149 * 150 * @param request 151 * The servlet request we are processing 152 * @param response 153 * The servlet response we are creating 154 */ 155 @Override 156 public void doGet(HttpServletRequest request, HttpServletResponse response) { 157 String jsonpcb = null; 158 PrintWriter writer = null; 159 try { 160 if (!isInstrumentationAccessAllowed(request, response)) { 161 return; 162 } 163 164 JsonGenerator jg = null; 165 try { 166 writer = response.getWriter(); 167 168 response.setContentType("application/json; charset=utf8"); 169 response.setHeader(ACCESS_CONTROL_ALLOW_METHODS, "GET"); 170 response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*"); 171 172 JsonFactory jsonFactory = new JsonFactory(); 173 jg = jsonFactory.createJsonGenerator(writer); 174 jg.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); 175 jg.useDefaultPrettyPrinter(); 176 jg.writeStartObject(); 177 178 if (mBeanServer == null) { 179 jg.writeStringField("result", "ERROR"); 180 jg.writeStringField("message", "No MBeanServer could be found"); 181 jg.close(); 182 LOG.error("No MBeanServer could be found."); 183 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 184 return; 185 } 186 187 // query per mbean attribute 188 String getmethod = request.getParameter("get"); 189 if (getmethod != null) { 190 String[] splitStrings = getmethod.split("\\:\\:"); 191 if (splitStrings.length != 2) { 192 jg.writeStringField("result", "ERROR"); 193 jg.writeStringField("message", "query format is not as expected."); 194 jg.close(); 195 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 196 return; 197 } 198 listBeans(jg, new ObjectName(splitStrings[0]), splitStrings[1], 199 response); 200 jg.close(); 201 return; 202 203 } 204 205 // query per mbean 206 String qry = request.getParameter("qry"); 207 if (qry == null) { 208 qry = "*:*"; 209 } 210 listBeans(jg, new ObjectName(qry), null, response); 211 } finally { 212 if (jg != null) { 213 jg.close(); 214 } 215 if (writer != null) { 216 writer.close(); 217 } 218 } 219 } catch ( IOException e ) { 220 LOG.error("Caught an exception while processing JMX request", e); 221 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 222 } catch ( MalformedObjectNameException e ) { 223 LOG.error("Caught an exception while processing JMX request", e); 224 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 225 } finally { 226 if (writer != null) { 227 writer.close(); 228 } 229 } 230 } 231 232 // --------------------------------------------------------- Private Methods 233 private void listBeans(JsonGenerator jg, ObjectName qry, String attribute, 234 HttpServletResponse response) 235 throws IOException { 236 LOG.debug("Listing beans for "+qry); 237 Set<ObjectName> names = null; 238 names = mBeanServer.queryNames(qry, null); 239 240 jg.writeArrayFieldStart("beans"); 241 Iterator<ObjectName> it = names.iterator(); 242 while (it.hasNext()) { 243 ObjectName oname = it.next(); 244 MBeanInfo minfo; 245 String code = ""; 246 Object attributeinfo = null; 247 try { 248 minfo = mBeanServer.getMBeanInfo(oname); 249 code = minfo.getClassName(); 250 String prs = ""; 251 try { 252 if ("org.apache.commons.modeler.BaseModelMBean".equals(code)) { 253 prs = "modelerType"; 254 code = (String) mBeanServer.getAttribute(oname, prs); 255 } 256 if (attribute!=null) { 257 prs = attribute; 258 attributeinfo = mBeanServer.getAttribute(oname, prs); 259 } 260 } catch (AttributeNotFoundException e) { 261 // If the modelerType attribute was not found, the class name is used 262 // instead. 263 LOG.error("getting attribute " + prs + " of " + oname 264 + " threw an exception", e); 265 } catch (MBeanException e) { 266 // The code inside the attribute getter threw an exception so log it, 267 // and fall back on the class name 268 LOG.error("getting attribute " + prs + " of " + oname 269 + " threw an exception", e); 270 } catch (RuntimeException e) { 271 // For some reason even with an MBeanException available to them 272 // Runtime exceptionscan still find their way through, so treat them 273 // the same as MBeanException 274 LOG.error("getting attribute " + prs + " of " + oname 275 + " threw an exception", e); 276 } catch ( ReflectionException e ) { 277 // This happens when the code inside the JMX bean (setter?? from the 278 // java docs) threw an exception, so log it and fall back on the 279 // class name 280 LOG.error("getting attribute " + prs + " of " + oname 281 + " threw an exception", e); 282 } 283 } catch (InstanceNotFoundException e) { 284 //Ignored for some reason the bean was not found so don't output it 285 continue; 286 } catch ( IntrospectionException e ) { 287 // This is an internal error, something odd happened with reflection so 288 // log it and don't output the bean. 289 LOG.error("Problem while trying to process JMX query: " + qry 290 + " with MBean " + oname, e); 291 continue; 292 } catch ( ReflectionException e ) { 293 // This happens when the code inside the JMX bean threw an exception, so 294 // log it and don't output the bean. 295 LOG.error("Problem while trying to process JMX query: " + qry 296 + " with MBean " + oname, e); 297 continue; 298 } 299 300 jg.writeStartObject(); 301 jg.writeStringField("name", oname.toString()); 302 303 jg.writeStringField("modelerType", code); 304 if ((attribute != null) && (attributeinfo == null)) { 305 jg.writeStringField("result", "ERROR"); 306 jg.writeStringField("message", "No attribute with name " + attribute 307 + " was found."); 308 jg.writeEndObject(); 309 jg.writeEndArray(); 310 jg.close(); 311 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 312 return; 313 } 314 315 if (attribute != null) { 316 writeAttribute(jg, attribute, attributeinfo); 317 } else { 318 MBeanAttributeInfo attrs[] = minfo.getAttributes(); 319 for (int i = 0; i < attrs.length; i++) { 320 writeAttribute(jg, oname, attrs[i]); 321 } 322 } 323 jg.writeEndObject(); 324 } 325 jg.writeEndArray(); 326 } 327 328 private void writeAttribute(JsonGenerator jg, ObjectName oname, MBeanAttributeInfo attr) throws IOException { 329 if (!attr.isReadable()) { 330 return; 331 } 332 String attName = attr.getName(); 333 if ("modelerType".equals(attName)) { 334 return; 335 } 336 if (attName.indexOf("=") >= 0 || attName.indexOf(":") >= 0 337 || attName.indexOf(" ") >= 0) { 338 return; 339 } 340 Object value = null; 341 try { 342 value = mBeanServer.getAttribute(oname, attName); 343 } catch (RuntimeMBeanException e) { 344 // UnsupportedOperationExceptions happen in the normal course of business, 345 // so no need to log them as errors all the time. 346 if (e.getCause() instanceof UnsupportedOperationException) { 347 LOG.debug("getting attribute "+attName+" of "+oname+" threw an exception", e); 348 } else { 349 LOG.error("getting attribute "+attName+" of "+oname+" threw an exception", e); 350 } 351 return; 352 } catch (RuntimeErrorException e) { 353 // RuntimeErrorException happens when an unexpected failure occurs in getAttribute 354 // for example https://issues.apache.org/jira/browse/DAEMON-120 355 LOG.debug("getting attribute "+attName+" of "+oname+" threw an exception", e); 356 return; 357 } catch (AttributeNotFoundException e) { 358 //Ignored the attribute was not found, which should never happen because the bean 359 //just told us that it has this attribute, but if this happens just don't output 360 //the attribute. 361 return; 362 } catch (MBeanException e) { 363 //The code inside the attribute getter threw an exception so log it, and 364 // skip outputting the attribute 365 LOG.error("getting attribute "+attName+" of "+oname+" threw an exception", e); 366 return; 367 } catch (RuntimeException e) { 368 //For some reason even with an MBeanException available to them Runtime exceptions 369 //can still find their way through, so treat them the same as MBeanException 370 LOG.error("getting attribute "+attName+" of "+oname+" threw an exception", e); 371 return; 372 } catch (ReflectionException e) { 373 //This happens when the code inside the JMX bean (setter?? from the java docs) 374 //threw an exception, so log it and skip outputting the attribute 375 LOG.error("getting attribute "+attName+" of "+oname+" threw an exception", e); 376 return; 377 } catch (InstanceNotFoundException e) { 378 //Ignored the mbean itself was not found, which should never happen because we 379 //just accessed it (perhaps something unregistered in-between) but if this 380 //happens just don't output the attribute. 381 return; 382 } 383 384 writeAttribute(jg, attName, value); 385 } 386 387 private void writeAttribute(JsonGenerator jg, String attName, Object value) throws IOException { 388 jg.writeFieldName(attName); 389 writeObject(jg, value); 390 } 391 392 private void writeObject(JsonGenerator jg, Object value) throws IOException { 393 if(value == null) { 394 jg.writeNull(); 395 } else { 396 Class<?> c = value.getClass(); 397 if (c.isArray()) { 398 jg.writeStartArray(); 399 int len = Array.getLength(value); 400 for (int j = 0; j < len; j++) { 401 Object item = Array.get(value, j); 402 writeObject(jg, item); 403 } 404 jg.writeEndArray(); 405 } else if(value instanceof Number) { 406 Number n = (Number)value; 407 jg.writeNumber(n.toString()); 408 } else if(value instanceof Boolean) { 409 Boolean b = (Boolean)value; 410 jg.writeBoolean(b); 411 } else if(value instanceof CompositeData) { 412 CompositeData cds = (CompositeData)value; 413 CompositeType comp = cds.getCompositeType(); 414 Set<String> keys = comp.keySet(); 415 jg.writeStartObject(); 416 for(String key: keys) { 417 writeAttribute(jg, key, cds.get(key)); 418 } 419 jg.writeEndObject(); 420 } else if(value instanceof TabularData) { 421 TabularData tds = (TabularData)value; 422 jg.writeStartArray(); 423 for(Object entry : tds.values()) { 424 writeObject(jg, entry); 425 } 426 jg.writeEndArray(); 427 } else { 428 jg.writeString(value.toString()); 429 } 430 } 431 } 432}