Project

General

Profile

Feature #2103 ยป JMXJsonServlet.java

Udo Offermann, 13.09.2021 15:35

 
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.jmx;
19

    
20
import org.apache.commons.logging.Log;
21
import org.apache.commons.logging.LogFactory;
22
import org.apache.hadoop.http.HttpServer2;
23
import org.codehaus.jackson.JsonFactory;
24
import org.codehaus.jackson.JsonGenerator;
25

    
26
import javax.management.AttributeNotFoundException;
27
import javax.management.InstanceNotFoundException;
28
import javax.management.IntrospectionException;
29
import javax.management.MBeanAttributeInfo;
30
import javax.management.MBeanException;
31
import javax.management.MBeanInfo;
32
import javax.management.MBeanServer;
33
import javax.management.MalformedObjectNameException;
34
import javax.management.ObjectName;
35
import javax.management.ReflectionException;
36
import javax.management.RuntimeErrorException;
37
import javax.management.RuntimeMBeanException;
38
import javax.management.openmbean.CompositeData;
39
import javax.management.openmbean.CompositeType;
40
import javax.management.openmbean.TabularData;
41
import javax.servlet.ServletContext;
42
import javax.servlet.ServletException;
43
import javax.servlet.http.HttpServlet;
44
import javax.servlet.http.HttpServletRequest;
45
import javax.servlet.http.HttpServletResponse;
46
import java.io.IOException;
47
import java.io.PrintWriter;
48
import java.lang.management.ManagementFactory;
49
import java.lang.reflect.Array;
50
import java.util.Iterator;
51
import java.util.Set;
52

    
53
/*
54
 * This servlet is based off of the JMXProxyServlet from Tomcat 7.0.14. It has
55
 * been rewritten to be read only and to output in a JSON format so it is not
56
 * really that close to the original.
57
 */
58
/**
59
 * Provides Read only web access to JMX.
60
 * <p>
61
 * This servlet generally will be placed under the /jmx URL for each
62
 * HttpServer.  It provides read only
63
 * access to JMX metrics.  The optional <code>qry</code> parameter
64
 * may be used to query only a subset of the JMX Beans.  This query
65
 * functionality is provided through the
66
 * {@link MBeanServer#queryNames(ObjectName, javax.management.QueryExp)}
67
 * method.
68
 * <p>
69
 * For example <code>http://.../jmx?qry=Hadoop:*</code> will return
70
 * all hadoop metrics exposed through JMX.
71
 * <p>
72
 * The optional <code>get</code> parameter is used to query an specific 
73
 * attribute of a JMX bean.  The format of the URL is
74
 * <code>http://.../jmx?get=MXBeanName::AttributeName<code>
75
 * <p>
76
 * For example 
77
 * <code>
78
 * http://../jmx?get=Hadoop:service=NameNode,name=NameNodeInfo::ClusterId
79
 * </code> will return the cluster id of the namenode mxbean.
80
 * <p>
81
 * If the <code>qry</code> or the <code>get</code> parameter is not formatted 
82
 * correctly then a 400 BAD REQUEST http response code will be returned. 
83
 * <p>
84
 * If a resouce such as a mbean or attribute can not be found, 
85
 * a 404 SC_NOT_FOUND http response code will be returned. 
86
 * <p>
87
 * The return format is JSON and in the form
88
 * <p>
89
 *  <code><pre>
90
 *  {
91
 *    "beans" : [
92
 *      {
93
 *        "name":"bean-name"
94
 *        ...
95
 *      }
96
 *    ]
97
 *  }
98
 *  </pre></code>
99
 *  <p>
100
 *  The servlet attempts to convert the the JMXBeans into JSON. Each
101
 *  bean's attributes will be converted to a JSON object member.
102
 *  
103
 *  If the attribute is a boolean, a number, a string, or an array
104
 *  it will be converted to the JSON equivalent. 
105
 *  
106
 *  If the value is a {@link CompositeData} then it will be converted
107
 *  to a JSON object with the keys as the name of the JSON member and
108
 *  the value is converted following these same rules.
109
 *  
110
 *  If the value is a {@link TabularData} then it will be converted
111
 *  to an array of the {@link CompositeData} elements that it contains.
112
 *  
113
 *  All other objects will be converted to a string and output as such.
114
 *  
115
 *  The bean's name and modelerType will be returned for all beans.
116
 *
117
 */
118
public class JMXJsonServlet extends HttpServlet {
119
  private static final Log LOG = LogFactory.getLog(JMXJsonServlet.class);
120
  static final String ACCESS_CONTROL_ALLOW_METHODS =
121
      "Access-Control-Allow-Methods";
122
  static final String ACCESS_CONTROL_ALLOW_ORIGIN =
123
      "Access-Control-Allow-Origin";
124

    
125
  private static final long serialVersionUID = 1L;
126

    
127
  /**
128
   * MBean server.
129
   */
130
  protected transient MBeanServer mBeanServer = null;
131

    
132
  // --------------------------------------------------------- Public Methods
133
  /**
134
   * Initialize this servlet.
135
   */
136
  @Override
137
  public void init() throws ServletException {
138
    // Retrieve the MBean server
139
    mBeanServer = ManagementFactory.getPlatformMBeanServer();
140
  }
141

    
142
  protected boolean isInstrumentationAccessAllowed(HttpServletRequest request,
143
      HttpServletResponse response) throws IOException {
144
    return HttpServer2.isInstrumentationAccessAllowed(getServletContext(),
145
        request, response);
146
  }
147

    
148
  /**
149
   * Disable TRACE method to avoid TRACE vulnerability.
150
   */
151
  @Override
152
  protected void doTrace(HttpServletRequest req, HttpServletResponse resp)
153
      throws ServletException, IOException {
154
    resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
155
  }
156

    
157
  /**
158
   * Process a GET request for the specified resource.
159
   * 
160
   * @param request
161
   *          The servlet request we are processing
162
   * @param response
163
   *          The servlet response we are creating
164
   */
165
  @Override
166
  public void doGet(HttpServletRequest request, HttpServletResponse response) {
167
    String jsonpcb = null;
168
    PrintWriter writer = null;
169
    try {
170
      // If user is a static user and auth Type is null, that means
171
      // there is a non-security environment and no need authorization,
172
      // otherwise, do the authorization.
173
      final ServletContext servletContext = getServletContext();
174
      if (!HttpServer2.isStaticUserAndNoneAuthType(servletContext, request) &&
175
          !isInstrumentationAccessAllowed(request, response)) {
176
        return;
177
      }
178
      
179
      JsonGenerator jg = null;
180
      try {
181
        writer = response.getWriter();
182
 
183
        response.setContentType("application/json; charset=utf8");
184
        response.setHeader(ACCESS_CONTROL_ALLOW_METHODS, "GET");
185
        response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
186

    
187
      JsonFactory jsonFactory = new JsonFactory();
188
      jg = jsonFactory.createJsonGenerator(writer);
189
      jg.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
190
      jg.useDefaultPrettyPrinter();
191
      jg.writeStartObject();
192

    
193
      if (mBeanServer == null) {
194
        jg.writeStringField("result", "ERROR");
195
        jg.writeStringField("message", "No MBeanServer could be found");
196
        jg.close();
197
        LOG.error("No MBeanServer could be found.");
198
        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
199
        return;
200
      }
201
      
202
      // query per mbean attribute
203
      String getmethod = request.getParameter("get");
204
      if (getmethod != null) {
205
        String[] splitStrings = getmethod.split("\\:\\:");
206
        if (splitStrings.length != 2) {
207
          jg.writeStringField("result", "ERROR");
208
          jg.writeStringField("message", "query format is not as expected.");
209
          jg.close();
210
          response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
211
          return;
212
        }
213
        listBeans(jg, new ObjectName(splitStrings[0]), splitStrings[1],
214
            response);
215
        jg.close();
216
        return;
217
        
218
      }
219

    
220
        // query per mbean
221
        String qry = request.getParameter("qry");
222
        if (qry == null) {
223
          qry = "*:*";
224
        }
225
        listBeans(jg, new ObjectName(qry), null, response);
226
      } finally {
227
        if (jg != null) {
228
          jg.close();
229
        }
230
        if (writer != null) {
231
          writer.close();
232
        }
233
      }
234
    } catch ( IOException e ) {
235
      LOG.error("Caught an exception while processing JMX request", e);
236
      response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
237
    } catch ( MalformedObjectNameException e ) {
238
      LOG.error("Caught an exception while processing JMX request", e);
239
      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
240
    } finally {
241
      if (writer != null) {
242
        writer.close();
243
      }
244
    }
245
  }
246

    
247
  // --------------------------------------------------------- Private Methods
248
  private void listBeans(JsonGenerator jg, ObjectName qry, String attribute, 
249
      HttpServletResponse response) 
250
  throws IOException {
251
    LOG.debug("Listing beans for "+qry);
252
    Set<ObjectName> names = null;
253
    names = mBeanServer.queryNames(qry, null);
254

    
255
    jg.writeArrayFieldStart("beans");
256
    Iterator<ObjectName> it = names.iterator();
257
    while (it.hasNext()) {
258
      ObjectName oname = it.next();
259
      MBeanInfo minfo;
260
      String code = "";
261
      Object attributeinfo = null;
262
      try {
263
        minfo = mBeanServer.getMBeanInfo(oname);
264
        code = minfo.getClassName();
265
        String prs = "";
266
        try {
267
          if ("org.apache.commons.modeler.BaseModelMBean".equals(code)) {
268
            prs = "modelerType";
269
            code = (String) mBeanServer.getAttribute(oname, prs);
270
          }
271
          if (attribute!=null) {
272
            prs = attribute;
273
            attributeinfo = mBeanServer.getAttribute(oname, prs);
274
          }
275
        } catch (AttributeNotFoundException e) {
276
          // If the modelerType attribute was not found, the class name is used
277
          // instead.
278
          LOG.error("getting attribute " + prs + " of " + oname
279
              + " threw an exception", e);
280
        } catch (MBeanException e) {
281
          // The code inside the attribute getter threw an exception so log it,
282
          // and fall back on the class name
283
          LOG.error("getting attribute " + prs + " of " + oname
284
              + " threw an exception", e);
285
        } catch (RuntimeException e) {
286
          // For some reason even with an MBeanException available to them
287
          // Runtime exceptionscan still find their way through, so treat them
288
          // the same as MBeanException
289
          LOG.error("getting attribute " + prs + " of " + oname
290
              + " threw an exception", e);
291
        } catch ( ReflectionException e ) {
292
          // This happens when the code inside the JMX bean (setter?? from the
293
          // java docs) threw an exception, so log it and fall back on the 
294
          // class name
295
          LOG.error("getting attribute " + prs + " of " + oname
296
              + " threw an exception", e);
297
        }
298
      } catch (InstanceNotFoundException e) {
299
        //Ignored for some reason the bean was not found so don't output it
300
        continue;
301
      } catch ( IntrospectionException e ) {
302
        // This is an internal error, something odd happened with reflection so
303
        // log it and don't output the bean.
304
        LOG.error("Problem while trying to process JMX query: " + qry
305
            + " with MBean " + oname, e);
306
        continue;
307
      } catch ( ReflectionException e ) {
308
        // This happens when the code inside the JMX bean threw an exception, so
309
        // log it and don't output the bean.
310
        LOG.error("Problem while trying to process JMX query: " + qry
311
            + " with MBean " + oname, e);
312
        continue;
313
      }
314

    
315
      jg.writeStartObject();
316
      jg.writeStringField("name", oname.toString());
317
      
318
      jg.writeStringField("modelerType", code);
319
      if ((attribute != null) && (attributeinfo == null)) {
320
        jg.writeStringField("result", "ERROR");
321
        jg.writeStringField("message", "No attribute with name " + attribute
322
            + " was found.");
323
        jg.writeEndObject();
324
        jg.writeEndArray();
325
        jg.close();
326
        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
327
        return;
328
      }
329
      
330
      if (attribute != null) {
331
        writeAttribute(jg, attribute, attributeinfo);
332
      } else {
333
        MBeanAttributeInfo attrs[] = minfo.getAttributes();
334
        for (int i = 0; i < attrs.length; i++) {
335
          writeAttribute(jg, oname, attrs[i]);
336
        }
337
      }
338
      jg.writeEndObject();
339
    }
340
    jg.writeEndArray();
341
  }
342

    
343
  private void writeAttribute(JsonGenerator jg, ObjectName oname, MBeanAttributeInfo attr) throws IOException {
344
    if (!attr.isReadable()) {
345
      return;
346
    }
347
    String attName = attr.getName();
348
    if ("modelerType".equals(attName)) {
349
      return;
350
    }
351
    if (attName.indexOf("=") >= 0 || attName.indexOf(":") >= 0
352
        || attName.indexOf(" ") >= 0) {
353
      return;
354
    }
355
    Object value = null;
356
    try {
357
      value = mBeanServer.getAttribute(oname, attName);
358
    } catch (RuntimeMBeanException e) {
359
      // UnsupportedOperationExceptions happen in the normal course of business,
360
      // so no need to log them as errors all the time.
361
      if (e.getCause() instanceof UnsupportedOperationException) {
362
        LOG.debug("getting attribute "+attName+" of "+oname+" threw an exception", e);
363
      } else {
364
        LOG.error("getting attribute "+attName+" of "+oname+" threw an exception", e);
365
      }
366
      return;
367
    } catch (RuntimeErrorException e) {
368
      // RuntimeErrorException happens when an unexpected failure occurs in getAttribute
369
      // for example https://issues.apache.org/jira/browse/DAEMON-120
370
      LOG.debug("getting attribute "+attName+" of "+oname+" threw an exception", e);
371
      return;
372
    } catch (AttributeNotFoundException e) {
373
      //Ignored the attribute was not found, which should never happen because the bean
374
      //just told us that it has this attribute, but if this happens just don't output
375
      //the attribute.
376
      return;
377
    } catch (MBeanException e) {
378
      //The code inside the attribute getter threw an exception so log it, and
379
      // skip outputting the attribute
380
      LOG.error("getting attribute "+attName+" of "+oname+" threw an exception", e);
381
      return;
382
    } catch (RuntimeException e) {
383
      //For some reason even with an MBeanException available to them Runtime exceptions
384
      //can still find their way through, so treat them the same as MBeanException
385
      LOG.error("getting attribute "+attName+" of "+oname+" threw an exception", e);
386
      return;
387
    } catch (ReflectionException e) {
388
      //This happens when the code inside the JMX bean (setter?? from the java docs)
389
      //threw an exception, so log it and skip outputting the attribute
390
      LOG.error("getting attribute "+attName+" of "+oname+" threw an exception", e);
391
      return;
392
    } catch (InstanceNotFoundException e) {
393
      //Ignored the mbean itself was not found, which should never happen because we
394
      //just accessed it (perhaps something unregistered in-between) but if this
395
      //happens just don't output the attribute.
396
      return;
397
    }
398

    
399
    writeAttribute(jg, attName, value);
400
  }
401
  
402
  private void writeAttribute(JsonGenerator jg, String attName, Object value) throws IOException {
403
    jg.writeFieldName(attName);
404
    writeObject(jg, value);
405
  }
406
  
407
  private void writeObject(JsonGenerator jg, Object value) throws IOException {
408
    if(value == null) {
409
      jg.writeNull();
410
    } else {
411
      Class<?> c = value.getClass();
412
      if (c.isArray()) {
413
        jg.writeStartArray();
414
        int len = Array.getLength(value);
415
        for (int j = 0; j < len; j++) {
416
          Object item = Array.get(value, j);
417
          writeObject(jg, item);
418
        }
419
        jg.writeEndArray();
420
      } else if(value instanceof Number) {
421
        Number n = (Number)value;
422
        jg.writeNumber(n.toString());
423
      } else if(value instanceof Boolean) {
424
        Boolean b = (Boolean)value;
425
        jg.writeBoolean(b);
426
      } else if(value instanceof CompositeData) {
427
        CompositeData cds = (CompositeData)value;
428
        CompositeType comp = cds.getCompositeType();
429
        Set<String> keys = comp.keySet();
430
        jg.writeStartObject();
431
        for(String key: keys) {
432
          writeAttribute(jg, key, cds.get(key));
433
        }
434
        jg.writeEndObject();
435
      } else if(value instanceof TabularData) {
436
        TabularData tds = (TabularData)value;
437
        jg.writeStartArray();
438
        for(Object entry : tds.values()) {
439
          writeObject(jg, entry);
440
        }
441
        jg.writeEndArray();
442
      } else {
443
        jg.writeString(value.toString());
444
      }
445
    }
446
  }
447
}
    (1-1/1)