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}