/*
* Copyright 2008-2017 by Emeric Vernat
*
* This file is part of Java Melody.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.bull.javamelody;
import java.io.IOException;
import java.security.CodeSource;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Pattern;
import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
/**
* Contexte du filtre http pour initialisation et destruction.
* @author Emeric Vernat
*/
class FilterContext {
private static final boolean MOJARRA_AVAILABLE = isMojarraAvailable();
private final String applicationType;
private final Collector collector;
private final Timer timer;
private final SamplingProfiler samplingProfiler;
private final TimerTask collectTimerTask;
private final Set<ObjectName> jmxNames = new HashSet<ObjectName>();
private static final class CollectTimerTask extends TimerTask {
private final Collector collector;
CollectTimerTask(Collector collector) {
super();
this.collector = collector;
}
/** {@inheritDoc} */
@Override
public void run() {
// il ne doit pas y avoir d'erreur dans cette task
collector.collectLocalContextWithoutErrors();
}
}
FilterContext(final String applicationType) {
super();
assert applicationType != null;
this.applicationType = applicationType;
boolean initOk = false;
this.timer = new Timer("javamelody"
+ Parameters.getContextPath(Parameters.getServletContext()).replace('/', ' '),
true);
try {
logSystemInformationsAndParameters();
initLogs();
if (Boolean.parseBoolean(Parameters.getParameter(Parameter.CONTEXT_FACTORY_ENABLED))) {
MonitoringInitialContextFactory.init();
}
// si l'application a utilisé JdbcDriver avant d'initialiser ce filtre
// (par exemple dans un listener de contexte), on doit récupérer son sqlCounter
// car il est lié à une connexion jdbc qui est certainement conservée dans un pool
// (sinon les requêtes sql sur cette connexion ne seront pas monitorées)
// sqlCounter dans JdbcWrapper peut être alimenté soit par une datasource soit par un driver
JdbcWrapper.SINGLETON.initServletContext(Parameters.getServletContext());
if (!Parameters.isNoDatabase()) {
JdbcWrapper.SINGLETON.rebindDataSources();
} else {
// si le paramètre no-database a été mis dans web.xml, des datasources jndi ont pu
// être rebindées auparavant par SessionListener, donc on annule ce rebinding
JdbcWrapper.SINGLETON.stop();
}
// initialisation du listener de jobs quartz
if (JobInformations.QUARTZ_AVAILABLE) {
JobGlobalListener.initJobGlobalListener();
}
if (MOJARRA_AVAILABLE) {
JsfActionHelper.initJsfActionListener();
}
this.samplingProfiler = initSamplingProfiler();
final List<Counter> counters = initCounters();
final String application = Parameters.getCurrentApplication();
this.collector = new Collector(application, counters, this.samplingProfiler);
this.collectTimerTask = new CollectTimerTask(collector);
initCollect();
if (Boolean.parseBoolean(Parameters.getParameter(Parameter.JMX_EXPOSE_ENABLED))) {
initJmxExpose();
}
UpdateChecker.init(timer, collector, applicationType);
initOk = true;
} finally {
if (!initOk) {
// si exception dans initialisation, on annule la création du timer
// (sinon tomcat ne serait pas content)
timer.cancel();
LOG.debug("JavaMelody init failed");
}
}
}
private static List<Counter> initCounters() {
// liaison des compteurs : les contextes par thread du sqlCounter ont pour parent le httpCounter
final Counter sqlCounter = JdbcWrapper.SINGLETON.getSqlCounter();
final Counter httpCounter = new Counter(Counter.HTTP_COUNTER_NAME, "dbweb.png", sqlCounter);
final Counter errorCounter = new Counter(Counter.ERROR_COUNTER_NAME, "error.png");
errorCounter.setMaxRequestsCount(250);
final Counter jpaCounter = MonitoringProxy.getJpaCounter();
final Counter ejbCounter = MonitoringProxy.getEjbCounter();
final Counter springCounter = MonitoringProxy.getSpringCounter();
final Counter guiceCounter = MonitoringProxy.getGuiceCounter();
final Counter servicesCounter = MonitoringProxy.getServicesCounter();
final Counter strutsCounter = MonitoringProxy.getStrutsCounter();
final Counter jsfCounter = MonitoringProxy.getJsfCounter();
final Counter logCounter = LoggingHandler.getLogCounter();
final Counter jspCounter = JspWrapper.getJspCounter();
final List<Counter> counters;
if (JobInformations.QUARTZ_AVAILABLE) {
final Counter jobCounter = JobGlobalListener.getJobCounter();
counters = Arrays.asList(httpCounter, sqlCounter, jpaCounter, ejbCounter, springCounter,
guiceCounter, servicesCounter, strutsCounter, jsfCounter, jspCounter,
errorCounter, logCounter, jobCounter);
} else {
counters = Arrays.asList(httpCounter, sqlCounter, jpaCounter, ejbCounter, springCounter,
guiceCounter, servicesCounter, strutsCounter, jsfCounter, jspCounter,
errorCounter, logCounter);
}
setRequestTransformPatterns(counters);
final String displayedCounters = Parameters.getParameter(Parameter.DISPLAYED_COUNTERS);
if (displayedCounters == null) {
// par défaut, les compteurs http, sql, error et log (et ceux qui sont utilisés) sont affichés
httpCounter.setDisplayed(true);
sqlCounter.setDisplayed(!Parameters.isNoDatabase());
errorCounter.setDisplayed(true);
logCounter.setDisplayed(true);
jpaCounter.setDisplayed(jpaCounter.isUsed());
ejbCounter.setDisplayed(ejbCounter.isUsed());
springCounter.setDisplayed(springCounter.isUsed());
guiceCounter.setDisplayed(guiceCounter.isUsed());
servicesCounter.setDisplayed(servicesCounter.isUsed());
strutsCounter.setDisplayed(strutsCounter.isUsed());
jsfCounter.setDisplayed(jsfCounter.isUsed());
jspCounter.setDisplayed(jspCounter.isUsed());
} else {
setDisplayedCounters(counters, displayedCounters);
}
LOG.debug("counters initialized");
return counters;
}
private static void setRequestTransformPatterns(List<Counter> counters) {
for (final Counter counter : counters) {
// le paramètre pour ce nom de compteur doit exister
final Parameter parameter = Parameter
.valueOfIgnoreCase(counter.getName() + "_TRANSFORM_PATTERN");
if (Parameters.getParameter(parameter) != null) {
final Pattern pattern = Pattern.compile(Parameters.getParameter(parameter),
Pattern.MULTILINE | Pattern.DOTALL);
counter.setRequestTransformPattern(pattern);
}
}
}
private static void setDisplayedCounters(List<Counter> counters, String displayedCounters) {
for (final Counter counter : counters) {
if (counter.isJobCounter()) {
// le compteur "job" a toujours displayed=true s'il est présent,
// même s'il n'est pas dans la liste des "displayedCounters"
counter.setDisplayed(true);
} else {
counter.setDisplayed(false);
}
}
if (!displayedCounters.isEmpty()) {
for (final String displayedCounter : displayedCounters.split(",")) {
final String displayedCounterName = displayedCounter.trim();
boolean found = false;
for (final Counter counter : counters) {
if (displayedCounterName.equalsIgnoreCase(counter.getName())) {
counter.setDisplayed(true);
found = true;
break;
}
}
if (!found) {
throw new IllegalArgumentException("Unknown counter: " + displayedCounterName);
}
}
}
}
private void initCollect() {
try {
Class.forName("org.jrobin.core.RrdDb");
// il a parfois été observé "ClassNotFoundException: org.jrobin.core.RrdException"
// dans tomcat lors de l'arrêt du serveur à l'appel de JRobin.stop()
Class.forName("org.jrobin.core.RrdException");
} catch (final ClassNotFoundException e) {
LOG.debug("jrobin classes unavailable: collect of data is disabled");
HttpCookieManager.setDefaultRange(Period.TOUT.getRange());
// si pas de jar jrobin, alors pas de collecte et période "Tout" par défaut
return;
}
try {
JRobin.initBackendFactory(timer);
} catch (final IOException e) {
LOG.warn(e.toString(), e);
}
final int resolutionSeconds = Parameters.getResolutionSeconds();
final int periodMillis = resolutionSeconds * 1000;
// on schedule la tâche de fond
timer.schedule(collectTimerTask, periodMillis, periodMillis);
LOG.debug("collect task scheduled every " + resolutionSeconds + 's');
// on appelle la collecte pour que les instances jrobin soient définies
// au cas où un graph de la page de monitoring soit demandé de suite
collector.collectLocalContextWithoutErrors();
LOG.debug("first collect of data done");
if (Parameters.getParameter(Parameter.MAIL_SESSION) != null
&& Parameters.getParameter(Parameter.ADMIN_EMAILS) != null) {
MailReport.scheduleReportMailForLocalServer(collector, timer);
LOG.debug("mail reports scheduled for "
+ Parameters.getParameter(Parameter.ADMIN_EMAILS));
}
}
private SamplingProfiler initSamplingProfiler() {
if (Parameters.getParameter(Parameter.SAMPLING_SECONDS) != null) {
final SamplingProfiler sampler;
final String excludedPackagesParameter = Parameters
.getParameter(Parameter.SAMPLING_EXCLUDED_PACKAGES);
final String includedPackagesParameter = Parameters
.getParameter(Parameter.SAMPLING_INCLUDED_PACKAGES);
if (excludedPackagesParameter == null && includedPackagesParameter == null) {
sampler = new SamplingProfiler();
} else {
sampler = new SamplingProfiler(excludedPackagesParameter,
includedPackagesParameter);
}
final TimerTask samplingTimerTask = new TimerTask() {
@Override
public void run() {
sampler.update();
}
};
final long periodInMillis = Math.round(
Double.parseDouble(Parameters.getParameter(Parameter.SAMPLING_SECONDS)) * 1000);
this.timer.schedule(samplingTimerTask, 10000, periodInMillis);
LOG.debug("hotspots sampling initialized");
return sampler;
}
return null;
}
private static void initLogs() {
// on branche le handler java.util.logging pour le counter de logs
LoggingHandler.getSingleton().register();
if (LOG.LOG4J_ENABLED) {
// si log4j est disponible on branche aussi l'appender pour le counter de logs
Log4JAppender.getSingleton().register();
}
if (LOG.LOG4J2_ENABLED) {
// si log4j2 est disponible on branche aussi l'appender pour le counter de logs
Log4J2Appender.getSingleton().register();
}
if (LOG.LOGBACK_ENABLED) {
// si logback est disponible on branche aussi l'appender pour le counter de logs
LogbackAppender.getSingleton().register();
}
LOG.debug("log listeners initialized");
}
private static boolean isMojarraAvailable() {
try {
Class.forName("com.sun.faces.application.ActionListenerImpl");
return true;
} catch (final Throwable e) { // NOPMD
return false;
}
}
private void logSystemInformationsAndParameters() {
// log les principales informations sur le système et sur les paramètres définis spécifiquement
LOG.debug("OS: " + System.getProperty("os.name") + ' '
+ System.getProperty("sun.os.patch.level") + ", " + System.getProperty("os.arch")
+ '/' + System.getProperty("sun.arch.data.model"));
LOG.debug("Java: " + System.getProperty("java.runtime.name") + ", "
+ System.getProperty("java.runtime.version"));
LOG.debug("Server: " + Parameters.getServletContext().getServerInfo());
LOG.debug("Webapp context: " + Parameters.getContextPath(Parameters.getServletContext()));
LOG.debug("JavaMelody version: " + Parameters.JAVAMELODY_VERSION);
final String location = getJavaMelodyLocation();
if (location != null) {
LOG.debug("JavaMelody classes loaded from: " + location);
}
LOG.debug("Application type: " + applicationType);
LOG.debug("Application version: " + MavenArtifact.getWebappVersion());
LOG.debug("Host: " + Parameters.getHostName() + '@' + Parameters.getHostAddress());
for (final Parameter parameter : Parameter.values()) {
final String value = Parameters.getParameter(parameter);
if (value != null && parameter != Parameter.ANALYTICS_ID) {
LOG.debug("parameter defined: " + parameter.getCode() + '=' + value);
}
}
}
private static String getJavaMelodyLocation() {
final Class<FilterContext> clazz = FilterContext.class;
final CodeSource codeSource = clazz.getProtectionDomain().getCodeSource();
if (codeSource != null && codeSource.getLocation() != null) {
String location = codeSource.getLocation().toString();
// location contient le nom du fichier jar
// (ou le nom du fichier de cette classe s'il y a un répertoire sans jar)
final String clazzFileName = clazz.getSimpleName() + ".class";
if (location.endsWith(clazzFileName)) {
location = location.substring(0, location.length() - clazzFileName.length());
}
return location;
}
return null;
}
/**
* Registers CounterRequestMXBean beans for each of the enabled counters.
* The beans are registered under "net.bull.javamelody:type=CounterRequest,context=<webapp>,name=<counter name>" names.
* @author Alexey Pushkin
*/
private void initJmxExpose() {
final String packageName = getClass().getName().substring(0,
getClass().getName().length() - getClass().getSimpleName().length() - 1);
String webapp = Parameters.getContextPath(Parameters.getServletContext());
if (webapp.length() >= 1 && webapp.charAt(0) == '/') {
webapp = webapp.substring(1, webapp.length());
}
final List<Counter> counters = collector.getCounters();
final MBeanServer platformMBeanServer = MBeans.getPlatformMBeanServer();
try {
for (final Counter counter : counters) {
if (!Parameters.isCounterHidden(counter.getName())) {
final CounterRequestMXBean.CounterRequestMXBeanImpl mxBean = new CounterRequestMXBean.CounterRequestMXBeanImpl(
counter);
final ObjectName name = new ObjectName(
packageName + ":type=CounterRequest,context=" + webapp + ",name="
+ counter.getName());
platformMBeanServer.registerMBean(mxBean, name);
jmxNames.add(name);
}
}
LOG.debug("JMX mbeans registered");
} catch (final JMException e) {
LOG.warn("failed to register JMX mbeans", e);
}
}
void stopCollector() {
// cette méthode est appelée par MonitoringFilter lorsqu'il y a un serveur de collecte
if (collectTimerTask != null) {
// 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
collectTimerTask.cancel();
}
// arrêt du collector
collector.stop();
}
void destroy() {
try {
try {
if (collector != null) {
new MonitoringController(collector, null).writeHtmlToLastShutdownFile();
}
} finally {
//on rebind les dataSources initiales à la place des proxy
JdbcWrapper.SINGLETON.stop();
deregisterJdbcDriver();
// on enlève l'appender de logback, log4j et le handler de java.util.logging
deregisterLogs();
// on enlève le listener de jobs quartz
if (JobInformations.QUARTZ_AVAILABLE) {
JobGlobalListener.destroyJobGlobalListener();
}
unregisterJmxExpose();
}
} finally {
MonitoringInitialContextFactory.stop();
// on arrête le thread du collector,
// on persiste les compteurs pour les relire à l'initialisation et ne pas perdre les stats
// et on vide les compteurs
if (timer != null) {
timer.cancel();
}
if (samplingProfiler != null) {
samplingProfiler.clear();
}
if (collector != null) {
collector.stop();
}
Collector.stopJRobin();
Collector.detachVirtualMachine();
}
}
private static void deregisterJdbcDriver() {
// on désinstalle le driver jdbc s'il est installé
// (mais sans charger la classe JdbcDriver pour ne pas installer le driver)
final Class<FilterContext> classe = FilterContext.class;
final String packageName = classe.getName().substring(0,
classe.getName().length() - classe.getSimpleName().length() - 1);
for (final Driver driver : Collections.list(DriverManager.getDrivers())) {
if (driver.getClass().getName().startsWith(packageName)) {
try {
DriverManager.deregisterDriver(driver);
} catch (final SQLException e) {
// ne peut arriver
throw new IllegalStateException(e);
}
}
}
}
private static void deregisterLogs() {
if (LOG.LOGBACK_ENABLED) {
LogbackAppender.getSingleton().deregister();
}
if (LOG.LOG4J_ENABLED) {
Log4JAppender.getSingleton().deregister();
}
LoggingHandler.getSingleton().deregister();
}
/**
* Unregisters CounterRequestMXBean beans.
*/
private void unregisterJmxExpose() {
if (jmxNames.isEmpty()) {
return;
}
try {
final MBeanServer platformMBeanServer = MBeans.getPlatformMBeanServer();
for (final ObjectName name : jmxNames) {
platformMBeanServer.unregisterMBean(name);
}
} catch (final JMException e) {
LOG.warn("failed to unregister JMX beans", e);
}
}
Collector getCollector() {
return collector;
}
Timer getTimer() {
return timer;
}
}