View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.hadoop.hbase.http.jmx;
19  
20  import java.io.IOException;
21  import java.io.PrintWriter;
22  import java.lang.management.ManagementFactory;
23  
24  import javax.management.MBeanServer;
25  import javax.management.MalformedObjectNameException;
26  import javax.management.ObjectName;
27  import javax.management.ReflectionException;
28  import javax.management.RuntimeErrorException;
29  import javax.management.RuntimeMBeanException;
30  import javax.management.openmbean.CompositeData;
31  import javax.management.openmbean.TabularData;
32  import javax.servlet.ServletException;
33  import javax.servlet.http.HttpServlet;
34  import javax.servlet.http.HttpServletRequest;
35  import javax.servlet.http.HttpServletResponse;
36  
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  import org.apache.hadoop.hbase.http.HttpServer;
40  import org.apache.hadoop.hbase.util.JSONBean;
41  
42  /*
43   * This servlet is based off of the JMXProxyServlet from Tomcat 7.0.14. It has
44   * been rewritten to be read only and to output in a JSON format so it is not
45   * really that close to the original.
46   */
47  /**
48   * Provides Read only web access to JMX.
49   * <p>
50   * This servlet generally will be placed under the /jmx URL for each
51   * HttpServer.  It provides read only
52   * access to JMX metrics.  The optional <code>qry</code> parameter
53   * may be used to query only a subset of the JMX Beans.  This query
54   * functionality is provided through the
55   * {@link MBeanServer#queryNames(ObjectName, javax.management.QueryExp)}
56   * method.
57   * </p>
58   * <p>
59   * For example <code>http://.../jmx?qry=Hadoop:*</code> will return
60   * all hadoop metrics exposed through JMX.
61   * </p>
62   * <p>
63   * The optional <code>get</code> parameter is used to query an specific
64   * attribute of a JMX bean.  The format of the URL is
65   * <code>http://.../jmx?get=MXBeanName::AttributeName</code>
66   * </p>
67   * <p>
68   * For example
69   * <code>
70   * http://../jmx?get=Hadoop:service=NameNode,name=NameNodeInfo::ClusterId
71   * </code> will return the cluster id of the namenode mxbean.
72   * </p>
73   * <p>
74   * If the <code>qry</code> or the <code>get</code> parameter is not formatted 
75   * correctly then a 400 BAD REQUEST http response code will be returned. 
76   * </p>
77   * <p>
78   * If a resouce such as a mbean or attribute can not be found, 
79   * a 404 SC_NOT_FOUND http response code will be returned. 
80   * </p>
81   * <p>
82   * The return format is JSON and in the form
83   * </p>
84   *  <pre><code>
85   *  {
86   *    "beans" : [
87   *      {
88   *        "name":"bean-name"
89   *        ...
90   *      }
91   *    ]
92   *  }
93   *  </code></pre>
94   *  <p>
95   *  The servlet attempts to convert the the JMXBeans into JSON. Each
96   *  bean's attributes will be converted to a JSON object member.
97   *
98   *  If the attribute is a boolean, a number, a string, or an array
99   *  it will be converted to the JSON equivalent.
100  *
101  *  If the value is a {@link CompositeData} then it will be converted
102  *  to a JSON object with the keys as the name of the JSON member and
103  *  the value is converted following these same rules.
104  *
105  *  If the value is a {@link TabularData} then it will be converted
106  *  to an array of the {@link CompositeData} elements that it contains.
107  *
108  *  All other objects will be converted to a string and output as such.
109  *
110  *  The bean's name and modelerType will be returned for all beans.
111  *
112  *  Optional paramater "callback" should be used to deliver JSONP response.
113  * </p>
114  *  
115  */
116 public class JMXJsonServlet extends HttpServlet {
117   private static final Log LOG = LogFactory.getLog(JMXJsonServlet.class);
118 
119   private static final long serialVersionUID = 1L;
120 
121   private static final String CALLBACK_PARAM = "callback";
122   /**
123    * If query string includes 'description', then we will emit bean and attribute descriptions to
124    * output IFF they are not null and IFF the description is not the same as the attribute name:
125    * i.e. specify an URL like so: /jmx?description=true
126    */
127   private static final String INCLUDE_DESCRIPTION = "description";
128 
129   /**
130    * MBean server.
131    */
132   protected transient MBeanServer mBeanServer;
133 
134   protected transient JSONBean jsonBeanWriter;
135 
136   /**
137    * Initialize this servlet.
138    */
139   @Override
140   public void init() throws ServletException {
141     // Retrieve the MBean server
142     mBeanServer = ManagementFactory.getPlatformMBeanServer();
143     this.jsonBeanWriter = new JSONBean();
144   }
145 
146   /**
147    * Process a GET request for the specified resource.
148    *
149    * @param request
150    *          The servlet request we are processing
151    * @param response
152    *          The servlet response we are creating
153    */
154   @Override
155   public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
156     try {
157       if (!HttpServer.isInstrumentationAccessAllowed(getServletContext(), request, response)) {
158         return;
159       }
160       String jsonpcb = null;
161       PrintWriter writer = null;
162       JSONBean.Writer beanWriter = null;
163       try {
164         jsonpcb = checkCallbackName(request.getParameter(CALLBACK_PARAM));
165         writer = response.getWriter();
166         beanWriter = this.jsonBeanWriter.open(writer);
167         // "callback" parameter implies JSONP outpout
168         if (jsonpcb != null) {
169           response.setContentType("application/javascript; charset=utf8");
170           writer.write(jsonpcb + "(");
171         } else {
172           response.setContentType("application/json; charset=utf8");
173         }
174         // Should we output description on each attribute and bean?
175         String tmpStr = request.getParameter(INCLUDE_DESCRIPTION);
176         boolean description = tmpStr != null && tmpStr.length() > 0;
177 
178         // query per mbean attribute
179         String getmethod = request.getParameter("get");
180         if (getmethod != null) {
181           String[] splitStrings = getmethod.split("\\:\\:");
182           if (splitStrings.length != 2) {
183             beanWriter.write("result", "ERROR");
184             beanWriter.write("message", "query format is not as expected.");
185             beanWriter.flush();
186             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
187             return;
188           }
189           if (beanWriter.write(this.mBeanServer, new ObjectName(splitStrings[0]),
190               splitStrings[1], description) != 0) {
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         if (beanWriter.write(this.mBeanServer, new ObjectName(qry), null, description) != 0) {
203           beanWriter.flush();
204           response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
205         }
206       } finally {
207         if (beanWriter != null) beanWriter.close();
208         if (jsonpcb != null) {
209            writer.write(");");
210         }
211         if (writer != null) {
212           writer.close();
213         }
214       }
215     } catch (IOException e) {
216       LOG.error("Caught an exception while processing JMX request", e);
217       response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
218     } catch (MalformedObjectNameException e) {
219       LOG.error("Caught an exception while processing JMX request", e);
220       response.sendError(HttpServletResponse.SC_BAD_REQUEST);
221     }
222   }
223 
224   /**
225    * Verifies that the callback property, if provided, is purely alphanumeric.
226    * This prevents a malicious callback name (that is javascript code) from being
227    * returned by the UI to an unsuspecting user.
228    *
229    * @param callbackName The callback name, can be null.
230    * @return The callback name
231    * @throws IOException If the name is disallowed.
232    */
233   private String checkCallbackName(String callbackName) throws IOException {
234     if (null == callbackName) {
235       return null;
236     }
237     if (callbackName.matches("[A-Za-z0-9_]+")) {
238       return callbackName;
239     }
240     throw new IOException("'callback' must be alphanumeric");
241   }
242 }