001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 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 HttpServer. It provides read 047 * only access to JMX metrics. The optional <code>qry</code> parameter may be used to query only a 048 * subset of the JMX Beans. This query functionality is provided through the 049 * {@link MBeanServer#queryNames(ObjectName, javax.management.QueryExp)} method. 050 * </p> 051 * <p> 052 * For example <code>http://.../jmx?qry=Hadoop:*</code> will return all hadoop metrics exposed 053 * through JMX. 054 * </p> 055 * <p> 056 * The optional <code>get</code> parameter is used to query an specific attribute of a JMX bean. The 057 * format of the URL is <code>http://.../jmx?get=MXBeanName::AttributeName</code> 058 * </p> 059 * <p> 060 * For example <code> 061 * http://../jmx?get=Hadoop:service=NameNode,name=NameNodeInfo::ClusterId 062 * </code> will return the cluster id of the namenode mxbean. 063 * </p> 064 * <p> 065 * If we are not sure on the exact attribute and we want to get all the attributes that match one or 066 * more given pattern then the format is 067 * <code>http://.../jmx?get=MXBeanName::*[RegExp1],*[RegExp2]</code> 068 * </p> 069 * <p> 070 * For example <code> 071 * <p> 072 * http://../jmx?get=Hadoop:service=HBase,name=RegionServer,sub=Tables::[a-zA-z_0-9]*memStoreSize 073 * </p> 074 * <p> 075 * http://../jmx?get=Hadoop:service=HBase,name=RegionServer,sub=Tables::[a-zA-z_0-9]*memStoreSize,[a-zA-z_0-9]*storeFileSize 076 * </p> 077 * </code> 078 * </p> 079 * If the <code>qry</code> or the <code>get</code> parameter is not formatted correctly then a 400 080 * BAD REQUEST http response code will be returned. 081 * </p> 082 * <p> 083 * If a resouce such as a mbean or attribute can not be found, a 404 SC_NOT_FOUND http response code 084 * will be returned. 085 * </p> 086 * <p> 087 * The return format is JSON and in the form 088 * </p> 089 * 090 * <pre> 091 * <code> 092 * { 093 * "beans" : [ 094 * { 095 * "name":"bean-name" 096 * ... 097 * } 098 * ] 099 * } 100 * </code> 101 * </pre> 102 * <p> 103 * The servlet attempts to convert the the JMXBeans into JSON. Each bean's attributes will be 104 * converted to a JSON object member. If the attribute is a boolean, a number, a string, or an array 105 * it will be converted to the JSON equivalent. If the value is a {@link CompositeData} then it will 106 * be converted to a JSON object with the keys as the name of the JSON member and the value is 107 * converted following these same rules. If the value is a {@link TabularData} then it will be 108 * converted to an array of the {@link CompositeData} elements that it contains. All other objects 109 * will be converted to a string and output as such. The bean's name and modelerType will be 110 * returned for all beans. Optional paramater "callback" should be used to deliver JSONP response. 111 * </p> 112 */ 113@InterfaceAudience.Private 114public class JMXJsonServlet extends HttpServlet { 115 private static final Logger LOG = LoggerFactory.getLogger(JMXJsonServlet.class); 116 117 private static final long serialVersionUID = 1L; 118 119 private static final String CALLBACK_PARAM = "callback"; 120 /** 121 * If query string includes 'description', then we will emit bean and attribute descriptions to 122 * output IFF they are not null and IFF the description is not the same as the attribute name: 123 * i.e. specify a URL like so: /jmx?description=true 124 */ 125 private static final String INCLUDE_DESCRIPTION = "description"; 126 127 /** 128 * MBean server. 129 */ 130 protected transient MBeanServer mBeanServer; 131 132 protected transient JSONBean jsonBeanWriter; 133 134 /** 135 * Initialize this servlet. 136 */ 137 @Override 138 public void init() throws ServletException { 139 // Retrieve the MBean server 140 mBeanServer = ManagementFactory.getPlatformMBeanServer(); 141 this.jsonBeanWriter = new JSONBean(); 142 } 143 144 /** 145 * Process a GET request for the specified resource. n * The servlet request we are processing n * 146 * The servlet response we are creating 147 */ 148 @Override 149 public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { 150 try { 151 if (!HttpServer.isInstrumentationAccessAllowed(getServletContext(), request, response)) { 152 return; 153 } 154 String jsonpcb = null; 155 PrintWriter writer = null; 156 JSONBean.Writer beanWriter = null; 157 try { 158 jsonpcb = checkCallbackName(request.getParameter(CALLBACK_PARAM)); 159 writer = response.getWriter(); 160 161 // "callback" parameter implies JSONP outpout 162 if (jsonpcb != null) { 163 response.setContentType("application/javascript; charset=utf8"); 164 writer.write(jsonpcb + "("); 165 } else { 166 response.setContentType("application/json; charset=utf8"); 167 } 168 beanWriter = this.jsonBeanWriter.open(writer); 169 // Should we output description on each attribute and bean? 170 String tmpStr = request.getParameter(INCLUDE_DESCRIPTION); 171 boolean description = tmpStr != null && tmpStr.length() > 0; 172 173 // query per mbean attribute 174 String getmethod = request.getParameter("get"); 175 if (getmethod != null) { 176 String[] splitStrings = getmethod.split("\\:\\:"); 177 if (splitStrings.length != 2) { 178 beanWriter.write("result", "ERROR"); 179 beanWriter.write("message", "query format is not as expected."); 180 beanWriter.flush(); 181 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 182 return; 183 } 184 if ( 185 beanWriter.write(this.mBeanServer, new ObjectName(splitStrings[0]), splitStrings[1], 186 description) != 0 187 ) { 188 beanWriter.flush(); 189 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 190 } 191 return; 192 } 193 194 // query per mbean 195 String qry = request.getParameter("qry"); 196 if (qry == null) { 197 qry = "*:*"; 198 } 199 String excl = request.getParameter("excl"); 200 ObjectName excluded = excl == null ? null : new ObjectName(excl); 201 202 if ( 203 beanWriter.write(this.mBeanServer, new ObjectName(qry), null, description, excluded) != 0 204 ) { 205 beanWriter.flush(); 206 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 207 } 208 } finally { 209 if (beanWriter != null) { 210 beanWriter.close(); 211 } 212 if (jsonpcb != null) { 213 writer.write(");"); 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.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 222 } catch (MalformedObjectNameException e) { 223 LOG.error("Caught an exception while processing JMX request", e); 224 response.sendError(HttpServletResponse.SC_BAD_REQUEST); 225 } 226 } 227 228 /** 229 * Verifies that the callback property, if provided, is purely alphanumeric. This prevents a 230 * malicious callback name (that is javascript code) from being returned by the UI to an 231 * unsuspecting user. 232 * @param callbackName The callback name, can be null. 233 * @return The callback name 234 * @throws IOException If the name is disallowed. 235 */ 236 private String checkCallbackName(String callbackName) throws IOException { 237 if (null == callbackName) { 238 return null; 239 } 240 if (callbackName.matches("[A-Za-z0-9_]+")) { 241 return callbackName; 242 } 243 throw new IOException("'callback' must be alphanumeric"); 244 } 245}