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.internal.model;
19
20 import java.io.File;
21 import java.io.IOException;
22 import java.util.ArrayList;
23 import java.util.Calendar;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.Date;
27 import java.util.HashMap;
28 import java.util.Iterator;
29 import java.util.LinkedHashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.concurrent.ConcurrentHashMap;
33
34 import net.bull.javamelody.JdbcWrapper;
35 import net.bull.javamelody.internal.common.LOG;
36 import net.bull.javamelody.internal.common.Parameters;
37 import net.bull.javamelody.internal.model.Counter.CounterRequestContextComparator;
38 import net.bull.javamelody.internal.model.SamplingProfiler.SampledMethod;
39 import net.bull.javamelody.internal.publish.MetricsPublisher;
40
41 /**
42  * Collecteur de données sur les compteurs, avec son propre thread, pour remplir les courbes.
43  * @author Emeric Vernat
44  */

45 public class Collector { // NOPMD
46     private static final long NOT_A_NUMBER = Long.MIN_VALUE;
47
48     // période entre 2 collectes en milli-secondes
49     private final int periodMillis;
50     private final String application;
51     private final List<Counter> counters;
52     private final SamplingProfiler samplingProfiler;
53     private final Map<String, JRobin> requestJRobinsById = new ConcurrentHashMap<>();
54     // les instances jrobins des compteurs sont créées à l'initialisation
55     private final Map<String, JRobin> counterJRobins = new LinkedHashMap<>();
56     private final Map<String, JRobin> otherJRobins = new LinkedHashMap<>();
57     // globalRequestsByCounter, requestsById, dayCountersByCounter et cpuTimeMillis
58     // sont utilisés par un seul thread lors des collectes,
59     // (et la méthode centrale "collect" est synchronisée pour éviter un accès concurrent
60     // avec la mise à jour avant le rapport html)
61     private final Map<Counter, CounterRequest> globalRequestsByCounter = new HashMap<>();
62     private final Map<String, CounterRequest> requestsById = new HashMap<>();
63     private final Map<Counter, Counter> dayCountersByCounter = new LinkedHashMap<>();
64     private final Map<Counter, Boolean> firstCollectDoneByCounter = new HashMap<>();
65     private long transactionCount = NOT_A_NUMBER;
66     private long cpuTimeMillis = NOT_A_NUMBER;
67     private long gcTimeMillis = NOT_A_NUMBER;
68     private long tomcatBytesReceived = NOT_A_NUMBER;
69     private long tomcatBytesSent = NOT_A_NUMBER;
70     private long lastCollectDuration;
71     private Throwable lastCollectorException;
72     private long estimatedMemorySize;
73     private long diskUsage;
74     private Date lastDateOfDeletedObsoleteFiles = new Date();
75     private boolean stopped;
76     private final boolean noDatabase = Parameters.isNoDatabase();
77     private List<MetricsPublisher> metricsPublishers;
78     private final WebappVersions webappVersions;
79     private final StorageLock storageLock;
80
81     /**
82      * Constructeur.
83      * @param application Code de l'application
84      * @param counters Liste des counters
85      */

86     public Collector(String application, List<Counter> counters) {
87         this(application, counters, null);
88     }
89
90     /**
91      * Constructeur.
92      * @param application Code de l'application
93      * @param counters Liste des counters
94      * @param samplingProfiler SamplingProfiler
95      */

96     public Collector(String application, List<Counter> counters,
97             SamplingProfiler samplingProfiler) {
98         super();
99         assert application != null;
100         assert counters != null;
101         this.application = application;
102         this.counters = Collections.unmodifiableList(new ArrayList<>(counters));
103         this.samplingProfiler = samplingProfiler;
104         // c'est le collector qui fixe le nom de l'application (avant la lecture des éventuels fichiers)
105         for (final Counter counter : counters) {
106             for (final Counter otherCounter : counters) {
107                 // on vérifie que les noms des compteurs sont uniques entre eux
108                 assert counter == otherCounter || !counter.getName().equals(otherCounter.getName());
109             }
110             counter.setApplication(application);
111             final Counter dayCounter = new PeriodCounterFactory(counter)
112                     .createDayCounterAtDate(new Date());
113             dayCountersByCounter.put(counter, dayCounter);
114         }
115         periodMillis = Parameters.getResolutionSeconds() * 1000;
116
117         try {
118             // on relit les compteurs à l'initialisation pour récupérer les stats;
119             // d'abord les compteurs non temporels, au cas où les compteurs par jour soient illisibles,
120             for (final Counter counter : counters) {
121                 counter.readFromFile();
122             }
123             // et seulement ensuite les compteurs du jour
124             for (final Counter counter : counters) {
125                 dayCountersByCounter.get(counter).readFromFile();
126             }
127             LOG.debug("counters data read from files in "
128                     + Parameters.getStorageDirectory(application));
129         } catch (final IOException e) {
130             // lecture échouée, tant pis
131             // (on n'interrompt pas toute l'initialisation juste pour un fichier illisible)
132             LOG.warn("exception while reading counters data from files in "
133                     + Parameters.getStorageDirectory(application), e);
134         }
135
136         // puis pose le lock
137         this.storageLock = new StorageLock(application);
138
139         this.webappVersions = new WebappVersions(application);
140     }
141
142     /**
143      * Retourne le code de l'application.
144      * @return String
145      */

146     public String getApplication() {
147         return application;
148     }
149
150     /**
151      * Retourne le {@link SamplingProfiler}.
152      * @return SamplingProfiler
153      */

154     public SamplingProfiler getSamplingProfiler() {
155         return samplingProfiler;
156     }
157
158     public List<SampledMethod> getHotspots() {
159         if (samplingProfiler == null) {
160             throw new IllegalStateException("Hotspots sampling is not enabled in this server");
161         }
162         return samplingProfiler.getHotspots(1000);
163     }
164
165     public Map<String, Date> getDatesByWebappVersions() {
166         return webappVersions.getDatesByVersions();
167     }
168
169     /**
170      * @return La liste des {@link Counter}s de ce collector
171      */

172     public List<Counter> getCounters() {
173         return counters;
174     }
175
176     /**
177      * @param counterName Nom d'un counter
178      * @return Le counter de ce collector ayant ce nom ou null si non trouvé
179      */

180     public Counter getCounterByName(String counterName) {
181         for (final Counter counter : counters) {
182             if (counter.getName().equals(counterName)) {
183                 return counter;
184             }
185         }
186         return null;
187     }
188
189     public Counter getCounterByRequestId(CounterRequest request) {
190         final String requestId = request.getId();
191         for (final Counter counter : counters) {
192             if (counter.isRequestIdFromThisCounter(requestId)) {
193                 return counter;
194             }
195         }
196         return null;
197     }
198
199     public List<CounterRequestContext> getRootCurrentContexts(List<Counter> newParentCounters) {
200         final List<CounterRequestContext> rootCurrentContexts = new ArrayList<>();
201         for (final Counter counter : counters) {
202             if (counter.isDisplayed()) {
203                 // a priori, les contextes root courants sont dans le compteur http
204                 // mais il est possible qu'il y en ait aussi dans ejb ou sql sans parent dans http
205                 rootCurrentContexts.addAll(counter.getOrderedRootCurrentContexts());
206             }
207         }
208         if (rootCurrentContexts.size() > 1) {
209             Collections.sort(rootCurrentContexts, Collections
210                     .reverseOrder(new CounterRequestContextComparator(System.currentTimeMillis())));
211
212             CounterRequestContext.replaceParentCounters(rootCurrentContexts, newParentCounters);
213         }
214         return rootCurrentContexts;
215     }
216
217     public long getLastCollectDuration() {
218         return lastCollectDuration;
219     }
220
221     public Throwable getLastCollectorException() {
222         return lastCollectorException;
223     }
224
225     public long getEstimatedMemorySize() {
226         return estimatedMemorySize;
227     }
228
229     public long getDiskUsage() {
230         if (diskUsage == 0) {
231             // si diskUsage == 0, le serveur a été démarré ce jour et la taille totale des fichiers
232             // n'a pas encore été calculée lors de la purge des fichiers obsolètes,
233             // donc on la calcule ici
234             final File storageDir = Parameters.getStorageDirectory(application);
235             long sum = 0;
236             final File[] files = storageDir.listFiles();
237             if (files != null) {
238                 for (final File file : files) {
239                     sum += file.length();
240                 }
241             }
242             diskUsage = sum;
243         }
244         return diskUsage;
245     }
246
247     public List<Counter> getRangeCounters(Range range) throws IOException {
248         if (range.getPeriod() == Period.TOUT) {
249             return new ArrayList<>(counters);
250         }
251         final Collection<Counter> currentDayCounters = dayCountersByCounter.values();
252         final List<Counter> result = new ArrayList<>(currentDayCounters.size());
253         for (final Counter dayCounter : currentDayCounters) {
254             final Counter counter = getRangeCounter(range, dayCounter);
255             result.add(counter);
256         }
257         return result;
258     }
259
260     private Counter getRangeCounter(Range range, Counter dayCounter) throws IOException {
261         final PeriodCounterFactory periodCounterFactory = new PeriodCounterFactory(dayCounter);
262         final Counter counter;
263         if (range.getPeriod() == null) {
264             counter = periodCounterFactory.getCustomCounter(range);
265         } else {
266             switch (range.getPeriod()) {
267             case JOUR:
268                 counter = periodCounterFactory.getDayCounter();
269                 break;
270             case SEMAINE:
271                 counter = periodCounterFactory.getWeekCounter();
272                 break;
273             case MOIS:
274                 counter = periodCounterFactory.getMonthCounter();
275                 break;
276             case ANNEE:
277                 counter = periodCounterFactory.getYearCounter();
278                 break;
279             case TOUT:
280                 throw new IllegalStateException(range.getPeriod().toString());
281             default:
282                 throw new IllegalArgumentException(range.getPeriod().toString());
283             }
284         }
285         return counter;
286     }
287
288     public List<Counter> getRangeCountersToBeDisplayed(Range range) throws IOException {
289         final List<Counter> result = new ArrayList<>(getRangeCounters(range));
290         final Iterator<Counter> it = result.iterator();
291         while (it.hasNext()) {
292             final Counter counter = it.next();
293             if (!counter.isDisplayed() || counter.isJobCounter()) {
294                 it.remove();
295             }
296         }
297         return Collections.unmodifiableList(result);
298     }
299
300     public Counter getRangeCounter(Range range, String counterName) throws IOException {
301         final Counter counter = getCounterByName(counterName);
302         if (counter == null) {
303             throw new IllegalArgumentException(counterName);
304         }
305         if (range.getPeriod() == Period.TOUT) {
306             return counter;
307         }
308         return getRangeCounter(range, dayCountersByCounter.get(counter));
309     }
310
311     public void collectLocalContextWithoutErrors() {
312         // ici on n'inclue pas les informations de la bdd et des threads
313         // car on n'en a pas besoin pour la collecte et cela économise des requêtes sql
314         try {
315             final JavaInformations javaInformations = new JavaInformations(
316                     Parameters.getServletContext(), false);
317
318             collectWithoutErrors(Collections.singletonList(javaInformations));
319         } catch (final Throwable t) { // NOPMD
320             // include cause in message for debugging logs in the report
321             LOG.warn("exception while collecting data: " + t, t);
322         }
323     }
324
325     public void collectWithoutErrors(List<JavaInformations> javaInformationsList) {
326         assert javaInformationsList != null;
327         final long start = System.currentTimeMillis();
328         try {
329             estimatedMemorySize = collect(javaInformationsList);
330             lastCollectorException = null;
331         } catch (final Throwable t) { // NOPMD
332             lastCollectorException = t;
333             // include cause in message for debugging logs in the report
334             LOG.warn("exception while collecting data: " + t, t);
335         }
336         // note : on n'inclue pas "new JavaInformations" de collectLocalContextWithoutErrors
337         // dans la durée de la collecte mais il est inférieur à 1 ms (sans bdd)
338         lastCollectDuration = Math.max(0, System.currentTimeMillis() - start);
339     }
340
341     private synchronized long collect(List<JavaInformations> javaInformationsList)
342             throws IOException {
343         long memorySize = 0;
344         try {
345             // si pas d'informations, on ne met pas 0 : on ne met rien
346             if (!javaInformationsList.isEmpty()) {
347                 if (metricsPublishers == null) {
348                     metricsPublishers = MetricsPublisher.getMetricsPublishers(javaInformationsList);
349                 }
350                 collectJavaInformations(javaInformationsList);
351                 collectOtherJavaInformations(javaInformationsList);
352                 collectTomcatInformations(javaInformationsList);
353             }
354             for (final Counter counter : counters) {
355                 // counter.isDisplayed() peut changer pour spring, ejb, guice ou services selon l'utilisation
356                 dayCountersByCounter.get(counter).setDisplayed(counter.isDisplayed());
357                 // collecte pour chaque compteur (hits par minute, temps moyen, % d'erreurs système)
358                 // Rq : il serait possible d'ajouter le débit total en Ko / minute (pour http)
359                 // mais autant monitorer les vrais débits réseaux au niveau de l'OS
360                 if (counter.isDisplayed()) {
361                     // si le compteur n'est pas affiché (par ex ejb), pas de collecte
362                     // et pas de persistance de fichiers jrobin ou du compteur
363                     memorySize += collectCounterData(counter);
364                 }
365             }
366         } finally {
367             if (metricsPublishers != null) {
368                 for (final MetricsPublisher metricsPublisher : metricsPublishers) {
369                     metricsPublisher.send();
370                 }
371             }
372         }
373
374         final Calendar calendar = Calendar.getInstance();
375         final int currentDayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
376         calendar.setTime(lastDateOfDeletedObsoleteFiles);
377         if (calendar.get(Calendar.DAY_OF_YEAR) != currentDayOfYear) {
378             // 1 fois par jour on supprime tous les fichiers .ser.gz obsolètes (modifiés il y a plus d'un an)
379             // et tous les fichiers .rrd obsolètes (modifiés il y a plus de 3 mois)
380             try {
381                 deleteObsoleteFiles();
382             } finally {
383                 lastDateOfDeletedObsoleteFiles = new Date();
384             }
385         }
386
387         if (!javaInformationsList.isEmpty()) {
388             final String webappVersion = javaInformationsList.get(0).getWebappVersion();
389             webappVersions.addVersionIfNeeded(webappVersion);
390         }
391
392         return memorySize;
393     }
394
395     private void collectJavaInformations(List<JavaInformations> javaInformationsList)
396             throws IOException {
397         long usedMemory = 0;
398         long processesCpuTimeMillis = 0;
399         int availableProcessors = 0;
400         int sessionCount = 0;
401         int activeThreadCount = 0;
402         int activeConnectionCount = 0;
403         int usedConnectionCount = 0;
404
405         for (final JavaInformations javaInformations : javaInformationsList) {
406             final MemoryInformations memoryInformations = javaInformations.getMemoryInformations();
407             usedMemory = add(memoryInformations.getUsedMemory(), usedMemory);
408             sessionCount = add(javaInformations.getSessionCount(), sessionCount);
409             activeThreadCount = add(javaInformations.getActiveThreadCount(), activeThreadCount);
410             activeConnectionCount = add(javaInformations.getActiveConnectionCount(),
411                     activeConnectionCount);
412             usedConnectionCount = add(javaInformations.getUsedConnectionCount(),
413                     usedConnectionCount);
414
415             // il y a au moins 1 coeur
416             availableProcessors = add(Math.max(javaInformations.getAvailableProcessors(), 1),
417                     availableProcessors);
418             // processesCpuTime n'est supporté que par le jdk sun
419             processesCpuTimeMillis = add(javaInformations.getProcessCpuTimeMillis(),
420                     processesCpuTimeMillis);
421         }
422         collectJRobinValues(usedMemory, processesCpuTimeMillis, availableProcessors, sessionCount,
423                 activeThreadCount, activeConnectionCount, usedConnectionCount);
424     }
425
426     // CHECKSTYLE:OFF
427     private void collectOtherJavaInformations(List<JavaInformations> javaInformationsList) // NOPMD
428             throws IOException {
429         // CHECKSTYLE:ON
430         long usedNonHeapMemory = 0;
431         long usedBufferedMemory = 0;
432         int loadedClassesCount = 0;
433         long garbageCollectionTimeMillis = 0;
434         long usedPhysicalMemorySize = 0;
435         long usedSwapSpaceSize = 0;
436         int availableProcessors = 0;
437         int sessionCount = 0;
438         long sessionAgeSum = 0;
439         int threadCount = 0;
440         long databaseTransactionCount = 0;
441         double systemLoadAverage = 0;
442         long unixOpenFileDescriptorCount = 0;
443         long freeDiskSpaceInTemp = Long.MAX_VALUE;
444         long usableDiskSpaceInTemp = Long.MAX_VALUE;
445         double systemCpuLoad = 0;
446
447         for (final JavaInformations javaInformations : javaInformationsList) {
448             final MemoryInformations memoryInformations = javaInformations.getMemoryInformations();
449             sessionCount = add(javaInformations.getSessionCount(), sessionCount);
450             sessionAgeSum = add(javaInformations.getSessionAgeSum(), sessionAgeSum);
451             threadCount = add(javaInformations.getThreadCount(), threadCount);
452             databaseTransactionCount = add(javaInformations.getTransactionCount(),
453                     databaseTransactionCount);
454             // il y a au moins 1 coeur
455             availableProcessors = add(Math.max(javaInformations.getAvailableProcessors(), 1),
456                     availableProcessors);
457             usedNonHeapMemory = add(memoryInformations.getUsedNonHeapMemory(), usedNonHeapMemory);
458             usedBufferedMemory = add(memoryInformations.getUsedBufferedMemory(),
459                     usedBufferedMemory);
460             loadedClassesCount = add(memoryInformations.getLoadedClassesCount(),
461                     loadedClassesCount);
462             usedPhysicalMemorySize = add(memoryInformations.getUsedPhysicalMemorySize(),
463                     usedPhysicalMemorySize);
464             usedSwapSpaceSize = add(memoryInformations.getUsedSwapSpaceSize(), usedSwapSpaceSize);
465             garbageCollectionTimeMillis = add(memoryInformations.getGarbageCollectionTimeMillis(),
466                     garbageCollectionTimeMillis);
467             // systemLoadAverage n'est supporté qu'à partir du jdk 1.6 sur linux ou unix
468             systemLoadAverage = add(javaInformations.getSystemLoadAverage(), systemLoadAverage);
469             // que sur linx ou unix
470             unixOpenFileDescriptorCount = add(javaInformations.getUnixOpenFileDescriptorCount(),
471                     unixOpenFileDescriptorCount);
472             // la valeur retenue est le minimum entre les serveurs
473             freeDiskSpaceInTemp = Math.min(javaInformations.getFreeDiskSpaceInTemp(),
474                     freeDiskSpaceInTemp);
475             usableDiskSpaceInTemp = Math.min(javaInformations.getUsableDiskSpaceInTemp(),
476                     usableDiskSpaceInTemp);
477             systemCpuLoad = add(javaInformations.getSystemCpuLoad(), systemCpuLoad);
478         }
479
480         // collecte du pourcentage de temps en ramasse-miette
481         if (garbageCollectionTimeMillis >= 0) {
482             if (this.gcTimeMillis != NOT_A_NUMBER) {
483                 // %gc = delta(somme(Temps GC)) / période / nb total de coeurs
484                 final int gcPercentage = Math
485                         .min((int) ((garbageCollectionTimeMillis - this.gcTimeMillis) * 100
486                                 / periodMillis / availableProcessors), 100);
487                 addJRobinValue(getOtherJRobin("gc"), gcPercentage);
488             } else {
489                 addJRobinValue(getOtherJRobin("gc"), 0d);
490             }
491             this.gcTimeMillis = garbageCollectionTimeMillis;
492         }
493
494         final Map<String, Double> otherJRobinsValues = new LinkedHashMap<>();
495         otherJRobinsValues.put("threadCount", (double) threadCount);
496         otherJRobinsValues.put("loadedClassesCount", (double) loadedClassesCount);
497         otherJRobinsValues.put("usedBufferedMemory", (double) usedBufferedMemory);
498         otherJRobinsValues.put("usedNonHeapMemory", (double) usedNonHeapMemory);
499         otherJRobinsValues.put("usedPhysicalMemorySize", (double) usedPhysicalMemorySize);
500         otherJRobinsValues.put("usedSwapSpaceSize", (double) usedSwapSpaceSize);
501         otherJRobinsValues.put("systemLoad", systemLoadAverage);
502         otherJRobinsValues.put("systemCpuLoad", systemCpuLoad / javaInformationsList.size());
503         otherJRobinsValues.put("fileDescriptors", (double) unixOpenFileDescriptorCount);
504         for (final Map.Entry<String, Double> entry : otherJRobinsValues.entrySet()) {
505             if (entry.getValue() >= 0) {
506                 addJRobinValue(getOtherJRobin(entry.getKey()), entry.getValue());
507             }
508         }
509
510         collectSessionsMeanAge(sessionAgeSum, sessionCount);
511
512         if (!noDatabase) {
513             // collecte du nombre de transactions base de données par minute
514             if (this.transactionCount != NOT_A_NUMBER) {
515                 final double periodMinutes = periodMillis / 60000d;
516                 addJRobinValue(getOtherJRobin("transactionsRate"),
517                         (databaseTransactionCount - this.transactionCount) / periodMinutes);
518             } else {
519                 addJRobinValue(getOtherJRobin("transactionsRate"), 0d);
520             }
521             this.transactionCount = databaseTransactionCount;
522         }
523
524         if (freeDiskSpaceInTemp != Long.MAX_VALUE) {
525             addJRobinValue(getOtherJRobin("Free_disk_space"), freeDiskSpaceInTemp);
526         }
527         if (usableDiskSpaceInTemp != Long.MAX_VALUE) {
528             addJRobinValue(getOtherJRobin("Usable_disk_space"), usableDiskSpaceInTemp);
529         }
530
531         // on pourrait collecter la valeur 100 dans jrobin pour qu'il fasse la moyenne
532         // du pourcentage de disponibilité, mais cela n'aurait pas de sens sans
533         // différenciation des indisponibilités prévues de celles non prévues
534     }
535
536     private void collectTomcatInformations(List<JavaInformations> javaInformationsList)
537             throws IOException {
538         int tomcatBusyThreads = 0;
539         long bytesReceived = 0;
540         long bytesSent = 0;
541         boolean tomcatUsed = false;
542
543         for (final JavaInformations javaInformations : javaInformationsList) {
544             for (final TomcatInformations tomcatInformations : javaInformations
545                     .getTomcatInformationsList()) {
546                 tomcatBusyThreads = add(tomcatInformations.getCurrentThreadsBusy(),
547                         tomcatBusyThreads);
548                 bytesReceived = add(tomcatInformations.getBytesReceived(), bytesReceived);
549                 bytesSent = add(tomcatInformations.getBytesSent(), bytesSent);
550                 tomcatUsed = true;
551             }
552         }
553
554         if (tomcatUsed) {
555             // collecte des informations de Tomcat
556             collectTomcatValues(tomcatBusyThreads, bytesReceived, bytesSent);
557         }
558     }
559
560     private void collectJRobinValues(long usedMemory, long processesCpuTimeMillis,
561             int availableProcessors, int sessionCount, int activeThreadCount,
562             int activeConnectionCount, int usedConnectionCount) throws IOException {
563         // collecte de la mémoire java
564         addJRobinValue(getCounterJRobin("usedMemory"), usedMemory);
565
566         // collecte du pourcentage d'utilisation cpu
567         if (processesCpuTimeMillis >= 0) {
568             // processesCpuTimeMillis est la somme pour tous les serveurs (et pour tous les coeurs)
569             // donc ce temps peut être n fois supérieur à periodMillis
570             // où n est le nombre total de coeurs sur tous les serveurs (si cluster);
571             // et cpuPercentage s'approchera à pleine charge de 100
572             // quel que soit le nombre de serveurs ou de coeurs;
573             // cpuPercentage ne peut être supérieur à 100
574             // car ce serait une valeur aberrante due aux imprécisions de mesure
575
576             if (this.cpuTimeMillis != NOT_A_NUMBER) {
577                 // en gros, %cpu = delta(somme(Temps cpu)) / période / nb total de coeurs
578                 final int cpuPercentage = Math
579                         .min((int) ((processesCpuTimeMillis - this.cpuTimeMillis) * 100
580                                 / periodMillis / availableProcessors), 100);
581                 addJRobinValue(getCounterJRobin("cpu"), cpuPercentage);
582             } else {
583                 addJRobinValue(getCounterJRobin("cpu"), 0d);
584             }
585             this.cpuTimeMillis = processesCpuTimeMillis;
586         }
587
588         // si ce collector est celui des nodes Jenkins, il n'y a pas de requêtes http
589         // donc il ne peut pas y avoir de sessions http ou de threads actifs
590         if (getCounterByName(Counter.HTTP_COUNTER_NAME) != null) {
591             // collecte du nombre de sessions http
592             if (sessionCount >= 0) {
593                 addJRobinValue(getCounterJRobin("httpSessions"), sessionCount);
594             }
595             // collecte du nombre de threads actifs (requêtes http en cours)
596             addJRobinValue(getCounterJRobin("activeThreads"), activeThreadCount);
597         }
598         if (!noDatabase) {
599             // collecte du nombre de connexions jdbc actives et du nombre de connexions jdbc ouvertes
600             addJRobinValue(getCounterJRobin("activeConnections"), activeConnectionCount);
601             addJRobinValue(getCounterJRobin("usedConnections"), usedConnectionCount);
602         }
603
604         // si ce collector est celui des nodes Jenkins, on collecte le nombre de builds en cours
605         // pour le graphique
606         if (getCounterByName(Counter.BUILDS_COUNTER_NAME) != null) {
607             addJRobinValue(getCounterJRobin("runningBuilds"), JdbcWrapper.getRunningBuildCount());
608             addJRobinValue(getCounterJRobin("buildQueueLength"), JdbcWrapper.getBuildQueueLength());
609             addJRobinValue(getCounterJRobin("buildQueueWaiting"),
610                     JdbcWrapper.getBuildQueueWaitingDurationsSum() / 1000d);
611         }
612     }
613
614     private void collectTomcatValues(int tomcatBusyThreads, long bytesReceived, long bytesSent)
615             throws IOException {
616         addJRobinValue(getOtherJRobin("tomcatBusyThreads"), tomcatBusyThreads);
617         if (this.tomcatBytesSent != NOT_A_NUMBER) {
618             final double periodMinutes = periodMillis / 60000d;
619             addJRobinValue(getOtherJRobin("tomcatBytesReceived"),
620                     (bytesReceived - this.tomcatBytesReceived) / periodMinutes);
621             addJRobinValue(getOtherJRobin("tomcatBytesSent"),
622                     (bytesSent - this.tomcatBytesSent) / periodMinutes);
623         } else {
624             addJRobinValue(getOtherJRobin("tomcatBytesReceived"), 0d);
625             addJRobinValue(getOtherJRobin("tomcatBytesSent"), 0d);
626         }
627         this.tomcatBytesReceived = bytesReceived;
628         this.tomcatBytesSent = bytesSent;
629     }
630
631     private void collectSessionsMeanAge(long sessionAgeSum, int sessionCount) throws IOException {
632         if (sessionCount >= 0 && getCounterByName(Counter.HTTP_COUNTER_NAME) != null) {
633             final long sessionAgeMeanInMinutes;
634             if (sessionCount > 0) {
635                 sessionAgeMeanInMinutes = sessionAgeSum / sessionCount / 60000;
636             } else {
637                 sessionAgeMeanInMinutes = -1;
638             }
639             addJRobinValue(getOtherJRobin("httpSessionsMeanAge"), sessionAgeMeanInMinutes);
640         }
641     }
642
643     private void addJRobinValue(JRobin jRobin, double value) throws IOException {
644         jRobin.addValue(value);
645         // if value == -1, then do not publish that metric,
646         // and metricsPublishers may be null (#735)
647         if (value >= 0 && metricsPublishers != null) {
648             for (final MetricsPublisher metricsPublisher : metricsPublishers) {
649                 metricsPublisher.addValue(jRobin.getName(), value);
650             }
651         }
652     }
653
654     private static double add(double t1, double t2) {
655         // avec des serveurs monitorés sur des OS/JVM multiples (windows, linux par exemple),
656         // des valeurs peuvent être négatives ie non disponibles pour une JVM/OS
657         // et positives ie disponibles pour une autre JVM/OS
658         // (par exemple, systemLoadAverage est négatif sur windows et positif sur linux,
659         // et dans ce cas, si windows et linux alors on ne somme pas et on garde linux,
660         // si linux et windows alors on garde linux,
661         // si linux et linux alors on somme linux+linux qui est positif
662         // si windows et windows alors on somme windows+windows qui est négatif
663         if (t1 < 0d && t2 > 0d) {
664             return t2;
665         } else if (t1 > 0d && t2 < 0d) {
666             return t1;
667         }
668         return t1 + t2;
669     }
670
671     private static long add(long t1, long t2) {
672         if (t1 < 0L && t2 > 0L) {
673             return t2;
674         } else if (t1 > 0L && t2 < 0L) {
675             return t1;
676         }
677         return t1 + t2;
678     }
679
680     private static int add(int t1, int t2) {
681         if (t1 < 0 && t2 > 0) {
682             return t2;
683         } else if (t1 > 0 && t2 < 0) {
684             return t1;
685         }
686         return t1 + t2;
687     }
688
689     private long collectCounterData(Counter counter) throws IOException {
690         // counterName vaut http, sql ou ws par exemple
691         final String counterName = counter.getName();
692         final List<CounterRequest> requests = counter.getRequests();
693         if (!counter.isErrorCounter()) {
694             // on calcule les totaux depuis le départ
695             final CounterRequest newGlobalRequest = new CounterRequest(counterName + " global",
696                     counterName);
697             for (final CounterRequest request : requests) {
698                 // ici, pas besoin de synchronized sur request puisque ce sont des clones indépendants
699                 newGlobalRequest.addHits(request);
700             }
701
702             // on récupère les instances de jrobin même s'il n'y a pas de hits ou pas de précédents totaux
703             // pour être sûr qu'elles soient initialisées (si pas instanciée alors pas de courbe)
704             final JRobin hitsJRobin;
705             final JRobin meanTimesJRobin;
706             final JRobin systemErrorsJRobin;
707             if (!counter.isJspOrStrutsCounter()) {
708                 hitsJRobin = getCounterJRobin(counterName + "HitsRate");
709                 meanTimesJRobin = getCounterJRobin(counterName + "MeanTimes");
710                 systemErrorsJRobin = getCounterJRobin(counterName + "SystemErrors");
711             } else {
712                 hitsJRobin = getOtherJRobin(counterName + "HitsRate");
713                 meanTimesJRobin = getOtherJRobin(counterName + "MeanTimes");
714                 systemErrorsJRobin = getOtherJRobin(counterName + "SystemErrors");
715             }
716
717             final CounterRequest globalRequest = globalRequestsByCounter.get(counter);
718             if (globalRequest != null) {
719                 // on clone et on soustrait les précédents totaux
720                 // pour obtenir les totaux sur la dernière période
721                 // rq : s'il n'y a de précédents totaux (à l'initialisation)
722                 // alors on n'inscrit pas de valeurs car les nouveaux hits
723                 // ne seront connus (en delta) qu'au deuxième passage
724                 // (au 1er passage, globalRequest contient déjà les données lues sur disque)
725                 final CounterRequest lastPeriodGlobalRequest = newGlobalRequest.clone();
726                 lastPeriodGlobalRequest.removeHits(globalRequest);
727
728                 final long hits = lastPeriodGlobalRequest.getHits();
729                 final long hitsParMinute = hits * 60 * 1000 / periodMillis;
730
731                 // on remplit le stockage avec les données
732                 addJRobinValue(hitsJRobin, hitsParMinute);
733                 // s'il n'y a pas eu de hits, alors la moyenne vaut -1 : elle n'a pas de sens
734                 if (hits > 0) {
735                     addJRobinValue(meanTimesJRobin, lastPeriodGlobalRequest.getMean());
736                     addJRobinValue(systemErrorsJRobin,
737                             lastPeriodGlobalRequest.getSystemErrorPercentage());
738
739                     // s'il y a eu des requêtes, on persiste le compteur pour ne pas perdre les stats
740                     // en cas de crash ou d'arrêt brutal (mais normalement ils seront aussi persistés
741                     // lors de l'arrêt du serveur)
742                     counter.writeToFile();
743                 }
744             }
745
746             // on sauvegarde les nouveaux totaux pour la prochaine fois
747             globalRequestsByCounter.put(counter, newGlobalRequest);
748         }
749
750         // données de temps moyen pour les courbes par requête
751         final long dayCounterEstimatedMemorySize = collectCounterRequestsAndErrorsData(counter,
752                 requests);
753         return counter.getEstimatedMemorySize() + dayCounterEstimatedMemorySize;
754     }
755
756     private long collectCounterRequestsAndErrorsData(Counter counter, List<CounterRequest> requests)
757             throws IOException {
758         final Counter dayCounter = getCurrentDayCounter(counter);
759         final boolean firstCollectDoneForCounter = Boolean.TRUE
760                 .equals(firstCollectDoneByCounter.get(counter));
761         final List<CounterRequest> filteredRequests = filterRequestsIfOverflow(counter, requests);
762         for (final CounterRequest newRequest : filteredRequests) {
763             collectCounterRequestData(dayCounter, newRequest, firstCollectDoneForCounter);
764         }
765         if (dayCounter.getRequestsCount() > dayCounter.getMaxRequestsCount()) {
766             // issue 339: ne pas laisser dans dayCounter trop de requêtes si elles sont à chaque fois différentes
767             filterRequestsIfOverflow(dayCounter, dayCounter.getRequests());
768         }
769         if (dayCounter.isErrorCounter()) {
770             dayCounter.addErrors(getDeltaOfErrors(counter, dayCounter));
771         }
772         dayCounter.writeToFile();
773         if (!firstCollectDoneForCounter) {
774             firstCollectDoneByCounter.put(counter, Boolean.TRUE);
775         }
776         return dayCounter.getEstimatedMemorySize();
777     }
778
779     private List<CounterRequest> filterRequestsIfOverflow(Counter counter,
780             List<CounterRequest> requests) {
781         final int maxRequestsCount = counter.getMaxRequestsCount();
782         if (requests.size() <= maxRequestsCount) {
783             return requests;
784         }
785         final List<CounterRequest> result = new ArrayList<>(requests);
786         for (final CounterRequest request : requests) {
787             if (result.size() > maxRequestsCount && request.getHits() < 10) {
788                 // Si le nombre de requêtes est supérieur à 10000
789                 // on suppose que l'application a des requêtes sql non bindées
790                 // (bien que cela ne soit en général pas conseillé).
791                 // En tout cas, on essaye ici d'éviter de saturer
792                 // la mémoire (et le disque dur) avec toutes ces requêtes
793                 // différentes en éliminant celles ayant moins de 10 hits.
794                 removeRequest(counter, request);
795                 result.remove(request);
796             }
797         }
798         while (result.size() > maxRequestsCount && !result.isEmpty()) {
799             // cas extrême: si le nombre de requêtes est encore trop grand,
800             // on enlève n'importe quelle requête
801             final CounterRequest request = result.get(0);
802             removeRequest(counter, request);
803             result.remove(request);
804         }
805         return result;
806     }
807
808     private void collectCounterRequestData(Counter dayCounter, CounterRequest newRequest,
809             boolean firstCollectDoneForCounter) throws IOException {
810         final String requestStorageId = newRequest.getId();
811
812         final CounterRequest request = requestsById.get(requestStorageId);
813         if (request != null) {
814             // idem : on clone et on soustrait les requêtes précédentes
815             // sauf si c'est l'initialisation
816             final CounterRequest lastPeriodRequest = newRequest.clone();
817             lastPeriodRequest.removeHits(request);
818             // avec la condition getHits() > 1 au lieu de getHits() > 0, on évite de créer des fichiers RRD
819             // pour les toutes les requêtes appelées une seule fois sur la dernière période
820             // et notamment pour les requêtes http "écrites au hasard" (par exemple, pour tester les failles d'un site web) ;
821             // cela réduit la place occupée par de nombreux fichiers très peu utiles
822             // (et s'il y a eu 0 hit, alors la moyenne vaut -1 : elle n'a pas de sens)
823             if (lastPeriodRequest.getHits() > 1 && !dayCounter.isJspOrStrutsCounter()
824                     && (!dayCounter.isErrorCounter() || dayCounter.isJobCounter())) {
825                 // on ne crée jamais de graphiques pour les "jsp""error" et "job" car peu utiles
826                 // et potentiellement lourd en usage disque et en mémoire utilisée
827                 final JRobin requestJRobin = getRequestJRobin(requestStorageId,
828                         newRequest.getName());
829                 // plus nécessaire: if (dayCounter.isErrorCounter()) requestJRobin.addValue(lastPeriodRequest.getHits());
830
831                 // pas addJRobinValue ici, il y en aurait trop pour Graphite
832                 requestJRobin.addValue(lastPeriodRequest.getMean());
833             }
834
835             // agrégation de la requête sur le compteur pour le jour courant
836             dayCounter.addHits(lastPeriodRequest);
837         } else if (firstCollectDoneForCounter) {
838             // si c'est la première collecte pour ce compteur (!firstCollectDoneForCounter), alors on n'ajoute pas
839             // newRequest dans dayCounter car cela ajouterait la première fois tout le contenu
840             // de la période "tout" dans le dayCounter du jour,
841             // (attention : la collecte d'un compteur peut être plus tard, issue 242),
842             // mais si c'est une collecte suivante (firstCollectDoneForCounter), alors on ajoute
843             // newRequest dans dayCounter car il s'agit simplement d'une nouvelle requête
844             // qui n'avait pas encore été rencontrée dans la période "tout"
845             dayCounter.addHits(newRequest);
846         }
847         requestsById.put(requestStorageId, newRequest);
848     }
849
850     private List<CounterError> getDeltaOfErrors(Counter counter, Counter dayCounter) {
851         final List<CounterError> errors = counter.getErrors();
852         if (errors.isEmpty()) {
853             return Collections.emptyList();
854         }
855         final long lastErrorTime;
856         final List<CounterError> dayErrors = dayCounter.getErrors();
857         if (dayErrors.isEmpty()) {
858             lastErrorTime = dayCounter.getStartDate().getTime();
859         } else {
860             lastErrorTime = dayErrors.get(dayErrors.size() - 1).getTime();
861         }
862         final List<CounterError> errorsOfDay = new ArrayList<>();
863         for (final CounterError error : errors) {
864             // il peut arriver de manquer une erreur dans l'affichage par jour
865             // si on récupère la liste et qu'il y a une nouvelle erreur dans la même ms
866             // mais tant pis et il y a peu de chance que cela arrive
867             if (error.getTime() > lastErrorTime) {
868                 errorsOfDay.add(error);
869             }
870         }
871         return errorsOfDay;
872     }
873
874     private Counter getCurrentDayCounter(Counter counter) throws IOException {
875         Counter dayCounter = dayCountersByCounter.get(counter);
876         final Calendar calendar = Calendar.getInstance();
877         final int currentDayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
878         calendar.setTime(dayCounter.getStartDate());
879         if (calendar.get(Calendar.DAY_OF_YEAR) != currentDayOfYear) {
880             // le jour a changé, on crée un compteur vide qui sera enregistré dans un nouveau fichier
881             dayCounter = new PeriodCounterFactory(dayCounter).buildNewDayCounter();
882             dayCountersByCounter.put(counter, dayCounter);
883         }
884         return dayCounter;
885     }
886
887     void deleteObsoleteFiles() {
888         final long rrdDiskUsage = CounterStorage.deleteObsoleteCounterFiles(getApplication());
889         final long serGzDiskUsage = JRobin.deleteObsoleteJRobinFiles(getApplication());
890         diskUsage = rrdDiskUsage + serGzDiskUsage;
891         // il manque la taille du fichier "last_shutdown.html", mais on n'est pas à ça près
892         LOG.debug("Obsolete files deleted. JavaMelody disk usage: " + diskUsage / 1024 + " KB");
893     }
894
895     private void removeRequest(Counter counter, CounterRequest newRequest) {
896         counter.removeRequest(newRequest.getName());
897         requestsById.remove(newRequest.getId());
898         final JRobin requestJRobin = requestJRobinsById.remove(newRequest.getId());
899         if (requestJRobin != null) {
900             requestJRobin.deleteFile();
901         }
902     }
903
904     private JRobin getRequestJRobin(String requestId, String requestName) throws IOException {
905         JRobin jrobin = requestJRobinsById.get(requestId);
906         if (jrobin == null) {
907             jrobin = JRobin.createInstance(getApplication(), requestId, requestName);
908             requestJRobinsById.put(requestId, jrobin);
909         }
910         return jrobin;
911     }
912
913     private JRobin getCounterJRobin(String name) throws IOException {
914         JRobin jrobin = counterJRobins.get(name);
915         if (jrobin == null) {
916             jrobin = JRobin.createInstance(getApplication(), name, null);
917             counterJRobins.put(name, jrobin);
918         }
919         return jrobin;
920     }
921
922     private JRobin getOtherJRobin(String name) throws IOException {
923         JRobin jrobin = otherJRobins.get(name);
924         if (jrobin == null) {
925             jrobin = JRobin.createInstance(getApplication(), name, null);
926             otherJRobins.put(name, jrobin);
927         }
928         return jrobin;
929     }
930
931     public JRobin getJRobin(String graphName) throws IOException {
932         JRobin jrobin = counterJRobins.get(graphName);
933         if (jrobin == null) {
934             jrobin = otherJRobins.get(graphName);
935             if (jrobin == null) {
936                 jrobin = requestJRobinsById.get(graphName);
937                 if (jrobin == null) {
938                     // un graph n'est pas toujours de suite dans jrobin
939                     // Use a temporary JRobin instance to create the graph.
940                     // The real request name can not be got at this point, passing the graphName as the request name.
941                     jrobin = JRobin.createInstanceIfFileExists(getApplication(), graphName,
942                             graphName);
943                 }
944             }
945         }
946         return jrobin;
947     }
948
949     public Collection<JRobin> getCounterJRobins() {
950         return Collections.unmodifiableCollection(counterJRobins.values());
951     }
952
953     Collection<JRobin> getOtherJRobins() {
954         return Collections.unmodifiableCollection(otherJRobins.values());
955     }
956
957     public Collection<JRobin> getDisplayedCounterJRobins() {
958         return getDisplayedJRobins(counterJRobins.values());
959     }
960
961     public Collection<JRobin> getDisplayedOtherJRobins() {
962         return getDisplayedJRobins(otherJRobins.values());
963     }
964
965     private Collection<JRobin> getDisplayedJRobins(Collection<JRobin> jrobins) {
966         final List<JRobin> displayedJRobins = new ArrayList<>(jrobins.size());
967         for (final JRobin jrobin : jrobins) {
968             final String jrobinName = jrobin.getName();
969             boolean displayed = true;
970             // inutile car on ne génère pas les jrobin pour le counter de ce nom là
971             //        if (jrobinName.startsWith(Counter.ERROR_COUNTER_NAME)) {
972             //            displayed = false;
973             //        } else {
974             for (final Counter counter : getCounters()) {
975                 if (jrobinName.startsWith(counter.getName())) {
976                     displayed = counter.isDisplayed();
977                     break;
978                 }
979             }
980             if (displayed) {
981                 displayedJRobins.add(jrobin);
982             }
983         }
984         return Collections.unmodifiableCollection(displayedJRobins);
985     }
986
987     /**
988      * Purge les données pour un compteur à partir de son nom.
989      * @param counterName Nom du compteur
990      */

991     void clearCounter(String counterName) {
992         final Counter counter = getCounterByName(counterName);
993         if (counter != null) {
994             final List<CounterRequest> requests = counter.getRequests();
995             // on réinitialise le counter
996             counter.clear();
997             // et on purge les données correspondantes du collector utilisées pour les deltas
998             globalRequestsByCounter.remove(counter);
999             for (final CounterRequest request : requests) {
1000                 requestsById.remove(request.getId());
1001                 requestJRobinsById.remove(request.getId());
1002             }
1003         }
1004     }
1005
1006     public boolean isStorageUsedByMultipleInstances() {
1007         return !storageLock.isAcquired();
1008     }
1009
1010     public void stop() {
1011         try {
1012             try {
1013                 // on persiste les compteurs pour les relire à l'initialisation et ne pas perdre les stats
1014                 for (final Counter counter : counters) {
1015                     counter.writeToFile();
1016                 }
1017             } finally {
1018                 storageLock.release();
1019             }
1020         } catch (final IOException e) {
1021             // persistance échouée, tant pis
1022             LOG.warn("exception while writing counters data to files", e);
1023         } finally {
1024             try {
1025                 for (final Counter counter : counters) {
1026                     counter.clear();
1027                 }
1028             } finally {
1029                 if (metricsPublishers != null) {
1030                     for (final MetricsPublisher metricsPublisher : metricsPublishers) {
1031                         metricsPublisher.stop();
1032                     }
1033                 }
1034             }
1035             stopped = true;
1036             // ici on ne fait pas de nettoyage de la liste counters car cette méthode
1037             // est appelée sur la webapp monitorée quand il y a un serveur de collecte
1038             // et que cette liste est envoyée au serveur de collecte,
1039             // et on ne fait pas de nettoyage des maps qui servent dans le cas
1040             // où le monitoring de la webapp monitorée est appelée par un navigateur
1041             // directement même si il y a par ailleurs un serveur de collecte
1042             // (dans ce dernier cas les données sont bien sûr partielles)
1043         }
1044     }
1045
1046     public boolean isStopped() {
1047         return stopped;
1048     }
1049
1050     public static void stopJRobin() {
1051         try {
1052             JRobin.stop();
1053         } catch (final Throwable t) { // NOPMD
1054             LOG.warn("stopping jrobin failed", t);
1055         }
1056     }
1057
1058     public static void detachVirtualMachine() {
1059         try {
1060             VirtualMachine.detach();
1061         } catch (final Throwable t) { // NOPMD
1062             LOG.warn("exception while detaching virtual machine", t);
1063         }
1064     }
1065
1066     /** {@inheritDoc} */
1067     @Override
1068     public String toString() {
1069         return getClass().getSimpleName() + "[application=" + getApplication() + ", periodMillis="
1070                 + periodMillis + ", counters=" + getCounters() + ']';
1071     }
1072 }
1073