| /* |
| * 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.hbase.http.jmx; |
| |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.lang.management.ManagementFactory; |
| import java.util.Iterator; |
| import java.util.List; |
| import javax.management.MBeanServer; |
| import javax.management.MalformedObjectNameException; |
| import javax.management.ObjectName; |
| import javax.management.openmbean.CompositeData; |
| 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.hadoop.hbase.http.HttpServer; |
| import org.apache.hadoop.hbase.util.JSONBean; |
| import org.apache.yetus.audience.InterfaceAudience; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import org.apache.hbase.thirdparty.com.google.common.base.Splitter; |
| |
| /* |
| * 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. |
| * <p> |
| * This servlet generally will be placed under the /jmx URL for each HttpServer. It provides read |
| * only access to JMX metrics. The optional <code>qry</code> 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. |
| * </p> |
| * <p> |
| * For example <code>http://.../jmx?qry=Hadoop:*</code> will return all hadoop metrics exposed |
| * through JMX. |
| * </p> |
| * <p> |
| * The optional <code>get</code> parameter is used to query an specific attribute of a JMX bean. The |
| * format of the URL is <code>http://.../jmx?get=MXBeanName::AttributeName</code> |
| * </p> |
| * <p> |
| * For example <code> |
| * http://../jmx?get=Hadoop:service=NameNode,name=NameNodeInfo::ClusterId |
| * </code> will return the cluster id of the namenode mxbean. |
| * </p> |
| * <p> |
| * If we are not sure on the exact attribute and we want to get all the attributes that match one or |
| * more given pattern then the format is |
| * <code>http://.../jmx?get=MXBeanName::*[RegExp1],*[RegExp2]</code> |
| * </p> |
| * <p> |
| * For example <code> |
| * <p> |
| * http://../jmx?get=Hadoop:service=HBase,name=RegionServer,sub=Tables::[a-zA-z_0-9]*memStoreSize |
| * </p> |
| * <p> |
| * http://../jmx?get=Hadoop:service=HBase,name=RegionServer,sub=Tables::[a-zA-z_0-9]*memStoreSize,[a-zA-z_0-9]*storeFileSize |
| * </p> |
| * </code> |
| * </p> |
| * If the <code>qry</code> or the <code>get</code> parameter is not formatted correctly then a 400 |
| * BAD REQUEST http response code will be returned. |
| * </p> |
| * <p> |
| * If a resouce such as a mbean or attribute can not be found, a 404 SC_NOT_FOUND http response code |
| * will be returned. |
| * </p> |
| * <p> |
| * The return format is JSON and in the form |
| * </p> |
| * |
| * <pre> |
| * <code> |
| * { |
| * "beans" : [ |
| * { |
| * "name":"bean-name" |
| * ... |
| * } |
| * ] |
| * } |
| * </code> |
| * </pre> |
| * <p> |
| * 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. Optional paramater "callback" should be used to deliver JSONP response. |
| * </p> |
| */ |
| @InterfaceAudience.Private |
| public class JMXJsonServlet extends HttpServlet { |
| private static final Logger LOG = LoggerFactory.getLogger(JMXJsonServlet.class); |
| |
| private static final long serialVersionUID = 1L; |
| |
| private static final String CALLBACK_PARAM = "callback"; |
| /** |
| * If query string includes 'description', then we will emit bean and attribute descriptions to |
| * output IFF they are not null and IFF the description is not the same as the attribute name: |
| * i.e. specify a URL like so: /jmx?description=true |
| */ |
| private static final String INCLUDE_DESCRIPTION = "description"; |
| |
| /** |
| * MBean server. |
| */ |
| protected transient MBeanServer mBeanServer; |
| |
| protected transient JSONBean jsonBeanWriter; |
| |
| /** |
| * Initialize this servlet. |
| */ |
| @Override |
| public void init() throws ServletException { |
| // Retrieve the MBean server |
| mBeanServer = ManagementFactory.getPlatformMBeanServer(); |
| this.jsonBeanWriter = new JSONBean(); |
| } |
| |
| /** |
| * Process a GET request for the specified resource. n * The servlet request we are processing n * |
| * The servlet response we are creating |
| */ |
| @Override |
| public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { |
| try { |
| if (!HttpServer.isInstrumentationAccessAllowed(getServletContext(), request, response)) { |
| return; |
| } |
| String jsonpcb = null; |
| PrintWriter writer = null; |
| JSONBean.Writer beanWriter = null; |
| try { |
| jsonpcb = checkCallbackName(request.getParameter(CALLBACK_PARAM)); |
| writer = response.getWriter(); |
| |
| // "callback" parameter implies JSONP outpout |
| if (jsonpcb != null) { |
| response.setContentType("application/javascript; charset=utf8"); |
| writer.write(jsonpcb + "("); |
| } else { |
| response.setContentType("application/json; charset=utf8"); |
| } |
| beanWriter = this.jsonBeanWriter.open(writer); |
| // Should we output description on each attribute and bean? |
| String tmpStr = request.getParameter(INCLUDE_DESCRIPTION); |
| boolean description = tmpStr != null && tmpStr.length() > 0; |
| |
| // query per mbean attribute |
| String getmethod = request.getParameter("get"); |
| if (getmethod != null) { |
| List<String> splitStrings = Splitter.onPattern("\\:\\:").splitToList(getmethod); |
| if (splitStrings.size() != 2) { |
| beanWriter.write("result", "ERROR"); |
| beanWriter.write("message", "query format is not as expected."); |
| beanWriter.flush(); |
| response.setStatus(HttpServletResponse.SC_BAD_REQUEST); |
| return; |
| } |
| Iterator<String> i = splitStrings.iterator(); |
| if ( |
| beanWriter.write(this.mBeanServer, new ObjectName(i.next()), i.next(), description) != 0 |
| ) { |
| beanWriter.flush(); |
| response.setStatus(HttpServletResponse.SC_BAD_REQUEST); |
| } |
| return; |
| } |
| |
| // query per mbean |
| String qry = request.getParameter("qry"); |
| if (qry == null) { |
| qry = "*:*"; |
| } |
| String excl = request.getParameter("excl"); |
| ObjectName excluded = excl == null ? null : new ObjectName(excl); |
| |
| if ( |
| beanWriter.write(this.mBeanServer, new ObjectName(qry), null, description, excluded) != 0 |
| ) { |
| beanWriter.flush(); |
| response.setStatus(HttpServletResponse.SC_BAD_REQUEST); |
| } |
| } finally { |
| if (beanWriter != null) { |
| beanWriter.close(); |
| } |
| if (jsonpcb != null) { |
| writer.write(");"); |
| } |
| if (writer != null) { |
| writer.close(); |
| } |
| } |
| } catch (IOException e) { |
| LOG.error("Caught an exception while processing JMX request", e); |
| response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); |
| } catch (MalformedObjectNameException e) { |
| LOG.error("Caught an exception while processing JMX request", e); |
| response.sendError(HttpServletResponse.SC_BAD_REQUEST); |
| } |
| } |
| |
| /** |
| * Verifies that the callback property, if provided, is purely alphanumeric. This prevents a |
| * malicious callback name (that is javascript code) from being returned by the UI to an |
| * unsuspecting user. |
| * @param callbackName The callback name, can be null. |
| * @return The callback name |
| * @throws IOException If the name is disallowed. |
| */ |
| private String checkCallbackName(String callbackName) throws IOException { |
| if (null == callbackName) { |
| return null; |
| } |
| if (callbackName.matches("[A-Za-z0-9_]+")) { |
| return callbackName; |
| } |
| throw new IOException("'callback' must be alphanumeric"); |
| } |
| } |