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  import org.owasp.esapi.ESAPI;
42  
43  /*
44   * This servlet is based off of the JMXProxyServlet from Tomcat 7.0.14. It has
45   * been rewritten to be read only and to output in a JSON format so it is not
46   * really that close to the original.
47   */
48  /**
49   * Provides Read only web access to JMX.
50   * <p>
51   * This servlet generally will be placed under the /jmx URL for each
52   * HttpServer.  It provides read only
53   * access to JMX metrics.  The optional <code>qry</code> parameter
54   * may be used to query only a subset of the JMX Beans.  This query
55   * functionality is provided through the
56   * {@link MBeanServer#queryNames(ObjectName, javax.management.QueryExp)}
57   * method.
58   * </p>
59   * <p>
60   * For example <code>http://.../jmx?qry=Hadoop:*</code> will return
61   * all hadoop metrics exposed through JMX.
62   * </p>
63   * <p>
64   * The optional <code>get</code> parameter is used to query an specific
65   * attribute of a JMX bean.  The format of the URL is
66   * <code>http://.../jmx?get=MXBeanName::AttributeName</code>
67   * </p>
68   * <p>
69   * For example
70   * <code>
71   * http://../jmx?get=Hadoop:service=NameNode,name=NameNodeInfo::ClusterId
72   * </code> will return the cluster id of the namenode mxbean.
73   * </p>
74   * <p>
75   * If the <code>qry</code> or the <code>get</code> parameter is not formatted 
76   * correctly then a 400 BAD REQUEST http response code will be returned. 
77   * </p>
78   * <p>
79   * If a resouce such as a mbean or attribute can not be found, 
80   * a 404 SC_NOT_FOUND http response code will be returned. 
81   * </p>
82   * <p>
83   * The return format is JSON and in the form
84   * </p>
85   *  <pre><code>
86   *  {
87   *    "beans" : [
88   *      {
89   *        "name":"bean-name"
90   *        ...
91   *      }
92   *    ]
93   *  }
94   *  </code></pre>
95   *  <p>
96   *  The servlet attempts to convert the the JMXBeans into JSON. Each
97   *  bean's attributes will be converted to a JSON object member.
98   *
99   *  If the attribute is a boolean, a number, a string, or an array
100  *  it will be converted to the JSON equivalent.
101  *
102  *  If the value is a {@link CompositeData} then it will be converted
103  *  to a JSON object with the keys as the name of the JSON member and
104  *  the value is converted following these same rules.
105  *
106  *  If the value is a {@link TabularData} then it will be converted
107  *  to an array of the {@link CompositeData} elements that it contains.
108  *
109  *  All other objects will be converted to a string and output as such.
110  *
111  *  The bean's name and modelerType will be returned for all beans.
112  *
113  *  Optional paramater "callback" should be used to deliver JSONP response.
114  * </p>
115  *  
116  */
117 public class JMXJsonServlet extends HttpServlet {
118   private static final Log LOG = LogFactory.getLog(JMXJsonServlet.class);
119 
120   private static final long serialVersionUID = 1L;
121 
122   private static final String CALLBACK_PARAM = "callback";
123   /**
124    * If query string includes 'description', then we will emit bean and attribute descriptions to
125    * output IFF they are not null and IFF the description is not the same as the attribute name:
126    * i.e. specify an URL like so: /jmx?description=true
127    */
128   private static final String INCLUDE_DESCRIPTION = "description";
129 
130   /**
131    * MBean server.
132    */
133   protected transient MBeanServer mBeanServer;
134 
135   protected transient JSONBean jsonBeanWriter;
136 
137   /**
138    * Initialize this servlet.
139    */
140   @Override
141   public void init() throws ServletException {
142     // Retrieve the MBean server
143     mBeanServer = ManagementFactory.getPlatformMBeanServer();
144     this.jsonBeanWriter = new JSONBean();
145   }
146 
147   /**
148    * Process a GET request for the specified resource.
149    *
150    * @param request
151    *          The servlet request we are processing
152    * @param response
153    *          The servlet response we are creating
154    */
155   @Override
156   @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="XSS_REQUEST_PARAMETER_TO_SERVLET_WRITER",
157     justification="TODO: See HBASE-15122")
158   public void doGet(HttpServletRequest request, HttpServletResponse response) {
159     try {
160       if (!HttpServer.isInstrumentationAccessAllowed(getServletContext(), request, response)) {
161         return;
162       }
163       String jsonpcb = null;
164       PrintWriter writer = null;
165       JSONBean.Writer beanWriter = null;
166       try {
167         writer = response.getWriter();
168         beanWriter = this.jsonBeanWriter.open(writer);
169         // "callback" parameter implies JSONP outpout
170         jsonpcb = request.getParameter(CALLBACK_PARAM);
171         if (jsonpcb != null) {
172           response.setContentType("application/javascript; charset=utf8");
173           writer.write(encodeJS(jsonpcb) + "(");
174         } else {
175           response.setContentType("application/json; charset=utf8");
176         }
177         // Should we output description on each attribute and bean?
178         String tmpStr = request.getParameter(INCLUDE_DESCRIPTION);
179         boolean description = tmpStr != null && tmpStr.length() > 0;
180 
181         // query per mbean attribute
182         String getmethod = request.getParameter("get");
183         if (getmethod != null) {
184           String[] splitStrings = getmethod.split("\\:\\:");
185           if (splitStrings.length != 2) {
186             beanWriter.write("result", "ERROR");
187             beanWriter.write("message", "query format is not as expected.");
188             beanWriter.flush();
189             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
190             return;
191           }
192           if (beanWriter.write(this.mBeanServer, new ObjectName(splitStrings[0]),
193               splitStrings[1], description) != 0) {
194             beanWriter.flush();
195             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
196           }
197           return;
198         }
199 
200         // query per mbean
201         String qry = request.getParameter("qry");
202         if (qry == null) {
203           qry = "*:*";
204         }
205         if (beanWriter.write(this.mBeanServer, new ObjectName(qry), null, description) != 0) {
206           beanWriter.flush();
207           response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
208         }
209       } finally {
210         if (beanWriter != null) beanWriter.close();
211         if (jsonpcb != null) {
212            writer.write(");");
213         }
214         if (writer != null) {
215           writer.close();
216         }
217       }
218     } catch (IOException e) {
219       LOG.error("Caught an exception while processing JMX request", e);
220       response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
221     } catch (MalformedObjectNameException e) {
222       LOG.error("Caught an exception while processing JMX request", e);
223       response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
224     }
225   }
226 
227   private String encodeJS(String inputStr) {
228     return ESAPI.encoder().encodeForJavaScript(inputStr);
229   }
230 
231 }