/* * 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 java.io.IOException; import java.net.URL; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import net.bull.javamelody.SamplingProfiler.SampledMethod; import org.apache.log4j.Logger; /** * Collecteur de données du serveur de collecte centralisé. * @author Emeric Vernat */ class CollectorServer { static final Logger LOGGER = Logger.getLogger("javamelody"); private static final int NB_COLLECT_THREADS = 10; private final Map<String, Throwable> lastCollectExceptionsByApplication = new ConcurrentHashMap<String, Throwable>(); private final Map<String, RemoteCollector> remoteCollectorsByApplication = new ConcurrentHashMap<String, RemoteCollector>(); private final ExecutorService executorService = Executors .newFixedThreadPool(NB_COLLECT_THREADS); private final Timer timer; /** * Constructeur. * @throws IOException e */ CollectorServer() throws IOException { super(); boolean initOk = false; this.timer = new Timer("collector", true); try { LOGGER.info("reading applications list from: " + Parameters.getCollectorApplicationsFile()); final Map<String, List<URL>> urlsByApplication = Parameters .getCollectorUrlsByApplications(); LOGGER.info("monitored applications: " + urlsByApplication.keySet()); LOGGER.info("urls of monitored applications: " + urlsByApplication); final int periodMillis = Parameters.getResolutionSeconds() * 1000; LOGGER.info("resolution of the monitoring in seconds: " + Parameters.getResolutionSeconds()); final TimerTask collectTask = new TimerTask() { /** {@inheritDoc} */ @Override public void run() { // il ne doit pas y avoir d'erreur dans cette task collectWithoutErrors(); // cette collecte ne peut interférer avec un autre thread, // car les compteurs sont mis à jour et utilisés par le même timer // et donc le même thread (les différentes tasks ne peuvent se chevaucher) } }; // on schedule la tâche de fond, // avec une exécution de suite en asynchrone pour initialiser les données timer.schedule(collectTask, 100, periodMillis); JRobin.initBackendFactory(timer); initOk = true; } finally { if (!initOk) { // si exception dans initialisation, on annule la création du timer // (sinon tomcat ne serait pas content) timer.cancel(); } } } void collectWithoutErrors() { // clone pour éviter ConcurrentModificationException final Map<String, List<URL>> clone; try { clone = new LinkedHashMap<String, List<URL>>( Parameters.getCollectorUrlsByApplications()); } catch (final IOException e) { LOGGER.warn(e.getMessage(), e); return; } for (final Map.Entry<String, List<URL>> entry : clone.entrySet()) { final String application = entry.getKey(); final List<URL> urls = entry.getValue(); executorService.submit(new Runnable() { @Override public void run() { collectForApplicationWithoutErrors(application, urls); } }); } } String collectForApplicationForAction(String application, List<URL> urls) throws IOException { return collectForApplication(new RemoteCollector(application, urls)); } void collectForApplicationWithoutErrors(String application, List<URL> urls) { try { collectForApplication(application, urls); final boolean becameAvailable = lastCollectExceptionsByApplication .containsKey(application); lastCollectExceptionsByApplication.remove(application); if (becameAvailable) { final String subject = "The application " + application + " is available again for the monitoring server"; notifyAdmins(subject, subject); } } catch (final Throwable e) { // NOPMD // si erreur sur une webapp (indisponibilité par exemple), on continue avec les autres // et il ne doit y avoir aucune erreur dans cette task try { LOGGER.warn("exception while collecting data for application " + application); LOGGER.warn(e.toString(), e); final boolean becameUnavailable = !lastCollectExceptionsByApplication .containsKey(application); lastCollectExceptionsByApplication.put(application, e); if (becameUnavailable) { final String subject = "The application " + application + " is unavailable for the monitoring server"; final String message = subject + "\n\nCause:\n" + e.toString(); notifyAdmins(subject, message); } } catch (final Throwable e2) { // NOPMD // tant pis, on continue quand même return; } } } String collectForApplication(String application, List<URL> urls) throws IOException { final boolean remoteCollectorAvailable = isApplicationDataAvailable(application); final RemoteCollector remoteCollector; if (!remoteCollectorAvailable) { remoteCollector = new RemoteCollector(application, urls); } else { remoteCollector = getRemoteCollectorByApplication(application); } final String messageForReport = collectForApplication(remoteCollector); if (!remoteCollectorAvailable) { // on initialise les remoteCollectors au fur et à mesure // puisqu'on ne peut pas forcément au démarrage // car la webapp à monitorer peut être indisponible remoteCollectorsByApplication.put(application, remoteCollector); if (Parameters.getParameter(Parameter.MAIL_SESSION) != null && Parameters.getParameter(Parameter.ADMIN_EMAILS) != null) { scheduleReportMailForCollectorServer(application); LOGGER.info("Periodic report scheduled for the application " + application + " to " + Parameters.getParameter(Parameter.ADMIN_EMAILS)); } } return messageForReport; } private String collectForApplication(RemoteCollector remoteCollector) throws IOException { final String application = remoteCollector.getApplication(); final List<URL> urls = remoteCollector.getURLs(); LOGGER.info("collect for the application " + application + " on " + urls); assert application != null; assert urls != null; final long start = System.currentTimeMillis(); final String messageForReport = remoteCollector.collectData(); final List<JavaInformations> javaInformationsList = remoteCollector .getJavaInformationsList(); final Collector collector = remoteCollector.getCollector(); collector.collectWithoutErrors(javaInformationsList); LOGGER.info("collect for the application " + application + " done in " + (System.currentTimeMillis() - start) + "ms"); if (LOGGER.isDebugEnabled()) { LOGGER.debug("counters " + application + " : " + collector.getCounters()); LOGGER.debug("javaInformations " + application + " : " + javaInformationsList); if (messageForReport != null) { LOGGER.debug("message " + application + " : " + messageForReport.replace("\n", ", ")); } } return messageForReport; } List<SessionInformations> collectSessionInformations(String application, String sessionId) throws IOException { return getRemoteCollectorByApplication(application).collectSessionInformations(sessionId); } List<SampledMethod> collectHotspots(String application) throws IOException { return getRemoteCollectorByApplication(application).collectHotspots(); } HeapHistogram collectHeapHistogram(String application) throws IOException { return getRemoteCollectorByApplication(application).collectHeapHistogram(); } DatabaseInformations collectDatabaseInformations(String application, int requestIndex) throws IOException { return getRemoteCollectorByApplication(application).collectDatabaseInformations( requestIndex); } List<List<ConnectionInformations>> collectConnectionInformations(String application) throws IOException { return getRemoteCollectorByApplication(application).collectConnectionInformations(); } String collectSqlRequestExplainPlan(String application, String sqlRequest) throws IOException { return getRemoteCollectorByApplication(application) .collectSqlRequestExplainPlan(sqlRequest); } Map<String, List<ProcessInformations>> collectProcessInformations(String application) throws IOException { return getRemoteCollectorByApplication(application).collectProcessInformations(); } List<JndiBinding> collectJndiBindings(String application, String path) throws IOException { return getRemoteCollectorByApplication(application).collectJndiBindings(path); } Map<String, List<MBeanNode>> collectMBeans(String application) throws IOException { return getRemoteCollectorByApplication(application).collectMBeans(); } Map<JavaInformations, List<CounterRequestContext>> collectCurrentRequests(String application) throws IOException { return getRemoteCollectorByApplication(application).collectCurrentRequests(); } List<List<ThreadInformations>> getThreadInformationsLists(String application) { return getRemoteCollectorByApplication(application).getThreadInformationsLists(); } void addCollectorApplication(String application, List<URL> urls) throws IOException { final Map<String, List<URL>> collectorUrlsByApplications = Parameters .getCollectorUrlsByApplications(); for (final URL addedUrl : urls) { final String addedUrlInExternalForm = addedUrl.toExternalForm(); for (final Map.Entry<String, List<URL>> entry : collectorUrlsByApplications.entrySet()) { for (final URL existingUrl : entry.getValue()) { if (existingUrl.toExternalForm().equals(addedUrlInExternalForm)) { throw new IOException("The URL " + addedUrlInExternalForm.substring(0, addedUrlInExternalForm.lastIndexOf('/')) + " has already been added in the application " + entry.getKey() + ". You can't monitor an application instance twice."); } } } } collectForApplication(application, urls); Parameters.addCollectorApplication(application, urls); } void removeCollectorApplication(String application) throws IOException { Parameters.removeCollectorApplication(application); remoteCollectorsByApplication.remove(application); } /** * Retourne le collector pour une application à partir de son code. * @param application Code de l'application * @return Collector */ Collector getCollectorByApplication(String application) { // application peut être null if (application == null) { return null; } final RemoteCollector remoteCollector = remoteCollectorsByApplication.get(application); if (remoteCollector == null) { return null; } return remoteCollector.getCollector(); } /** * Retourne la liste des informations java à partir du code de l'application. * @param application Code de l'application * @return Liste de JavaInformations */ List<JavaInformations> getJavaInformationsByApplication(String application) { // application peut être null if (application == null) { return null; } final RemoteCollector remoteCollector = remoteCollectorsByApplication.get(application); if (remoteCollector == null) { return null; } return remoteCollector.getJavaInformationsList(); } private RemoteCollector getRemoteCollectorByApplication(String application) { assert application != null; final RemoteCollector remoteCollector = remoteCollectorsByApplication.get(application); assert remoteCollector != null; return remoteCollector; } /** * Retourne true si les données d'une application sont disponibles (c'est-à-dire si au moins * une communication avec l'application a pu avoir lieu) * @param application Code l'application * @return boolean */ boolean isApplicationDataAvailable(String application) { assert application != null; return remoteCollectorsByApplication.containsKey(application); } /** * Retourne le code de la première application dans la liste * @return String */ String getFirstApplication() { if (remoteCollectorsByApplication.isEmpty()) { return null; } return remoteCollectorsByApplication.keySet().iterator().next(); } /** * Retourne la map des dernières erreurs de collecte par codes d'applications ou null * si la dernière collecte pour l'application s'est exécutée sans exception. * @return Map */ Map<String, Throwable> getLastCollectExceptionsByApplication() { return Collections.unmodifiableMap(lastCollectExceptionsByApplication); } private void notifyAdmins(String subject, String message) { final String mailSession = Parameters.getParameter(Parameter.MAIL_SESSION); final String adminEmails = Parameters.getParameter(Parameter.ADMIN_EMAILS); if (mailSession != null && adminEmails != null) { final Mailer mailer = new Mailer(mailSession); try { mailer.send(adminEmails, subject, message, null, false); } catch (final Exception e) { LOGGER.warn(e.toString(), e); } } } void scheduleReportMailForCollectorServer(String application) { assert application != null; for (final Period period : MailReport.getMailPeriods()) { scheduleReportMailForCollectorServer(application, period); } } void scheduleReportMailForCollectorServer(final String application, final Period period) { assert application != null; assert period != null; final TimerTask task = new TimerTask() { /** {@inheritDoc} */ @Override public void run() { try { // envoi du rapport final Collector collector = getCollectorByApplication(application); final List<JavaInformations> javaInformationsList = getJavaInformationsByApplication(application); new MailReport().sendReportMail(collector, true, javaInformationsList, period); } catch (final Throwable t) { // NOPMD // pas d'erreur dans cette task LOG.warn("sending mail report failed", t); } // on reschedule à la même heure la semaine suivante sans utiliser de période de 24h*7 // car certains jours font 23h ou 25h et on ne veut pas introduire de décalage scheduleReportMailForCollectorServer(application, period); } }; // schedule 1 fois la tâche timer.schedule(task, MailReport.getNextExecutionDate(period)); } /** * Stoppe les collectes dans ce serveur de collecte et purge les données. */ void stop() { // stoppe le timer timer.cancel(); // stoppe les threads de collecte, en attendant qu'ils terminent les tâches en cours executorService.shutdown(); for (final RemoteCollector remoteCollector : remoteCollectorsByApplication.values()) { remoteCollector.getCollector().stop(); } // nettoyage avant le retrait de la webapp au cas où celui-ci ne suffise pas remoteCollectorsByApplication.clear(); } static List<URL> getUrlsByApplication(String application) throws IOException { assert application != null; return Parameters.getCollectorUrlsByApplications().get(application); } }