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.hbase.http.jmx; 019 020import java.io.IOException; 021import java.io.PrintWriter; 022import java.lang.management.ManagementFactory; 023import javax.management.MBeanServer; 024import javax.management.MalformedObjectNameException; 025import javax.management.ObjectName; 026import javax.management.openmbean.CompositeData; 027import javax.management.openmbean.TabularData; 028import javax.servlet.ServletException; 029import javax.servlet.http.HttpServlet; 030import javax.servlet.http.HttpServletRequest; 031import javax.servlet.http.HttpServletResponse; 032import org.apache.hadoop.hbase.http.HttpServer; 033import org.apache.hadoop.hbase.util.JSONBean; 034import org.apache.yetus.audience.InterfaceAudience; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037 038/* 039 * This servlet is based off of the JMXProxyServlet from Tomcat 7.0.14. It has 040 * been rewritten to be read only and to output in a JSON format so it is not 041 * really that close to the original. 042 */ 043/** 044 * Provides Read only web access to JMX. 045 * <p> 046 * This servlet generally will be placed under the /jmx URL for each 047 * HttpServer. It provides read only 048 * access to JMX metrics. The optional <code>qry</code> parameter 049 * may be used to query only a subset of the JMX Beans. This query 050 * functionality is provided through the 051 * {@link MBeanServer#queryNames(ObjectName, javax.management.QueryExp)} 052 * method. 053 * </p> 054 * <p> 055 * For example <code>http://.../jmx?qry=Hadoop:*</code> will return 056 * all hadoop metrics exposed through JMX. 057 * </p> 058 * <p> 059 * The optional <code>get</code> parameter is used to query an specific 060 * attribute of a JMX bean. The format of the URL is 061 * <code>http://.../jmx?get=MXBeanName::AttributeName</code> 062 * </p> 063 * <p> 064 * For example 065 * <code> 066 * http://../jmx?get=Hadoop:service=NameNode,name=NameNodeInfo::ClusterId 067 * </code> will return the cluster id of the namenode mxbean. 068 * </p> 069 * <p> 070 * If we are not sure on the exact attribute and we want to get all the attributes that match one or 071 * more given pattern then the format is 072 * <code>http://.../jmx?get=MXBeanName::*[RegExp1],*[RegExp2]</code> 073 * </p> 074 * <p> 075 * For example 076 * <code> 077 * <p> 078 * http://../jmx?get=Hadoop:service=HBase,name=RegionServer,sub=Tables::[a-zA-z_0-9]*memStoreSize 079 * </p> 080 * <p> 081 * http://../jmx?get=Hadoop:service=HBase,name=RegionServer,sub=Tables::[a-zA-z_0-9]*memStoreSize,[a-zA-z_0-9]*storeFileSize 082 * </p> 083 * </code> 084 * </p> 085 * If the <code>qry</code> or the <code>get</code> parameter is not formatted 086 * correctly then a 400 BAD REQUEST http response code will be returned. 087 * </p> 088 * <p> 089 * If a resouce such as a mbean or attribute can not be found, 090 * a 404 SC_NOT_FOUND http response code will be returned. 091 * </p> 092 * <p> 093 * The return format is JSON and in the form 094 * </p> 095 * <pre><code> 096 * { 097 * "beans" : [ 098 * { 099 * "name":"bean-name" 100 * ... 101 * } 102 * ] 103 * } 104 * </code></pre> 105 * <p> 106 * The servlet attempts to convert the the JMXBeans into JSON. Each 107 * bean's attributes will be converted to a JSON object member. 108 * 109 * If the attribute is a boolean, a number, a string, or an array 110 * it will be converted to the JSON equivalent. 111 * 112 * If the value is a {@link CompositeData} then it will be converted 113 * to a JSON object with the keys as the name of the JSON member and 114 * the value is converted following these same rules. 115 * 116 * If the value is a {@link TabularData} then it will be converted 117 * to an array of the {@link CompositeData} elements that it contains. 118 * 119 * All other objects will be converted to a string and output as such. 120 * 121 * The bean's name and modelerType will be returned for all beans. 122 * 123 * Optional paramater "callback" should be used to deliver JSONP response. 124 * </p> 125 * 126 */ 127@InterfaceAudience.Private 128public class JMXJsonServlet extends HttpServlet { 129 private static final Logger LOG = LoggerFactory.getLogger(JMXJsonServlet.class); 130 131 private static final long serialVersionUID = 1L; 132 133 private static final String CALLBACK_PARAM = "callback"; 134 /** 135 * If query string includes 'description', then we will emit bean and attribute descriptions to 136 * output IFF they are not null and IFF the description is not the same as the attribute name: 137 * i.e. specify a URL like so: /jmx?description=true 138 */ 139 private static final String INCLUDE_DESCRIPTION = "description"; 140 141 /** 142 * MBean server. 143 */ 144 protected transient MBeanServer mBeanServer; 145 146 protected transient JSONBean jsonBeanWriter; 147 148 /** 149 * Initialize this servlet. 150 */ 151 @Override 152 public void init() throws ServletException { 153 // Retrieve the MBean server 154 mBeanServer = ManagementFactory.getPlatformMBeanServer(); 155 this.jsonBeanWriter = new JSONBean(); 156 } 157 158 /** 159 * Process a GET request for the specified resource. 160 * 161 * @param request 162 * The servlet request we are processing 163 * @param response 164 * The servlet response we are creating 165 */ 166 @Override 167 public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { 168 try { 169 if (!HttpServer.isInstrumentationAccessAllowed(getServletContext(), request, response)) { 170 return; 171 } 172 String jsonpcb = null; 173 PrintWriter writer = null; 174 JSONBean.Writer beanWriter = null; 175 try { 176 jsonpcb = checkCallbackName(request.getParameter(CALLBACK_PARAM)); 177 writer = response.getWriter(); 178 179 // "callback" parameter implies JSONP outpout 180 if (jsonpcb != null) { 181 response.setContentType("application/javascript; charset=utf8"); 182 writer.write(jsonpcb + "("); 183 } else { 184 response.setContentType("application/json; charset=utf8"); 185 } 186 beanWriter = this.jsonBeanWriter.open(writer); 187 // Should we output description on each attribute and bean? 188 String tmpStr = request.getParameter(INCLUDE_DESCRIPTION); 189 boolean description = tmpStr != null && tmpStr.length() > 0; 190 191 // query per mbean attribute 192 String getmethod = request.getParameter("get"); 193 if (getmethod != null) { 194 String[] splitStrings = getmethod.split("\\:\\:"); 195 if (splitStrings.length != 2) { 196 beanWriter.write("result", "ERROR"); 197 beanWriter.write("message", "query format is not as expected."); 198 beanWriter.flush(); 199 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 200 return; 201 } 202 if (beanWriter.write(this.mBeanServer, new ObjectName(splitStrings[0]), 203 splitStrings[1], description) != 0) { 204 beanWriter.flush(); 205 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 206 } 207 return; 208 } 209 210 // query per mbean 211 String qry = request.getParameter("qry"); 212 if (qry == null) { 213 qry = "*:*"; 214 } 215 if (beanWriter.write(this.mBeanServer, new ObjectName(qry), null, description) != 0) { 216 beanWriter.flush(); 217 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 218 } 219 } finally { 220 if (beanWriter != null) { 221 beanWriter.close(); 222 } 223 if (jsonpcb != null) { 224 writer.write(");"); 225 } 226 if (writer != null) { 227 writer.close(); 228 } 229 } 230 } catch (IOException e) { 231 LOG.error("Caught an exception while processing JMX request", e); 232 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 233 } catch (MalformedObjectNameException e) { 234 LOG.error("Caught an exception while processing JMX request", e); 235 response.sendError(HttpServletResponse.SC_BAD_REQUEST); 236 } 237 } 238 239 /** 240 * Verifies that the callback property, if provided, is purely alphanumeric. 241 * This prevents a malicious callback name (that is javascript code) from being 242 * returned by the UI to an unsuspecting user. 243 * 244 * @param callbackName The callback name, can be null. 245 * @return The callback name 246 * @throws IOException If the name is disallowed. 247 */ 248 private String checkCallbackName(String callbackName) throws IOException { 249 if (null == callbackName) { 250 return null; 251 } 252 if (callbackName.matches("[A-Za-z0-9_]+")) { 253 return callbackName; 254 } 255 throw new IOException("'callback' must be alphanumeric"); 256 } 257}