/* * Copyright 2008-2014 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 static net.bull.javamelody.HttpParameters.COLLECTOR_PARAMETER; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.regex.Pattern; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * Filtre de servlet pour le monitoring. * C'est la classe de ce filtre qui doit être déclarée dans le fichier web.xml de la webapp. * @author Emeric Vernat */ public class MonitoringFilter implements Filter { private static boolean instanceCreated; private static final List<String> CONTEXT_PATHS = new ArrayList<String>(); private boolean instanceEnabled; // Ces variables httpCounter et errorCounter conservent un état qui est global au filtre // et à l'application (donc thread-safe). private Counter httpCounter; private Counter errorCounter; private boolean monitoringDisabled; private boolean logEnabled; private Pattern urlExcludePattern; private FilterContext filterContext; private HttpAuth httpAuth; private FilterConfig filterConfig; private String monitoringUrl; /** * Constructeur. */ public MonitoringFilter() { super(); if (instanceCreated) { // ce filter a déjà été chargé précédemment et est chargé une 2ème fois donc on désactive cette 2ème instance // (cela peut arriver par exemple dans glassfish v3 lorsque le filter est déclaré dans le fichier web.xml // et déclaré par ailleurs dans le fichier web-fragment.xml à l'intérieur du jar, issue 147), // mais il peut être réactivé dans init (issue 193) instanceEnabled = false; } else { instanceEnabled = true; setInstanceCreated(true); } } private static void setInstanceCreated(boolean newInstanceCreated) { instanceCreated = newInstanceCreated; } /** {@inheritDoc} */ @Override public void init(FilterConfig config) throws ServletException { final long start = System.currentTimeMillis(); final String contextPath = Parameters.getContextPath(config.getServletContext()); if (!instanceEnabled) { if (!CONTEXT_PATHS.contains(contextPath)) { // si jars dans tomcat/lib, il y a plusieurs instances mais dans des webapps différentes (issue 193) instanceEnabled = true; } else { return; } } CONTEXT_PATHS.add(contextPath); this.filterConfig = config; Parameters.initialize(config); monitoringDisabled = Boolean.parseBoolean(Parameters.getParameter(Parameter.DISABLED)); if (monitoringDisabled) { return; } LOG.debug("JavaMelody filter init started"); this.filterContext = new FilterContext(); this.httpAuth = new HttpAuth(); config.getServletContext().setAttribute(ReportServlet.FILTER_CONTEXT_KEY, filterContext); final Collector collector = filterContext.getCollector(); this.httpCounter = collector.getCounterByName(Counter.HTTP_COUNTER_NAME); this.errorCounter = collector.getCounterByName(Counter.ERROR_COUNTER_NAME); logEnabled = Boolean.parseBoolean(Parameters.getParameter(Parameter.LOG)); if (Parameters.getParameter(Parameter.URL_EXCLUDE_PATTERN) != null) { // lance une PatternSyntaxException si la syntaxe du pattern est invalide urlExcludePattern = Pattern.compile(Parameters .getParameter(Parameter.URL_EXCLUDE_PATTERN)); } final long duration = System.currentTimeMillis() - start; LOG.debug("JavaMelody filter init done in " + duration + " ms"); } /** {@inheritDoc} */ @Override public void destroy() { final long start = System.currentTimeMillis(); if (monitoringDisabled || !instanceEnabled) { return; } try { if (filterContext != null) { filterContext.destroy(); } } finally { final String contextPath = Parameters.getContextPath(filterConfig.getServletContext()); CONTEXT_PATHS.remove(contextPath); // nettoyage avant le retrait de la webapp au cas où celui-ci ne suffise pas httpCounter = null; errorCounter = null; urlExcludePattern = null; filterConfig = null; filterContext = null; } final long duration = System.currentTimeMillis() - start; LOG.debug("JavaMelody filter destroy done in " + duration + " ms"); } /** {@inheritDoc} */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse) || monitoringDisabled || !instanceEnabled) { // si ce n'est pas une requête http ou si le monitoring est désactivé, on fait suivre chain.doFilter(request, response); return; } final HttpServletRequest httpRequest = (HttpServletRequest) request; final HttpServletResponse httpResponse = (HttpServletResponse) response; if (httpRequest.getRequestURI().equals(getMonitoringUrl(httpRequest))) { doMonitoring(httpRequest, httpResponse); return; } if (!httpCounter.isDisplayed() || isRequestExcluded((HttpServletRequest) request)) { // si cette url est exclue ou si le counter http est désactivé, on ne monitore pas cette requête http chain.doFilter(request, response); return; } doFilter(chain, httpRequest, httpResponse); } private void doFilter(FilterChain chain, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException { final CounterServletResponseWrapper wrappedResponse = new CounterServletResponseWrapper( httpResponse); final HttpServletRequest wrappedRequest = createRequestWrapper(httpRequest, wrappedResponse); final long start = System.currentTimeMillis(); final long startCpuTime = ThreadInformations.getCurrentThreadCpuTime(); boolean systemError = false; Throwable systemException = null; String requestName = getRequestName(wrappedRequest); final String completeRequestName = getCompleteRequestName(wrappedRequest, true); try { JdbcWrapper.ACTIVE_THREAD_COUNT.incrementAndGet(); // on binde le contexte de la requête http pour les requêtes sql httpCounter.bindContext(requestName, completeRequestName, httpRequest.getRemoteUser(), startCpuTime); // on binde la requête http (utilisateur courant et requête complète) pour les derniers logs d'erreurs httpRequest.setAttribute(CounterError.REQUEST_KEY, completeRequestName); CounterError.bindRequest(httpRequest); chain.doFilter(wrappedRequest, wrappedResponse); if (!httpRequest.isAsyncStarted()) { wrappedResponse.flushBuffer(); } } catch (final Throwable t) { // NOPMD // on catche Throwable pour avoir tous les cas d'erreur système systemException = t; throwException(t); } finally { if (httpCounter == null) { // "the destroy method is only called once all threads within the filter's doFilter method have exited // or after a timeout period has passed" // si timeout, alors on évite ici une NPE (cf issue 262) return; // NOPMD } try { // Si la durée est négative (arrive bien que rarement en cas de synchronisation d'horloge système), // alors on considère que la durée est 0. // Rq : sous Windows XP, currentTimeMillis a une résolution de 16ms environ // (discrètisation de la durée en 0, 16 ou 32 ms, etc ...) // et sous linux ou Windows Vista la résolution est bien meilleure. // On n'utilise pas nanoTime car il peut être un peu plus lent (mesuré à 2 microsecondes, // voir aussi http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6440250) // et car des millisecondes suffisent pour une requête http final long duration = Math.max(System.currentTimeMillis() - start, 0); final long cpuUsedMillis = (ThreadInformations.getCurrentThreadCpuTime() - startCpuTime) / 1000000; JdbcWrapper.ACTIVE_THREAD_COUNT.decrementAndGet(); putUserInfoInSession(httpRequest); if (systemException != null) { systemError = true; final StringWriter stackTrace = new StringWriter(200); systemException.printStackTrace(new PrintWriter(stackTrace)); errorCounter.addRequestForSystemError(systemException.toString(), duration, cpuUsedMillis, stackTrace.toString()); } else if (wrappedResponse.getCurrentStatus() >= HttpServletResponse.SC_BAD_REQUEST && wrappedResponse.getCurrentStatus() != HttpServletResponse.SC_UNAUTHORIZED) { // SC_UNAUTHORIZED (401) is not an error, it is the first handshake of a Basic (or Digest) Auth (issue 455) systemError = true; errorCounter.addRequestForSystemError( "Error" + wrappedResponse.getCurrentStatus(), duration, cpuUsedMillis, null); } // taille du flux sortant final int responseSize = wrappedResponse.getDataLength(); // nom identifiant la requête if (wrappedResponse.getCurrentStatus() == HttpServletResponse.SC_NOT_FOUND) { // Sécurité : si status http est 404, alors requestName est Error404 // pour éviter de saturer la mémoire avec potentiellement beaucoup d'url différentes requestName = "Error404"; } // on enregistre la requête dans les statistiques httpCounter.addRequest(requestName, duration, cpuUsedMillis, systemError, responseSize); // on log sur Log4J ou java.util.logging dans la catégorie correspond au nom du filtre dans web.xml log(httpRequest, requestName, duration, systemError, responseSize); } finally { // normalement le unbind du contexte a été fait dans httpCounter.addRequest // mais pour être sûr au cas où il y ait une exception comme OutOfMemoryError // on le refait ici pour éviter des erreurs par la suite, // car il ne doit pas y avoir de contexte restant au delà de la requête http httpCounter.unbindContext(); // et unbind de la requête http CounterError.unbindRequest(); } } } protected HttpServletRequest createRequestWrapper(HttpServletRequest request, HttpServletResponse response) throws IOException { HttpServletRequest wrappedRequest = JspWrapper.createHttpRequestWrapper(request, response); final PayloadNameRequestWrapper payloadNameRequestWrapper = new PayloadNameRequestWrapper( wrappedRequest); payloadNameRequestWrapper.initialize(); if (payloadNameRequestWrapper.getPayloadRequestType() != null) { wrappedRequest = payloadNameRequestWrapper; } return wrappedRequest; } protected String getRequestName(HttpServletRequest request) { return getCompleteRequestName(request, false); } protected final String getMonitoringUrl(HttpServletRequest httpRequest) { if (monitoringUrl == null) { monitoringUrl = httpRequest.getContextPath() + Parameters.getMonitoringPath(); } return monitoringUrl; } private void putUserInfoInSession(HttpServletRequest httpRequest) { final HttpSession session = httpRequest.getSession(false); if (session == null) { // la session n'est pas encore créée (et ne le sera peut-être jamais) return; } // on ne met dans la session ces attributs que si ils n'y sont pas déjà // (pour que la session ne soit pas resynchronisée si serveur en cluster par exemple), // donc l'adresse ip est celle de la première requête créant une session, // et si l'adresse ip change ensuite c'est très étrange // mais elle n'est pas mise à jour dans la session if (session.getAttribute(SessionInformations.SESSION_COUNTRY_KEY) == null) { // langue préférée du navigateur, getLocale ne peut être null final Locale locale = httpRequest.getLocale(); if (locale.getCountry().length() > 0) { session.setAttribute(SessionInformations.SESSION_COUNTRY_KEY, locale.getCountry()); } else { session.setAttribute(SessionInformations.SESSION_COUNTRY_KEY, locale.getLanguage()); } } if (session.getAttribute(SessionInformations.SESSION_REMOTE_ADDR) == null) { // adresse ip final String forwardedFor = httpRequest.getHeader("X-Forwarded-For"); final String remoteAddr; if (forwardedFor == null) { remoteAddr = httpRequest.getRemoteAddr(); } else { remoteAddr = httpRequest.getRemoteAddr() + " forwarded for " + forwardedFor; } session.setAttribute(SessionInformations.SESSION_REMOTE_ADDR, remoteAddr); } if (session.getAttribute(SessionInformations.SESSION_REMOTE_USER) == null) { // login utilisateur, peut être null final String remoteUser = httpRequest.getRemoteUser(); if (remoteUser != null) { session.setAttribute(SessionInformations.SESSION_REMOTE_USER, remoteUser); } } } private void doMonitoring(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException { if (!isAllowed(httpRequest, httpResponse)) { return; } final Collector collector = filterContext.getCollector(); final MonitoringController monitoringController = new MonitoringController(collector, null); monitoringController.doActionIfNeededAndReport(httpRequest, httpResponse, filterConfig.getServletContext()); if ("stop".equalsIgnoreCase(httpRequest.getParameter(COLLECTOR_PARAMETER))) { // on a été appelé par un serveur de collecte qui fera l'aggrégation dans le temps, // le stockage et les courbes, donc on arrête le timer s'il est démarré // et on vide les stats pour que le serveur de collecte ne récupère que les deltas for (final Counter counter : collector.getCounters()) { counter.clear(); } if (!collector.isStopped()) { LOG.debug("Stopping the javamelody collector in this webapp, because a collector server from " + httpRequest.getRemoteAddr() + " wants to collect the data itself"); filterContext.stopCollector(); } } } private static String getCompleteRequestName(HttpServletRequest httpRequest, boolean includeQueryString) { // on ne prend pas httpRequest.getPathInfo() // car requestURI == <context>/<servlet>/<pathInfo>, // et dans le cas où il y a plusieurs servlets (par domaine fonctionnel ou technique) // pathInfo ne contient pas l'indication utile de la servlet String tmp = httpRequest.getRequestURI().substring(httpRequest.getContextPath().length()); // si la requête http contient un ";", par exemple ";jsessionid=12345567890ABCDEF" // quand le navigateur web n'accepte pas les cookies, alors on ignore ce qu'il y a à partir de ";" // et on ne garde que la requête http elle-même final int lastIndexOfSemiColon = tmp.lastIndexOf(';'); if (lastIndexOfSemiColon != -1) { tmp = tmp.substring(0, lastIndexOfSemiColon); } final String method; if ("XMLHttpRequest".equals(httpRequest.getHeader("X-Requested-With"))) { method = "ajax " + httpRequest.getMethod(); } else { method = httpRequest.getMethod(); } if (!includeQueryString) { //Check payload request to support GWT, SOAP, and XML-RPC statistic gathering if (httpRequest instanceof PayloadNameRequestWrapper) { final PayloadNameRequestWrapper wrapper = (PayloadNameRequestWrapper) httpRequest; return tmp + wrapper.getPayloadRequestName() + ' ' + wrapper.getPayloadRequestType(); } return tmp + ' ' + method; } final String queryString = httpRequest.getQueryString(); if (queryString == null) { return tmp + ' ' + method; } return tmp + '?' + queryString + ' ' + method; } private boolean isRequestExcluded(HttpServletRequest httpRequest) { return urlExcludePattern != null && urlExcludePattern.matcher( httpRequest.getRequestURI() .substring(httpRequest.getContextPath().length())).matches(); } // cette méthode est protected pour pouvoir être surchargée dans une classe définie par l'application protected boolean isAllowed(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException { return httpAuth.isAllowed(httpRequest, httpResponse); } // cette méthode est protected pour pouvoir être surchargée dans une classe définie par l'application protected void log(HttpServletRequest httpRequest, String requestName, long duration, boolean systemError, int responseSize) { if (!logEnabled) { return; } final String filterName = filterConfig.getFilterName(); LOG.logHttpRequest(httpRequest, requestName, duration, systemError, responseSize, filterName); } private static void throwException(Throwable t) throws IOException, ServletException { if (t instanceof Error) { throw (Error) t; } else if (t instanceof RuntimeException) { throw (RuntimeException) t; } else if (t instanceof IOException) { throw (IOException) t; } else if (t instanceof ServletException) { throw (ServletException) t; } else { // n'arrive à priori pas car chain.doFilter ne déclare que IOException et ServletException // mais au cas où throw new ServletException(t.getMessage(), t); } } FilterContext getFilterContext() { return filterContext; } }