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