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.IOException;
21 import java.io.PrintWriter;
22 import java.io.StringWriter;
23 import java.net.URL;
24 import java.net.URLEncoder;
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.regex.Pattern;
29
30 import javax.servlet.Filter;
31 import javax.servlet.FilterChain;
32 import javax.servlet.FilterConfig;
33 import javax.servlet.ServletException;
34 import javax.servlet.ServletRequest;
35 import javax.servlet.ServletResponse;
36 import javax.servlet.http.HttpServletRequest;
37 import javax.servlet.http.HttpServletResponse;
38 import javax.servlet.http.HttpSession;
39
40 import net.bull.javamelody.internal.common.HttpParameter;
41 import net.bull.javamelody.internal.common.HttpPart;
42 import net.bull.javamelody.internal.common.LOG;
43 import net.bull.javamelody.internal.common.Parameters;
44 import net.bull.javamelody.internal.model.Collector;
45 import net.bull.javamelody.internal.model.Counter;
46 import net.bull.javamelody.internal.model.CounterError;
47 import net.bull.javamelody.internal.model.CounterRequestContext;
48 import net.bull.javamelody.internal.model.LabradorRetriever;
49 import net.bull.javamelody.internal.model.ThreadInformations;
50 import net.bull.javamelody.internal.web.CounterServletResponseWrapper;
51 import net.bull.javamelody.internal.web.HttpAuth;
52 import net.bull.javamelody.internal.web.MonitoringController;
53 import net.bull.javamelody.internal.web.RumInjector;
54
55 /**
56  * Filtre de servlet pour le monitoring.
57  * C'est la classe de ce filtre qui doit être déclarée dans le fichier web.xml de la webapp.
58  * @author Emeric Vernat
59  */

60 public class MonitoringFilter implements Filter {
61
62     private static boolean instanceCreated;
63
64     private static final List<String> CONTEXT_PATHS = new ArrayList<>();
65
66     private static URL unregisterApplicationNodeInCollectServerUrl;
67
68     private boolean instanceEnabled;
69
70     // "Classic" by default
71     // (not "Jenkins""JIRA""Confluence""Bamboo""Sonar""Liferay""Alfresco""Grails""Collector server")
72     private String applicationType = "Classic";
73
74     // Ces variables httpCounter et errorCounter conservent un état qui est global au filtre
75     // et à l'application (donc thread-safe).
76     private Counter httpCounter;
77     private Counter errorCounter;
78
79     private boolean monitoringDisabled;
80     private boolean logEnabled;
81     private boolean rumEnabled;
82     private Pattern urlExcludePattern;
83     private FilterContext filterContext;
84     private HttpAuth httpAuth;
85     private FilterConfig filterConfig;
86     private String monitoringUrl;
87     private boolean servletApi2;
88
89     /**
90      * Constructeur.
91      */

92     public MonitoringFilter() {
93         super();
94         if (instanceCreated) {
95             // 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
96             // (cela peut arriver par exemple dans glassfish v3 lorsque le filter est déclaré dans le fichier web.xml
97             // et déclaré par ailleurs dans le fichier web-fragment.xml à l'intérieur du jar, issue 147),
98             // mais il peut être réactivé dans init (issue 193)
99             instanceEnabled = false;
100         } else {
101             instanceEnabled = true;
102             setInstanceCreated(true);
103         }
104     }
105
106     private static void setInstanceCreated(boolean newInstanceCreated) {
107         instanceCreated = newInstanceCreated;
108     }
109
110     /**
111      * @return Type of application
112      */

113     public String getApplicationType() {
114         return applicationType;
115     }
116
117     /**
118      * @param applicationType Type of application
119      */

120     public void setApplicationType(final String applicationType) {
121         this.applicationType = applicationType;
122     }
123
124     /** {@inheritDoc} */
125     @Override
126     public void init(FilterConfig config) throws ServletException {
127         final long start = System.currentTimeMillis(); // NOPMD
128         final String contextPath = Parameters.getContextPath(config.getServletContext());
129         if (!instanceEnabled) {
130             if (!CONTEXT_PATHS.contains(contextPath)) {
131                 // si jars dans tomcat/lib, il y a plusieurs instances mais dans des webapps différentes (issue 193)
132                 instanceEnabled = true;
133             } else {
134                 return;
135             }
136         }
137         CONTEXT_PATHS.add(contextPath);
138         this.filterConfig = config;
139         this.servletApi2 = config.getServletContext().getMajorVersion() < 3;
140         Parameters.initialize(config);
141         monitoringDisabled = Parameter.DISABLED.getValueAsBoolean();
142         if (monitoringDisabled) {
143             return;
144         }
145
146         LOG.debug("JavaMelody filter init started");
147
148         this.filterContext = new FilterContext(getApplicationType());
149         this.httpAuth = new HttpAuth();
150         config.getServletContext().setAttribute(ReportServlet.FILTER_CONTEXT_KEY, filterContext);
151         final Collector collector = filterContext.getCollector();
152         this.httpCounter = collector.getCounterByName(Counter.HTTP_COUNTER_NAME);
153         this.errorCounter = collector.getCounterByName(Counter.ERROR_COUNTER_NAME);
154
155         logEnabled = Parameter.LOG.getValueAsBoolean();
156         rumEnabled = Parameter.RUM_ENABLED.getValueAsBoolean();
157         if (Parameter.URL_EXCLUDE_PATTERN.getValue() != null) {
158             // lance une PatternSyntaxException si la syntaxe du pattern est invalide
159             urlExcludePattern = Pattern.compile(Parameter.URL_EXCLUDE_PATTERN.getValue());
160         }
161
162         final long duration = System.currentTimeMillis() - start;
163         LOG.debug("JavaMelody filter init done in " + duration + " ms");
164     }
165
166     /** {@inheritDoc} */
167     @Override
168     public void destroy() {
169         if (monitoringDisabled || !instanceEnabled) {
170             return;
171         }
172         final long start = System.currentTimeMillis();
173
174         try {
175             if (filterContext != null) {
176                 filterContext.destroy();
177             }
178         } finally {
179             final String contextPath = Parameters.getContextPath(filterConfig.getServletContext());
180             CONTEXT_PATHS.remove(contextPath);
181             // nettoyage avant le retrait de la webapp au cas où celui-ci ne suffise pas
182             httpCounter = null;
183             errorCounter = null;
184             urlExcludePattern = null;
185             filterConfig = null;
186             filterContext = null;
187         }
188         final long duration = System.currentTimeMillis() - start;
189         LOG.debug("JavaMelody filter destroy done in " + duration + " ms");
190     }
191
192     /** {@inheritDoc} */
193     @Override
194     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
195             throws IOException, ServletException {
196         if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)
197                 || monitoringDisabled || !instanceEnabled) {
198             // si ce n'est pas une requête http ou si le monitoring est désactivé, on fait suivre
199             chain.doFilter(request, response);
200             return;
201         }
202         final HttpServletRequest httpRequest = (HttpServletRequest) request;
203         final HttpServletResponse httpResponse = (HttpServletResponse) response;
204
205         if (httpRequest.getRequestURI().equals(getMonitoringUrl(httpRequest))) {
206             doMonitoring(httpRequest, httpResponse);
207             return;
208         }
209         if (!httpCounter.isDisplayed() || isRequestExcluded((HttpServletRequest) request)) {
210             // si cette url est exclue ou si le counter http est désactivé, on ne monitore pas cette requête http
211             chain.doFilter(request, response);
212             return;
213         }
214
215         doFilter(chain, httpRequest, httpResponse);
216     }
217
218     private void doFilter(FilterChain chain, HttpServletRequest httpRequest,
219             HttpServletResponse httpResponse) throws IOException, ServletException {
220         final long start = System.currentTimeMillis();
221         final long startCpuTime = ThreadInformations.getCurrentThreadCpuTime();
222         final long startAllocatedBytes = ThreadInformations.getCurrentThreadAllocatedBytes();
223         final CounterServletResponseWrapper wrappedResponse = createResponseWrapper(httpRequest,
224                 httpResponse);
225         final HttpServletRequest wrappedRequest = createRequestWrapper(httpRequest,
226                 wrappedResponse);
227         boolean systemError = false;
228         Throwable systemException = null;
229         String requestName = getRequestName(wrappedRequest);
230         final String completeRequestName = getCompleteRequestName(wrappedRequest, true);
231         try {
232             JdbcWrapper.ACTIVE_THREAD_COUNT.incrementAndGet();
233             // on binde le contexte de la requête http pour les requêtes sql
234             httpCounter.bindContext(requestName, completeRequestName, httpRequest, startCpuTime,
235                     startAllocatedBytes);
236             // on binde la requête http (utilisateur courant et requête complète) pour les derniers logs d'erreurs
237             httpRequest.setAttribute(CounterError.REQUEST_KEY, completeRequestName);
238             CounterError.bindRequest(httpRequest);
239             chain.doFilter(wrappedRequest, wrappedResponse);
240             if (servletApi2 || !httpRequest.isAsyncStarted()) {
241                 wrappedResponse.flushStream();
242             }
243         } catch (final Throwable t) { // NOPMD
244             // on catche Throwable pour avoir tous les cas d'erreur système
245             systemException = t;
246             throwException(t);
247         } finally {
248             if (httpCounter == null) {
249                 // "the destroy method is only called once all threads within the filter's doFilter method have exited
250                 // or after a timeout period has passed"
251                 // si timeout, alors on évite ici une NPE (cf  issue 262)
252                 return// NOPMD
253             }
254             try {
255                 // Si la durée est négative (arrive bien que rarement en cas de synchronisation d'horloge système),
256                 // alors on considère que la durée est 0.
257                 // Rq : sous Windows XP, currentTimeMillis a une résolution de 16ms environ
258                 // (discrètisation de la durée en 0, 16 ou 32 ms, etc ...)
259                 // et sous linux ou Windows Vista la résolution est bien meilleure.
260                 // On n'utilise pas nanoTime car il peut être un peu plus lent (mesuré à 2 microsecondes,
261                 // voir aussi http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6440250)
262                 // et car des millisecondes suffisent pour une requête http
263                 final long duration = Math.max(System.currentTimeMillis() - start, 0);
264                 final int cpuUsedMillis = (int) ((ThreadInformations.getCurrentThreadCpuTime()
265                         - startCpuTime) / 1000000L);
266                 final int allocatedKBytes;
267                 if (startAllocatedBytes >= 0) {
268                     allocatedKBytes = (int) ((ThreadInformations.getCurrentThreadAllocatedBytes()
269                             - startAllocatedBytes) / 1024L);
270                 } else {
271                     allocatedKBytes = -1;
272                 }
273                 JdbcWrapper.ACTIVE_THREAD_COUNT.decrementAndGet();
274                 putUserInfoInSession(httpRequest);
275                 if (systemException != null) {
276                     systemError = true;
277                     final StringWriter stackTrace = new StringWriter(200);
278                     systemException.printStackTrace(new PrintWriter(stackTrace));
279                     errorCounter.addRequestForSystemError(systemException.toString(), duration,
280                             cpuUsedMillis, allocatedKBytes, stackTrace.toString());
281                 } else if (wrappedResponse.getCurrentStatus() >= HttpServletResponse.SC_BAD_REQUEST
282                         && wrappedResponse
283                                 .getCurrentStatus() != HttpServletResponse.SC_UNAUTHORIZED) {
284                     // SC_UNAUTHORIZED (401) is not an error, it is the first handshake of a Basic (or Digest) Auth (issue 455)
285                     systemError = true;
286                     errorCounter.addRequestForSystemError(
287                             "Error" + wrappedResponse.getCurrentStatus(), duration, cpuUsedMillis,
288                             allocatedKBytes, null);
289                 }
290                 // prise en compte de Spring bestMatchingPattern s'il y a
291                 requestName = CounterRequestContext.getHttpRequestName(httpRequest, requestName);
292                 // taille du flux sortant
293                 final long responseSize = wrappedResponse.getDataLength();
294                 // nom identifiant la requête
295                 if (wrappedResponse.getCurrentStatus() == HttpServletResponse.SC_NOT_FOUND) {
296                     // Sécurité : si status http est 404, alors requestName est Error404
297                     // pour éviter de saturer la mémoire avec potentiellement beaucoup d'url différentes
298                     requestName = "Error404";
299                 }
300
301                 // on enregistre la requête dans les statistiques
302                 httpCounter.addRequest(requestName, duration, cpuUsedMillis, allocatedKBytes,
303                         systemError, responseSize);
304                 // on log sur Log4J ou java.util.logging dans la catégorie correspond au nom du filtre dans web.xml
305                 log(httpRequest, requestName, duration, systemError,
306                         wrappedResponse.getCurrentStatus(), responseSize);
307             } finally {
308                 // normalement le unbind du contexte a été fait dans httpCounter.addRequest
309                 // mais pour être sûr au cas où il y ait une exception comme OutOfMemoryError
310                 // on le refait ici pour éviter des erreurs par la suite,
311                 // car il ne doit pas y avoir de contexte restant au delà de la requête http
312                 httpCounter.unbindContext();
313                 // et unbind de la requête http
314                 CounterError.unbindRequest();
315             }
316         }
317     }
318
319     protected CounterServletResponseWrapper createResponseWrapper(HttpServletRequest httpRequest,
320             HttpServletResponse httpResponse) {
321         HttpServletResponse httpResponse2 = httpResponse;
322         if (rumEnabled) {
323             httpResponse2 = RumInjector.createRumResponseWrapper(httpRequest, httpResponse,
324                     getRequestName(httpRequest));
325         }
326         return new CounterServletResponseWrapper(httpResponse2);
327     }
328
329     protected HttpServletRequest createRequestWrapper(HttpServletRequest request,
330             HttpServletResponse response) throws IOException {
331         HttpServletRequest wrappedRequest = JspWrapper.createHttpRequestWrapper(request, response);
332         final PayloadNameRequestWrapper payloadNameRequestWrapper = new PayloadNameRequestWrapper(
333                 wrappedRequest);
334         payloadNameRequestWrapper.initialize();
335         if (payloadNameRequestWrapper.getPayloadRequestType() != null) {
336             wrappedRequest = payloadNameRequestWrapper;
337         }
338         return wrappedRequest;
339     }
340
341     protected String getRequestName(HttpServletRequest request) {
342         return getCompleteRequestName(request, false);
343     }
344
345     protected String getMonitoringUrl(HttpServletRequest httpRequest) {
346         if (monitoringUrl == null) {
347             monitoringUrl = httpRequest.getContextPath() + Parameters.getMonitoringPath();
348         }
349         return monitoringUrl;
350     }
351
352     private void putUserInfoInSession(HttpServletRequest httpRequest) {
353         final HttpSession session = httpRequest.getSession(false);
354         if (session == null) {
355             // la session n'est pas encore créée (et ne le sera peut-être jamais)
356             return;
357         }
358         // on ne met dans la session ces attributs que si ils n'y sont pas déjà
359         // (pour que la session ne soit pas resynchronisée si serveur en cluster par exemple),
360         // donc l'adresse ip est celle de la première requête créant une session,
361         // et si l'adresse ip change ensuite c'est très étrange
362         // mais elle n'est pas mise à jour dans la session
363         if (session.getAttribute(SessionListener.SESSION_COUNTRY_KEY) == null) {
364             // langue préférée du navigateur, getLocale ne peut être null
365             final Locale locale = httpRequest.getLocale();
366             if (!locale.getCountry().isEmpty()) {
367                 session.setAttribute(SessionListener.SESSION_COUNTRY_KEY, locale.getCountry());
368             } else {
369                 session.setAttribute(SessionListener.SESSION_COUNTRY_KEY, locale.getLanguage());
370             }
371         }
372         if (session.getAttribute(SessionListener.SESSION_REMOTE_ADDR) == null) {
373             // adresse ip
374             final String forwardedFor = httpRequest.getHeader("X-Forwarded-For");
375             final String remoteAddr;
376             if (forwardedFor == null) {
377                 remoteAddr = httpRequest.getRemoteAddr();
378             } else {
379                 remoteAddr = httpRequest.getRemoteAddr() + " forwarded for " + forwardedFor;
380             }
381             session.setAttribute(SessionListener.SESSION_REMOTE_ADDR, remoteAddr);
382         }
383         if (session.getAttribute(SessionListener.SESSION_REMOTE_USER) == null) {
384             // login utilisateur, peut être null
385             final String remoteUser = httpRequest.getRemoteUser();
386             if (remoteUser != null) {
387                 session.setAttribute(SessionListener.SESSION_REMOTE_USER, remoteUser);
388             }
389         }
390         if (session.getAttribute(SessionListener.SESSION_USER_AGENT) == null) {
391             final String userAgent = httpRequest.getHeader("User-Agent");
392             session.setAttribute(SessionListener.SESSION_USER_AGENT, userAgent);
393         }
394     }
395
396     private void doMonitoring(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
397             throws IOException, ServletException {
398         if (isRumMonitoring(httpRequest, httpResponse)) {
399             return;
400         }
401
402         if (!isAllowed(httpRequest, httpResponse)) {
403             return;
404         }
405
406         final Collector collector = filterContext.getCollector();
407         final MonitoringController monitoringController = new MonitoringController(collector, null);
408         monitoringController.doActionIfNeededAndReport(httpRequest, httpResponse,
409                 filterConfig.getServletContext());
410
411         if ("stop".equalsIgnoreCase(HttpParameter.COLLECTOR.getParameterFrom(httpRequest))) {
412             // on a été appelé par un serveur de collecte qui fera l'aggrégation dans le temps,
413             // le stockage et les courbes, donc on arrête le timer s'il est démarré
414             // et on vide les stats pour que le serveur de collecte ne récupère que les deltas
415             for (final Counter counter : collector.getCounters()) {
416                 counter.clear();
417             }
418
419             if (!collector.isStopped()) {
420                 LOG.debug(
421                         "Stopping the javamelody collector in this webapp, because a collector server from "
422                                 + httpRequest.getRemoteAddr()
423                                 + " wants to collect the data itself");
424                 filterContext.stopCollector();
425             }
426         }
427     }
428
429     protected final boolean isRumMonitoring(HttpServletRequest httpRequest,
430             HttpServletResponse httpResponse) throws IOException {
431         if (rumEnabled) {
432             // these 2 ifs must be before isAllowed verification
433             if (RumInjector.isRumResource(HttpParameter.RESOURCE.getParameterFrom(httpRequest))) {
434                 // this is to give the boomerang.min.js content
435                 MonitoringController.doResource(httpResponse,
436                         HttpParameter.RESOURCE.getParameterFrom(httpRequest));
437                 return true;
438             } else if (HttpPart.RUM.isPart(httpRequest)) {
439                 // this is the call by the boomerang beacon to notify RUM data
440                 MonitoringController.noCache(httpResponse);
441                 httpResponse.setContentType("image/png");
442                 RumInjector.addRumHit(httpRequest, httpCounter);
443                 return true;
444             }
445         }
446         return false;
447     }
448
449     private static String getCompleteRequestName(HttpServletRequest httpRequest,
450             boolean includeQueryString) {
451         // on ne prend pas httpRequest.getPathInfo()
452         // car requestURI == <context>/<servlet>/<pathInfo>,
453         // et dans le cas où il y a plusieurs servlets (par domaine fonctionnel ou technique)
454         // pathInfo ne contient pas l'indication utile de la servlet
455         String tmp = httpRequest.getRequestURI().substring(httpRequest.getContextPath().length());
456         // si la requête http contient un ";", par exemple ";jsessionid=12345567890ABCDEF"
457         // quand le navigateur web n'accepte pas les cookies, alors on ignore ce qu'il y a à partir de ";"
458         // et on ne garde que la requête http elle-même
459         final int lastIndexOfSemiColon = tmp.lastIndexOf(';');
460         if (lastIndexOfSemiColon != -1) {
461             tmp = tmp.substring(0, lastIndexOfSemiColon);
462         }
463         final String method;
464         if ("XMLHttpRequest".equals(httpRequest.getHeader("X-Requested-With"))) {
465             method = "ajax " + httpRequest.getMethod();
466         } else {
467             method = httpRequest.getMethod();
468         }
469         if (!includeQueryString) {
470             //Check payload request to support GWT, SOAP, and XML-RPC statistic gathering
471             if (httpRequest instanceof PayloadNameRequestWrapper) {
472                 final PayloadNameRequestWrapper wrapper = (PayloadNameRequestWrapper) httpRequest;
473                 return tmp + wrapper.getPayloadRequestName() + ' '
474                         + wrapper.getPayloadRequestType();
475             }
476
477             return tmp + ' ' + method;
478         }
479         final String queryString = httpRequest.getQueryString();
480         if (queryString == null) {
481             return tmp + ' ' + method;
482         }
483         return tmp + '?' + queryString + ' ' + method;
484     }
485
486     private boolean isRequestExcluded(HttpServletRequest httpRequest) {
487         return urlExcludePattern != null && urlExcludePattern.matcher(
488                 httpRequest.getRequestURI().substring(httpRequest.getContextPath().length()))
489                 .matches();
490     }
491
492     // cette méthode est protected pour pouvoir être surchargée dans une classe définie par l'application
493     protected boolean isAllowed(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
494             throws IOException {
495         return httpAuth.isAllowed(httpRequest, httpResponse);
496     }
497
498     // cette méthode est protected pour pouvoir être surchargée dans une classe définie par l'application
499     protected void log(HttpServletRequest httpRequest, String requestName, long duration,
500             boolean systemError, int responseStatus, long responseSize) {
501         if (!logEnabled) {
502             return;
503         }
504         final String filterName = filterConfig.getFilterName();
505         LOG.logHttpRequest(httpRequest, requestName, duration, systemError, responseStatus,
506                 responseSize, filterName);
507     }
508
509     private static void throwException(Throwable t) throws IOException, ServletException {
510         if (t instanceof Error) {
511             throw (Error) t;
512         } else if (t instanceof RuntimeException) {
513             throw (RuntimeException) t;
514         } else if (t instanceof IOException) {
515             throw (IOException) t;
516         } else if (t instanceof ServletException) {
517             throw (ServletException) t;
518         } else {
519             // n'arrive à priori pas car chain.doFilter ne déclare que IOException et ServletException
520             // mais au cas où
521             throw new ServletException(t.getMessage(), t);
522         }
523     }
524
525     FilterContext getFilterContext() {
526         return filterContext;
527     }
528
529     /**
530      * Asynchronously calls the optional collect server to register this application's node to be monitored.
531      * @param applicationName Name of the application in the collect server:<br/>
532      *     if it already exists the node will be added with the other nodes, if null name will be "contextPath_hostname".
533      * @param collectServerUrl Url of the collect server,
534      *     for example http://11.22.33.44:8080
535      * @param applicationNodeUrl Url of this application node to be called by the collect server,
536      *     for example http://55.66.77.88:8080/mywebapp
537      */

538     public static void registerApplicationNodeInCollectServer(String applicationName,
539             URL collectServerUrl, URL applicationNodeUrl) {
540         if (collectServerUrl == null || applicationNodeUrl == null) {
541             throw new IllegalArgumentException(
542                     "collectServerUrl and applicationNodeUrl must not be null");
543         }
544         final String appName;
545         if (applicationName == null) {
546             appName = Parameters.getCurrentApplication();
547         } else {
548             appName = applicationName;
549         }
550         final URL registerUrl;
551         try {
552             registerUrl = new URL(collectServerUrl.toExternalForm() + "?appName="
553                     + URLEncoder.encode(appName, "UTF-8") + "&appUrls="
554                     // "UTF-8" as said in javadoc
555                     + URLEncoder.encode(applicationNodeUrl.toExternalForm(), "UTF-8")
556                     + "&action=registerNode");
557             unregisterApplicationNodeInCollectServerUrl = new URL(
558                     registerUrl.toExternalForm().replace("registerNode""unregisterNode"));
559         } catch (final IOException e) {
560             // can't happen if urls are ok
561             throw new IllegalArgumentException(e);
562         }
563
564         // this is an asynchronous call because if this method is called when the webapp is starting,
565         // the webapp can not respond to the collect server for the first collect of data
566         final Thread thread = new Thread("javamelody registerApplicationNodeInCollectServer") {
567             @Override
568             public void run() {
569                 try {
570                     Thread.sleep(10000);
571                 } catch (final InterruptedException e) {
572                     throw new IllegalStateException(e);
573                 }
574                 try {
575                     new LabradorRetriever(registerUrl).post(null);
576                     LOG.info("application node added to the collect server");
577                 } catch (final IOException e) {
578                     LOG.warn("Unable to register application's node in the collect server ( " + e
579                             + ')', e);
580                 }
581             }
582         };
583         thread.setDaemon(true);
584         thread.start();
585     }
586
587     /**
588      * Call the optional collect server to unregister this application's node.
589      * @throws IOException e
590      */

591     public static void unregisterApplicationNodeInCollectServer() throws IOException {
592         if (unregisterApplicationNodeInCollectServerUrl != null) {
593             new LabradorRetriever(unregisterApplicationNodeInCollectServerUrl).post(null);
594             LOG.info("application node removed from the collect server");
595         }
596     }
597 }
598