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