View Javadoc
1   /*
2    * Licensed under the GPL License. You may not use this file except in compliance with the License.
3    * You may obtain a copy of the License at
4    *
5    *   https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
6    *
7    * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
8    * WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
9    * PURPOSE.
10   */
11  package psiprobe.beans;
12  
13  import com.maxmind.db.CHMCache;
14  import com.maxmind.geoip2.DatabaseReader;
15  import com.maxmind.geoip2.exception.AddressNotFoundException;
16  import com.maxmind.geoip2.model.CountryResponse;
17  import com.maxmind.geoip2.record.Country;
18  
19  import java.io.File;
20  import java.net.InetAddress;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Locale;
26  import java.util.Set;
27  
28  import javax.inject.Inject;
29  import javax.management.InstanceNotFoundException;
30  import javax.management.MBeanServer;
31  import javax.management.MBeanServerNotification;
32  import javax.management.MalformedObjectNameException;
33  import javax.management.Notification;
34  import javax.management.NotificationListener;
35  import javax.management.ObjectInstance;
36  import javax.management.ObjectName;
37  import javax.management.RuntimeOperationsException;
38  
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  import psiprobe.model.Connector;
43  import psiprobe.model.RequestProcessor;
44  import psiprobe.model.ThreadPool;
45  import psiprobe.model.jmx.ThreadPoolObjectName;
46  import psiprobe.tools.JmxTools;
47  
48  /**
49   * This class interfaces Tomcat JMX functionality to read connection status. The class essentially
50   * provides and maintains the list of connection ThreadPools.
51   */
52  public class ContainerListenerBean implements NotificationListener {
53  
54    /** The Constant logger. */
55    private static final Logger logger = LoggerFactory.getLogger(ContainerListenerBean.class);
56  
57    /** The allowed operation. */
58    private final Set<String> allowedOperation =
59        new HashSet<>(Arrays.asList("start", "stop", "pause", "resume"));
60  
61    /** The pool names. */
62    private List<ThreadPoolObjectName> poolNames;
63  
64    /** The executor names. */
65    private List<ObjectName> executorNames;
66  
67    /** Used to obtain required {@link MBeanServer} instance. */
68    @Inject
69    private ContainerWrapperBean containerWrapper;
70  
71    /**
72     * Gets the container wrapper.
73     *
74     * @return the container wrapper
75     */
76    public ContainerWrapperBean getContainerWrapper() {
77      return containerWrapper;
78    }
79  
80    /**
81     * Sets the container wrapper.
82     *
83     * @param containerWrapper the new container wrapper
84     */
85    public void setContainerWrapper(ContainerWrapperBean containerWrapper) {
86      this.containerWrapper = containerWrapper;
87    }
88  
89    /**
90     * Checks if is initialized.
91     *
92     * @return true, if is initialized
93     */
94    private boolean isInitialized() {
95      return poolNames != null && !poolNames.isEmpty();
96    }
97  
98    /**
99     * Finds ThreadPoolObjectName by its string name.
100    *
101    * @param name - pool name
102    *
103    * @return null if the input name is null or ThreadPoolObjectName is not found
104    */
105   private ThreadPoolObjectName findPool(String name) {
106     if (name != null && isInitialized()) {
107       for (ThreadPoolObjectName threadPoolObjectName : poolNames) {
108         if (name.equals(threadPoolObjectName.getThreadPoolName().getKeyProperty("name"))) {
109           return threadPoolObjectName;
110         }
111       }
112     }
113     return null;
114   }
115 
116   /**
117    * Handles creation and deletion of new "worker" threads.
118    *
119    * @param notification the notification
120    * @param object the object
121    */
122   @Override
123   public synchronized void handleNotification(Notification notification, Object object) {
124     if (!(notification instanceof MBeanServerNotification)) {
125       return;
126     }
127 
128     if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notification.getType())
129         || MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(notification.getType())) {
130 
131       ObjectName objectName = ((MBeanServerNotification) notification).getMBeanName();
132       if ("RequestProcessor".equals(objectName.getKeyProperty("type"))) {
133         ThreadPoolObjectName threadPoolObjectName = findPool(objectName.getKeyProperty("worker"));
134         if (threadPoolObjectName != null) {
135           if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notification.getType())) {
136             threadPoolObjectName.getRequestProcessorNames().add(objectName);
137           } else {
138             threadPoolObjectName.getRequestProcessorNames().remove(objectName);
139           }
140         }
141       }
142     }
143   }
144 
145   /**
146    * Load ObjectNames for the relevant MBeans so they can be queried at a later stage without
147    * searching MBean server over and over again.
148    *
149    * @throws MalformedObjectNameException the malformed object name exception
150    * @throws InstanceNotFoundException the instance not found exception
151    */
152   private synchronized void initialize()
153       throws MalformedObjectNameException, InstanceNotFoundException {
154 
155     MBeanServer server = getContainerWrapper().getResourceResolver().getMBeanServer();
156     String serverName = getContainerWrapper().getTomcatContainer().getName();
157     Set<ObjectInstance> threadPools =
158         server.queryMBeans(new ObjectName(serverName + ":type=ThreadPool,name=\"*\""), null);
159     poolNames = new ArrayList<>(threadPools.size());
160     for (ObjectInstance threadPool : threadPools) {
161 
162       ThreadPoolObjectName threadPoolObjectName = new ThreadPoolObjectName();
163       ObjectName threadPoolName = threadPool.getObjectName();
164 
165       String name = threadPoolName.getKeyProperty("name");
166 
167       threadPoolObjectName.setThreadPoolName(threadPoolName);
168       ObjectName grpName = server
169           .getObjectInstance(new ObjectName(
170               threadPoolName.getDomain() + ":type=GlobalRequestProcessor,name=" + name))
171           .getObjectName();
172       threadPoolObjectName.setGlobalRequestProcessorName(grpName);
173 
174       /*
175        * unfortunately exact workers could not be found at the time of testing so we filter out the
176        * relevant workers within the loop
177        */
178       Set<ObjectInstance> workers = server.queryMBeans(
179           new ObjectName(threadPoolName.getDomain() + ":type=RequestProcessor,*"), null);
180 
181       for (ObjectInstance worker : workers) {
182         ObjectName wrkName = worker.getObjectName();
183         if (name.equals(wrkName.getKeyProperty("worker"))) {
184           threadPoolObjectName.getRequestProcessorNames().add(wrkName);
185         }
186       }
187 
188       poolNames.add(threadPoolObjectName);
189     }
190 
191     Set<ObjectInstance> executors =
192         server.queryMBeans(new ObjectName(serverName + ":type=Executor,*"), null);
193     executorNames = new ArrayList<>(executors.size());
194     for (ObjectInstance executor : executors) {
195       ObjectName executorName = executor.getObjectName();
196       executorNames.add(executorName);
197     }
198 
199     // Register with MBean server
200     server.addNotificationListener(new ObjectName("JMImplementation:type=MBeanServerDelegate"),
201         this, null, null);
202 
203   }
204 
205   /**
206    * Gets the thread pools.
207    *
208    * @return the thread pools
209    *
210    * @throws Exception the exception
211    */
212   public synchronized List<ThreadPool> getThreadPools() throws Exception {
213     if (!isInitialized()) {
214       initialize();
215     }
216 
217     List<ThreadPool> threadPools = new ArrayList<>(poolNames.size());
218 
219     MBeanServer server = getContainerWrapper().getResourceResolver().getMBeanServer();
220 
221     for (ObjectName executorName : executorNames) {
222       ThreadPool threadPool = new ThreadPool();
223       threadPool.setName(executorName.getKeyProperty("name"));
224       threadPool.setMaxThreads(JmxTools.getIntAttr(server, executorName, "maxThreads"));
225       threadPool.setMaxSpareThreads(JmxTools.getIntAttr(server, executorName, "largestPoolSize"));
226       threadPool.setMinSpareThreads(JmxTools.getIntAttr(server, executorName, "minSpareThreads"));
227       threadPool.setCurrentThreadsBusy(JmxTools.getIntAttr(server, executorName, "activeCount"));
228       threadPool.setCurrentThreadCount(JmxTools.getIntAttr(server, executorName, "poolSize"));
229       threadPools.add(threadPool);
230     }
231 
232     for (ThreadPoolObjectName threadPoolObjectName : poolNames) {
233       ObjectName poolName = threadPoolObjectName.getThreadPoolName();
234 
235       ThreadPool threadPool = new ThreadPool();
236       threadPool.setName(poolName.getKeyProperty("name"));
237       threadPool.setMaxThreads(JmxTools.getIntAttr(server, poolName, "maxThreads"));
238 
239       if (JmxTools.hasAttribute(server, poolName, "maxSpareThreads")) {
240         threadPool.setMaxSpareThreads(JmxTools.getIntAttr(server, poolName, "maxSpareThreads"));
241         threadPool.setMinSpareThreads(JmxTools.getIntAttr(server, poolName, "minSpareThreads"));
242       }
243 
244       threadPool.setCurrentThreadsBusy(JmxTools.getIntAttr(server, poolName, "currentThreadsBusy"));
245       threadPool.setCurrentThreadCount(JmxTools.getIntAttr(server, poolName, "currentThreadCount"));
246 
247       /*
248        * Tomcat will return -1 for maxThreads if the connector uses an executor for its threads. In
249        * this case, don't add its ThreadPool to the results.
250        */
251       if (threadPool.getMaxThreads() > -1) {
252         threadPools.add(threadPool);
253       }
254     }
255     return threadPools;
256   }
257 
258   /**
259    * Toggle connector status.
260    *
261    * @param operation the operation
262    * @param port the port
263    *
264    * @throws Exception the exception
265    */
266   public synchronized void toggleConnectorStatus(String operation, String port) throws Exception {
267 
268     if (!allowedOperation.contains(operation)) {
269       logger.error("operation {} not supported", operation);
270       throw new IllegalArgumentException("Not support operation");
271     }
272 
273     ObjectName objectName = new ObjectName("Catalina:type=Connector,port=" + port);
274 
275     MBeanServer server = getContainerWrapper().getResourceResolver().getMBeanServer();
276 
277     JmxTools.invoke(server, objectName, operation, null, null);
278 
279     logger.info("operation {} on Connector {} invoked success", operation, objectName);
280   }
281 
282   /**
283    * Gets the connectors.
284    *
285    * @param includeRequestProcessors the include request processors
286    *
287    * @return the connectors
288    *
289    * @throws Exception the exception
290    */
291   public synchronized List<Connector> getConnectors(boolean includeRequestProcessors)
292       throws Exception {
293 
294     boolean workerThreadNameSupported = true;
295 
296     if (!isInitialized()) {
297       initialize();
298     }
299 
300     List<Connector> connectors = new ArrayList<>(poolNames.size());
301 
302     MBeanServer server = getContainerWrapper().getResourceResolver().getMBeanServer();
303 
304     for (ThreadPoolObjectName threadPoolObjectName : poolNames) {
305       ObjectName poolName = threadPoolObjectName.getThreadPoolName();
306 
307       Connector connector = new Connector();
308 
309       String name = poolName.getKeyProperty("name");
310 
311       connector.setProtocolHandler(poolName.getKeyProperty("name"));
312 
313       if (name.startsWith("\"") && name.endsWith("\"")) {
314         name = name.substring(1, name.length() - 1);
315       }
316 
317       String[] arr = name.split("-", -1);
318       String port = "-1";
319       if (arr.length == 3) {
320         port = arr[2];
321       }
322 
323       if (!"-1".equals(port)) {
324         String str = "Catalina:type=Connector,port=" + port;
325 
326         ObjectName objectName = new ObjectName(str);
327 
328         // add some useful information for connector list
329         connector.setStatus(JmxTools.getStringAttr(server, objectName, "stateName"));
330         connector.setProtocol(JmxTools.getStringAttr(server, objectName, "protocol"));
331         connector
332             .setSecure(Boolean.parseBoolean(JmxTools.getStringAttr(server, objectName, "secure")));
333         connector.setPort(JmxTools.getIntAttr(server, objectName, "port"));
334         connector.setLocalPort(JmxTools.getIntAttr(server, objectName, "localPort"));
335         connector.setSchema(JmxTools.getStringAttr(server, objectName, "schema"));
336       }
337 
338       ObjectName grpName = threadPoolObjectName.getGlobalRequestProcessorName();
339 
340       connector.setMaxTime(JmxTools.getLongAttr(server, grpName, "maxTime"));
341       connector.setProcessingTime(JmxTools.getLongAttr(server, grpName, "processingTime"));
342       connector.setBytesReceived(JmxTools.getLongAttr(server, grpName, "bytesReceived"));
343       connector.setBytesSent(JmxTools.getLongAttr(server, grpName, "bytesSent"));
344       connector.setRequestCount(JmxTools.getIntAttr(server, grpName, "requestCount"));
345       connector.setErrorCount(JmxTools.getIntAttr(server, grpName, "errorCount"));
346 
347       if (includeRequestProcessors) {
348         List<ObjectName> wrkNames = threadPoolObjectName.getRequestProcessorNames();
349         for (ObjectName wrkName : wrkNames) {
350           RequestProcessor rp = new RequestProcessor();
351           rp.setName(wrkName.getKeyProperty("name"));
352           rp.setStage(JmxTools.getIntAttr(server, wrkName, "stage"));
353           rp.setProcessingTime(JmxTools.getLongAttr(server, wrkName, "requestProcessingTime"));
354           rp.setBytesSent(JmxTools.getLongAttr(server, wrkName, "requestBytesSent"));
355           rp.setBytesReceived(JmxTools.getLongAttr(server, wrkName, "requestBytesReceived"));
356           try {
357             rp.setRemoteAddr(JmxTools.getStringAttr(server, wrkName, "remoteAddr"));
358           } catch (RuntimeOperationsException ex) {
359             logger.trace("", ex);
360           }
361 
362           if (rp.getRemoteAddr() != null) {
363             // Show flag as defined in jvm for localhost
364             if (InetAddress.getByName(rp.getRemoteAddr()).isLoopbackAddress()) {
365               rp.setRemoteAddrLocale(new Locale(System.getProperty("user.language"),
366                   System.getProperty("user.country")));
367             } else {
368               // Show flag for non-localhost using geo lite
369               try (DatabaseReader reader = new DatabaseReader.Builder(new File(
370                   getClass().getClassLoader().getResource("GeoLite2-Country.mmdb").toURI()))
371                   .withCache(new CHMCache()).build()) {
372                 CountryResponse response =
373                     reader.country(InetAddress.getByName(rp.getRemoteAddr()));
374                 Country country = response.getCountry();
375                 rp.setRemoteAddrLocale(new Locale("", country.getIsoCode()));
376               } catch (AddressNotFoundException e) {
377                 logger.debug("Address Not Found: {}", e.getMessage());
378                 logger.trace("", e);
379               }
380             }
381           }
382 
383           rp.setVirtualHost(JmxTools.getStringAttr(server, wrkName, "virtualHost"));
384           rp.setMethod(JmxTools.getStringAttr(server, wrkName, "method"));
385           rp.setCurrentUri(JmxTools.getStringAttr(server, wrkName, "currentUri"));
386           rp.setCurrentQueryString(JmxTools.getStringAttr(server, wrkName, "currentQueryString"));
387           rp.setProtocol(JmxTools.getStringAttr(server, wrkName, "protocol"));
388 
389           // Relies on https://issues.apache.org/bugzilla/show_bug.cgi?id=41128
390           if (workerThreadNameSupported
391               && JmxTools.hasAttribute(server, wrkName, "workerThreadName")) {
392 
393             rp.setWorkerThreadName(JmxTools.getStringAttr(server, wrkName, "workerThreadName"));
394             rp.setWorkerThreadNameSupported(true);
395           } else {
396             /*
397              * attribute should consistently either exist or be missing across all the workers so it
398              * does not make sense to check attribute existence if we have found once that it is not
399              * supported
400              */
401             rp.setWorkerThreadNameSupported(false);
402             workerThreadNameSupported = false;
403           }
404           connector.addRequestProcessor(rp);
405         }
406       }
407 
408       connectors.add(connector);
409     }
410     return connectors;
411   }
412 
413 }