1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody;
19
20 import java.io.File;
21 import java.io.IOException;
22 import java.security.CodeSource;
23 import java.sql.Driver;
24 import java.sql.DriverManager;
25 import java.sql.SQLException;
26 import java.util.Arrays;
27 import java.util.Collections;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Set;
31 import java.util.Timer;
32 import java.util.TimerTask;
33 import java.util.regex.Pattern;
34
35 import javax.management.JMException;
36 import javax.management.MBeanServer;
37 import javax.management.ObjectName;
38
39 import net.bull.javamelody.internal.common.I18N;
40 import net.bull.javamelody.internal.common.LOG;
41 import net.bull.javamelody.internal.common.Parameters;
42 import net.bull.javamelody.internal.model.Collector;
43 import net.bull.javamelody.internal.model.Counter;
44 import net.bull.javamelody.internal.model.JRobin;
45 import net.bull.javamelody.internal.model.JobInformations;
46 import net.bull.javamelody.internal.model.MBeans;
47 import net.bull.javamelody.internal.model.MavenArtifact;
48 import net.bull.javamelody.internal.model.Period;
49 import net.bull.javamelody.internal.model.SamplingProfiler;
50 import net.bull.javamelody.internal.model.UpdateChecker;
51 import net.bull.javamelody.internal.web.HttpCookieManager;
52 import net.bull.javamelody.internal.web.MailReport;
53 import net.bull.javamelody.internal.web.MonitoringController;
54
55 /**
56  * Contexte du filtre http pour initialisation et destruction.
57  * @author Emeric Vernat
58  */

59 class FilterContext {
60     private static final boolean MOJARRA_AVAILABLE = isMojarraAvailable();
61     private static final boolean JPA2_AVAILABLE = isJpa2Available();
62
63     private final String applicationType;
64     private final Collector collector;
65     private final Timer timer;
66     private final SamplingProfiler samplingProfiler;
67     private final TimerTask collectTimerTask;
68     private final Set<ObjectName> jmxNames = new HashSet<>();
69
70     private static final class CollectTimerTask extends TimerTask {
71         private final Collector collector;
72
73         CollectTimerTask(Collector collector) {
74             super();
75             this.collector = collector;
76         }
77
78         /** {@inheritDoc} */
79         @Override
80         public void run() {
81             // il ne doit pas y avoir d'erreur dans cette task
82             collector.collectLocalContextWithoutErrors();
83         }
84     }
85
86     FilterContext(final String applicationType) {
87         super();
88         assert applicationType != null;
89         this.applicationType = applicationType;
90
91         boolean initOk = false;
92         this.timer = new Timer("javamelody"
93                 + Parameters.getContextPath(Parameters.getServletContext()).replace('/', ' '),
94                 true);
95         try {
96             logSystemInformationsAndParameters();
97
98             initLogs();
99
100             if (Parameter.CONTEXT_FACTORY_ENABLED.getValueAsBoolean()) {
101                 MonitoringInitialContextFactory.init();
102             }
103
104             // si l'application a utilisé JdbcDriver avant d'initialiser ce filtre
105             // (par exemple dans un listener de contexte), on doit récupérer son sqlCounter
106             // car il est lié à une connexion jdbc qui est certainement conservée dans un pool
107             // (sinon les requêtes sql sur cette connexion ne seront pas monitorées)
108             // sqlCounter dans JdbcWrapper peut être alimenté soit par une datasource soit par un driver
109             JdbcWrapper.SINGLETON.initServletContext(Parameters.getServletContext());
110             if (!Parameters.isNoDatabase()) {
111                 JdbcWrapper.SINGLETON.rebindDataSources();
112             } else {
113                 // si le paramètre no-database a été mis dans web.xml, des datasources jndi ont pu
114                 // être rebindées auparavant par SessionListener, donc on annule ce rebinding
115                 JdbcWrapper.SINGLETON.stop();
116             }
117
118             // initialisation du listener de jobs quartz
119             if (JobInformations.QUARTZ_AVAILABLE) {
120                 JobGlobalListener.initJobGlobalListener();
121             }
122
123             if (MOJARRA_AVAILABLE) {
124                 JsfActionHelper.initJsfActionListener();
125             }
126             if (JPA2_AVAILABLE) {
127                 JpaPersistence.initPersistenceProviderResolver();
128             }
129
130             this.samplingProfiler = initSamplingProfiler();
131
132             final List<Counter> counters = initCounters();
133             final String application = Parameters.getCurrentApplication();
134             this.collector = new Collector(application, counters, this.samplingProfiler);
135             this.collectTimerTask = new CollectTimerTask(collector);
136
137             initCollect();
138
139             if (Parameter.JMX_EXPOSE_ENABLED.getValueAsBoolean()) {
140                 initJmxExpose();
141             }
142
143             UpdateChecker.init(timer, collector, applicationType);
144
145             if (Parameters.getServletContext().getServerInfo().contains("Google App Engine")) {
146                 // https://issuetracker.google.com/issues/72216727
147                 final String fontConfig = System.getProperty("java.home")
148                         + "/lib/fontconfig.Prodimage.properties";
149                 if (new File(fontConfig).exists()) {
150                     System.setProperty("sun.awt.fontconfig", fontConfig);
151                 }
152             }
153
154             initOk = true;
155         } finally {
156             if (!initOk) {
157                 // si exception dans initialisation, on annule la création du timer
158                 // (sinon tomcat ne serait pas content)
159                 timer.cancel();
160                 LOG.debug("JavaMelody init failed");
161             }
162         }
163     }
164
165     private static List<Counter> initCounters() {
166         // liaison des compteurs : les contextes par thread du sqlCounter ont pour parent le httpCounter
167         final Counter sqlCounter = JdbcWrapper.SINGLETON.getSqlCounter();
168         final Counter httpCounter = new Counter(Counter.HTTP_COUNTER_NAME, "dbweb.png", sqlCounter);
169         final Counter errorCounter = new Counter(Counter.ERROR_COUNTER_NAME, "error.png");
170         errorCounter.setMaxRequestsCount(250);
171
172         final Counter jpaCounter = MonitoringProxy.getJpaCounter();
173         final Counter ejbCounter = MonitoringProxy.getEjbCounter();
174         final Counter springCounter = MonitoringProxy.getSpringCounter();
175         final Counter guiceCounter = MonitoringProxy.getGuiceCounter();
176         final Counter servicesCounter = MonitoringProxy.getServicesCounter();
177         final Counter strutsCounter = MonitoringProxy.getStrutsCounter();
178         final Counter jsfCounter = MonitoringProxy.getJsfCounter();
179         final Counter logCounter = LoggingHandler.getLogCounter();
180         final Counter jspCounter = JspWrapper.getJspCounter();
181         final List<Counter> counters;
182         if (JobInformations.QUARTZ_AVAILABLE) {
183             final Counter jobCounter = JobGlobalListener.getJobCounter();
184             counters = Arrays.asList(httpCounter, sqlCounter, jpaCounter, ejbCounter, springCounter,
185                     guiceCounter, servicesCounter, strutsCounter, jsfCounter, jspCounter,
186                     errorCounter, logCounter, jobCounter);
187         } else {
188             counters = Arrays.asList(httpCounter, sqlCounter, jpaCounter, ejbCounter, springCounter,
189                     guiceCounter, servicesCounter, strutsCounter, jsfCounter, jspCounter,
190                     errorCounter, logCounter);
191         }
192
193         setRequestTransformPatterns(counters);
194         final String displayedCounters = Parameter.DISPLAYED_COUNTERS.getValue();
195         if (displayedCounters == null) {
196             // par défaut, les compteurs http, sql, error et log (et ceux qui sont utilisés) sont affichés
197             httpCounter.setDisplayed(true);
198             sqlCounter.setDisplayed(!Parameters.isNoDatabase());
199             errorCounter.setDisplayed(true);
200             logCounter.setDisplayed(true);
201             jpaCounter.setDisplayed(jpaCounter.isUsed());
202             ejbCounter.setDisplayed(ejbCounter.isUsed());
203             springCounter.setDisplayed(springCounter.isUsed());
204             guiceCounter.setDisplayed(guiceCounter.isUsed());
205             servicesCounter.setDisplayed(servicesCounter.isUsed());
206             strutsCounter.setDisplayed(strutsCounter.isUsed());
207             jsfCounter.setDisplayed(jsfCounter.isUsed());
208             jspCounter.setDisplayed(jspCounter.isUsed());
209         } else {
210             setDisplayedCounters(counters, displayedCounters);
211         }
212         LOG.debug("counters initialized");
213         return counters;
214     }
215
216     private static void setRequestTransformPatterns(List<Counter> counters) {
217         for (final Counter counter : counters) {
218             // le paramètre pour ce nom de compteur doit exister
219             final Parameter parameter = Parameter
220                     .valueOfIgnoreCase(counter.getName() + "_TRANSFORM_PATTERN");
221             if (parameter.getValue() != null) {
222                 final Pattern pattern = Pattern.compile(parameter.getValue(),
223                         Pattern.MULTILINE | Pattern.DOTALL);
224                 counter.setRequestTransformPattern(pattern);
225             }
226         }
227     }
228
229     private static void setDisplayedCounters(List<Counter> counters, String displayedCounters) {
230         for (final Counter counter : counters) {
231             // le compteur "job" a toujours displayed=true s'il est présent,
232             // même s'il n'est pas dans la liste des "displayedCounters"
233             counter.setDisplayed(counter.isJobCounter());
234         }
235         if (!displayedCounters.isEmpty()) {
236             for (final String displayedCounter : displayedCounters.split(",")) {
237                 final String displayedCounterName = displayedCounter.trim();
238                 boolean found = false;
239                 for (final Counter counter : counters) {
240                     if (displayedCounterName.equalsIgnoreCase(counter.getName())) {
241                         counter.setDisplayed(true);
242                         found = true;
243                         break;
244                     }
245                 }
246                 if (!found) {
247                     throw new IllegalArgumentException("Unknown counter: " + displayedCounterName);
248                 }
249             }
250         }
251     }
252
253     private void initCollect() {
254         try {
255             Class.forName("org.jrobin.core.RrdDb");
256             // il a parfois été observé "ClassNotFoundException: org.jrobin.core.RrdException"
257             // dans tomcat lors de l'arrêt du serveur à l'appel de JRobin.stop()
258             Class.forName("org.jrobin.core.RrdException");
259         } catch (final ClassNotFoundException e) {
260             LOG.debug("jrobin classes unavailable: collect of data is disabled");
261             HttpCookieManager.setDefaultRange(Period.TOUT.getRange());
262             // si pas de jar jrobin, alors pas de collecte et période "Tout" par défaut
263             return;
264         }
265
266         if (collector.isStorageUsedByMultipleInstances()) {
267             LOG.info(I18N.getString("storage_used_by_multiple_instances"));
268         }
269
270         try {
271             JRobin.initBackendFactory(timer);
272         } catch (final IOException e) {
273             LOG.warn(e.toString(), e);
274         }
275         final int resolutionSeconds = Parameters.getResolutionSeconds();
276         final int periodMillis = resolutionSeconds * 1000;
277         // on schedule la tâche de fond
278         timer.schedule(collectTimerTask, periodMillis, periodMillis);
279         LOG.debug("collect task scheduled every " + resolutionSeconds + 's');
280
281         // on appelle la collecte pour que les instances jrobin soient définies
282         // au cas où un graph de la page de monitoring soit demandé de suite
283         collector.collectLocalContextWithoutErrors();
284         LOG.debug("first collect of data done");
285
286         if (Parameter.MAIL_SESSION.getValue() != null
287                 && Parameter.ADMIN_EMAILS.getValue() != null) {
288             MailReport.scheduleReportMailForLocalServer(collector, timer);
289             LOG.debug("mail reports scheduled for " + Parameter.ADMIN_EMAILS.getValue());
290         }
291     }
292
293     private SamplingProfiler initSamplingProfiler() {
294         if (Parameter.SAMPLING_SECONDS.getValue() != null) {
295             final SamplingProfiler sampler;
296             final String excludedPackagesParameter = Parameter.SAMPLING_EXCLUDED_PACKAGES
297                     .getValue();
298             final String includedPackagesParameter = Parameter.SAMPLING_INCLUDED_PACKAGES
299                     .getValue();
300             if (excludedPackagesParameter == null && includedPackagesParameter == null) {
301                 sampler = new SamplingProfiler();
302             } else {
303                 sampler = new SamplingProfiler(excludedPackagesParameter,
304                         includedPackagesParameter);
305             }
306             final TimerTask samplingTimerTask = new TimerTask() {
307                 @Override
308                 public void run() {
309                     sampler.update();
310                 }
311             };
312             final long periodInMillis = Math
313                     .round(Double.parseDouble(Parameter.SAMPLING_SECONDS.getValue()) * 1000);
314             this.timer.schedule(samplingTimerTask, 10000, periodInMillis);
315             LOG.debug("hotspots sampling initialized");
316
317             return sampler;
318         }
319         return null;
320     }
321
322     private static void initLogs() {
323         // on branche le handler java.util.logging pour le counter de logs
324         LoggingHandler.getSingleton().register();
325
326         if (LOG.LOG4J_ENABLED) {
327             // si log4j est disponible on branche aussi l'appender pour le counter de logs
328             Log4JAppender.getSingleton().register();
329         }
330
331         if (LOG.LOG4J2_ENABLED) {
332             // si log4j2 est disponible on branche aussi l'appender pour le counter de logs
333             Log4J2Appender.getSingleton().register();
334         }
335
336         if (LOG.LOGBACK_ENABLED) {
337             // si logback est disponible on branche aussi l'appender pour le counter de logs
338             LogbackAppender.getSingleton().register();
339         }
340         LOG.debug("log listeners initialized");
341     }
342
343     private static boolean isMojarraAvailable() {
344         try {
345             Class.forName("com.sun.faces.application.ActionListenerImpl");
346             return true;
347         } catch (final Throwable e) { // NOPMD
348             return false;
349         }
350     }
351
352     private static boolean isJpa2Available() {
353         try {
354             Class.forName("javax.persistence.spi.PersistenceProviderResolverHolder");
355             return true;
356         } catch (final Throwable e) { // NOPMD
357             return false;
358         }
359     }
360
361     private void logSystemInformationsAndParameters() {
362         // log les principales informations sur le système et sur les paramètres définis spécifiquement
363         LOG.debug("OS: " + System.getProperty("os.name") + ' '
364                 + System.getProperty("sun.os.patch.level") + ", " + System.getProperty("os.arch")
365                 + '/' + System.getProperty("sun.arch.data.model"));
366         LOG.debug("Java: " + System.getProperty("java.runtime.name") + ", "
367                 + System.getProperty("java.runtime.version"));
368         LOG.debug("Server: " + Parameters.getServletContext().getServerInfo());
369         LOG.debug("Webapp context: " + Parameters.getContextPath(Parameters.getServletContext()));
370         LOG.debug("JavaMelody version: " + Parameters.JAVAMELODY_VERSION);
371         final String location = getJavaMelodyLocation();
372         if (location != null) {
373             LOG.debug("JavaMelody classes loaded from: " + location);
374         }
375         LOG.debug("Application type: " + applicationType);
376         LOG.debug("Application version: " + MavenArtifact.getWebappVersion());
377         LOG.debug("Host: " + Parameters.getHostName() + '@' + Parameters.getHostAddress());
378         for (final Parameter parameter : Parameter.values()) {
379             final String value = parameter.getValue();
380             if (value != null && parameter != Parameter.ANALYTICS_ID) {
381                 if (parameter == Parameter.AUTHORIZED_USERS) {
382                     LOG.debug("parameter defined: " + Parameter.AUTHORIZED_USERS.getCode()
383                             + "=*****");
384                 } else {
385                     LOG.debug("parameter defined: " + parameter.getCode() + '=' + value);
386                 }
387             }
388         }
389     }
390
391     private static String getJavaMelodyLocation() {
392         final Class<FilterContext> clazz = FilterContext.class;
393         final CodeSource codeSource = clazz.getProtectionDomain().getCodeSource();
394         if (codeSource != null && codeSource.getLocation() != null) {
395             String location = codeSource.getLocation().toString();
396             // location contient le nom du fichier jar
397             // (ou le nom du fichier de cette classe s'il y a un répertoire sans jar)
398             final String clazzFileName = clazz.getSimpleName() + ".class";
399             if (location.endsWith(clazzFileName)) {
400                 location = location.substring(0, location.length() - clazzFileName.length());
401             }
402             return location;
403         }
404         return null;
405     }
406
407     /**
408      * Registers CounterRequestMXBean beans for each of the enabled counters.
409      * The beans are registered under "net.bull.javamelody:type=CounterRequest,context=<webapp>,name=<counter name>" names.
410      * @author Alexey Pushkin
411      */

412     private void initJmxExpose() {
413         final String packageName = getClass().getName().substring(0,
414                 getClass().getName().length() - getClass().getSimpleName().length() - 1);
415         String webapp = Parameters.getContextPath(Parameters.getServletContext());
416         if (webapp.length() >= 1 && webapp.charAt(0) == '/') {
417             webapp = webapp.substring(1);
418         }
419         final List<Counter> counters = collector.getCounters();
420         final MBeanServer platformMBeanServer = MBeans.getPlatformMBeanServer();
421         try {
422             for (final Counter counter : counters) {
423                 if (!Parameters.isCounterHidden(counter.getName())) {
424                     final CounterRequestMXBean.CounterRequestMXBeanImpl mxBean = new CounterRequestMXBean.CounterRequestMXBeanImpl(
425                             counter);
426                     final ObjectName name = new ObjectName(
427                             packageName + ":type=CounterRequest,context=" + webapp + ",name="
428                                     + counter.getName());
429                     platformMBeanServer.registerMBean(mxBean, name);
430                     jmxNames.add(name);
431                 }
432             }
433             LOG.debug("JMX mbeans registered");
434         } catch (final JMException e) {
435             LOG.warn("failed to register JMX mbeans", e);
436         }
437     }
438
439     void stopCollector() {
440         // cette méthode est appelée par MonitoringFilter lorsqu'il y a un serveur de collecte
441         if (collectTimerTask != null) {
442             // on arrête juste la tâche de collecte, mais pas le timer, ni la tâche d'UpdateChecker ni la tâche de sampling
443             collectTimerTask.cancel();
444         }
445         // arrêt du collector
446         collector.stop();
447     }
448
449     void destroy() {
450         try {
451             try {
452                 if (collector != null) {
453                     new MonitoringController(collector, null).writeHtmlToLastShutdownFile();
454                 }
455             } finally {
456                 //on rebind les dataSources initiales à la place des proxy
457                 JdbcWrapper.SINGLETON.stop();
458
459                 deregisterJdbcDriver();
460
461                 // on enlève l'appender de logback, log4j et le handler de java.util.logging
462                 deregisterLogs();
463
464                 // on enlève le listener de jobs quartz
465                 if (JobInformations.QUARTZ_AVAILABLE) {
466                     JobGlobalListener.destroyJobGlobalListener();
467                 }
468
469                 unregisterJmxExpose();
470             }
471         } finally {
472             MonitoringInitialContextFactory.stop();
473
474             // on arrête le thread du collector,
475             // on persiste les compteurs pour les relire à l'initialisation et ne pas perdre les stats
476             // et on vide les compteurs
477             if (timer != null) {
478                 timer.cancel();
479             }
480             if (samplingProfiler != null) {
481                 samplingProfiler.clear();
482             }
483             if (collector != null) {
484                 collector.stop();
485             }
486             Collector.stopJRobin();
487             Collector.detachVirtualMachine();
488         }
489     }
490
491     private static void deregisterJdbcDriver() {
492         // on désinstalle le driver jdbc s'il est installé
493         // (mais sans charger la classe JdbcDriver pour ne pas installer le driver)
494         final Class<FilterContext> classe = FilterContext.class;
495         final String packageName = classe.getName().substring(0,
496                 classe.getName().length() - classe.getSimpleName().length() - 1);
497         for (final Driver driver : Collections.list(DriverManager.getDrivers())) {
498             if (driver.getClass().getName().startsWith(packageName)) {
499                 try {
500                     DriverManager.deregisterDriver(driver);
501                 } catch (final SQLException e) {
502                     // ne peut arriver
503                     throw new IllegalStateException(e);
504                 }
505             }
506         }
507     }
508
509     private static void deregisterLogs() {
510         if (LOG.LOGBACK_ENABLED) {
511             LogbackAppender.getSingleton().deregister();
512         }
513         if (LOG.LOG4J_ENABLED) {
514             Log4JAppender.getSingleton().deregister();
515         }
516         LoggingHandler.getSingleton().deregister();
517     }
518
519     /**
520      * Unregisters CounterRequestMXBean beans.
521      */

522     private void unregisterJmxExpose() {
523         if (jmxNames.isEmpty()) {
524             return;
525         }
526         try {
527             final MBeanServer platformMBeanServer = MBeans.getPlatformMBeanServer();
528             for (final ObjectName name : jmxNames) {
529                 platformMBeanServer.unregisterMBean(name);
530             }
531         } catch (final JMException e) {
532             LOG.warn("failed to unregister JMX beans", e);
533         }
534     }
535
536     Collector getCollector() {
537         return collector;
538     }
539
540     Timer getTimer() {
541         return timer;
542     }
543 }
544