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 }