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