/*
* Copyright 2008-2017 by Emeric Vernat
*
* This file is part of Java Melody.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.bull.javamelody;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import net.bull.javamelody.Counter.CounterRequestContextComparator;
import net.bull.javamelody.SamplingProfiler.SampledMethod;
/**
* Collecteur de données sur les compteurs, avec son propre thread, pour remplir les courbes.
* @author Emeric Vernat
*/
class Collector { // NOPMD
private static final Comparator<Map.Entry<String, Date>> WEBAPP_VERSIONS_VALUE_COMPARATOR = Collections
.reverseOrder(new MapValueComparator<String, Date>());
private static final String VERSIONS_FILENAME = "versions.properties";
private static final String VERSIONS_DATE_PATTERN = "yyyy/MM/dd";
private static final long NOT_A_NUMBER = Long.MIN_VALUE;
// période entre 2 collectes en milli-secondes
private final int periodMillis;
private final String application;
private final List<Counter> counters;
private final SamplingProfiler samplingProfiler;
private final Map<String, JRobin> requestJRobinsById = new ConcurrentHashMap<String, JRobin>();
// les instances jrobins des compteurs sont créées à l'initialisation
private final Map<String, JRobin> counterJRobins = new LinkedHashMap<String, JRobin>();
private final Map<String, JRobin> otherJRobins = new LinkedHashMap<String, JRobin>();
// globalRequestsByCounter, requestsById, dayCountersByCounter et cpuTimeMillis
// sont utilisés par un seul thread lors des collectes,
// (et la méthode centrale "collect" est synchronisée pour éviter un accès concurrent
// avec la mise à jour avant le rapport html)
private final Map<Counter, CounterRequest> globalRequestsByCounter = new HashMap<Counter, CounterRequest>();
private final Map<String, CounterRequest> requestsById = new HashMap<String, CounterRequest>();
private final Map<Counter, Counter> dayCountersByCounter = new LinkedHashMap<Counter, Counter>();
private final Map<Counter, Boolean> firstCollectDoneByCounter = new HashMap<Counter, Boolean>();
private long transactionCount = NOT_A_NUMBER;
private long cpuTimeMillis = NOT_A_NUMBER;
private long gcTimeMillis = NOT_A_NUMBER;
private long tomcatBytesReceived = NOT_A_NUMBER;
private long tomcatBytesSent = NOT_A_NUMBER;
private long lastCollectDuration;
private long estimatedMemorySize;
private long diskUsage;
private Date lastDateOfDeletedObsoleteFiles = new Date();
private boolean stopped;
private final boolean noDatabase = Parameters.isNoDatabase();
/**
* Les versions de l'applications avec pour chacune la date de déploiement.
*/
private final Map<String, Date> datesByWebappVersions;
private static class MapValueComparator<K, V extends Comparable<V>>
implements Comparator<Map.Entry<K, V>>, Serializable {
private static final long serialVersionUID = 1L;
MapValueComparator() {
super();
}
@Override
public int compare(Map.Entry<K, V> o1, Map.Entry<K, V> o2) {
return o1.getValue().compareTo(o2.getValue());
}
}
/**
* Constructeur.
* @param application Code de l'application
* @param counters Liste des counters
*/
Collector(String application, List<Counter> counters) {
this(application, counters, null);
}
/**
* Constructeur.
* @param application Code de l'application
* @param counters Liste des counters
* @param samplingProfiler SamplingProfiler
*/
Collector(String application, List<Counter> counters, SamplingProfiler samplingProfiler) {
super();
assert application != null;
assert counters != null;
this.application = application;
this.counters = Collections.unmodifiableList(new ArrayList<Counter>(counters));
this.samplingProfiler = samplingProfiler;
// c'est le collector qui fixe le nom de l'application (avant la lecture des éventuels fichiers)
for (final Counter counter : counters) {
for (final Counter otherCounter : counters) {
// on vérifie que les noms des compteurs sont uniques entre eux
assert counter == otherCounter || !counter.getName().equals(otherCounter.getName());
}
counter.setApplication(application);
final Counter dayCounter = new PeriodCounterFactory(counter)
.createDayCounterAtDate(new Date());
dayCountersByCounter.put(counter, dayCounter);
}
periodMillis = Parameters.getResolutionSeconds() * 1000;
try {
// on relit les compteurs à l'initialisation pour récupérer les stats;
// d'abord les compteurs non temporels, au cas où les compteurs par jour soient illisibles,
for (final Counter counter : counters) {
counter.readFromFile();
}
// et seulement ensuite les compteurs du jour
for (final Counter counter : counters) {
dayCountersByCounter.get(counter).readFromFile();
}
LOG.debug("counters data read from files in "
+ Parameters.getStorageDirectory(application));
} catch (final IOException e) {
// lecture échouée, tant pis
// (on n'interrompt pas toute l'initialisation juste pour un fichier illisible)
LOG.warn("exception while reading counters data from files in "
+ Parameters.getStorageDirectory(application), e);
}
datesByWebappVersions = readDatesByWebappVersions();
}
/**
* Retourne le code de l'application.
* @return String
*/
String getApplication() {
return application;
}
/**
* Retourne le SamplingProfiler.
* @return SamplingProfiler
*/
SamplingProfiler getSamplingProfiler() {
return samplingProfiler;
}
List<SampledMethod> getHotspots() {
if (samplingProfiler == null) {
throw new IllegalStateException("Hotspots sampling is not enabled in this server");
}
return samplingProfiler.getHotspots(1000);
}
Map<String, Date> getDatesByWebappVersions() {
final List<Map.Entry<String, Date>> entries = new ArrayList<Map.Entry<String, Date>>(
datesByWebappVersions.entrySet());
Collections.sort(entries, WEBAPP_VERSIONS_VALUE_COMPARATOR);
final Map<String, Date> map = new LinkedHashMap<String, Date>();
for (final Map.Entry<String, Date> entry : entries) {
map.put(entry.getKey(), entry.getValue());
}
return Collections.unmodifiableMap(map);
}
/**
* @return La liste des counters de ce collector
*/
List<Counter> getCounters() {
return counters;
}
/**
* @param counterName Nom d'un counter
* @return Le counter de ce collector ayant ce nom ou null si non trouvé
*/
Counter getCounterByName(String counterName) {
for (final Counter counter : counters) {
if (counter.getName().equals(counterName)) {
return counter;
}
}
return null;
}
Counter getCounterByRequestId(CounterRequest request) {
final String requestId = request.getId();
for (final Counter counter : counters) {
if (counter.isRequestIdFromThisCounter(requestId)) {
return counter;
}
}
return null;
}
List<CounterRequestContext> getRootCurrentContexts(List<Counter> newParentCounters) {
final List<CounterRequestContext> rootCurrentContexts = new ArrayList<CounterRequestContext>();
for (final Counter counter : counters) {
if (counter.isDisplayed()) {
// a priori, les contextes root courants sont dans le compteur http
// mais il est possible qu'il y en ait aussi dans ejb ou sql sans parent dans http
rootCurrentContexts.addAll(counter.getOrderedRootCurrentContexts());
}
}
if (rootCurrentContexts.size() > 1) {
Collections.sort(rootCurrentContexts, Collections
.reverseOrder(new CounterRequestContextComparator(System.currentTimeMillis())));
CounterRequestContext.replaceParentCounters(rootCurrentContexts, newParentCounters);
}
return rootCurrentContexts;
}
long getLastCollectDuration() {
return lastCollectDuration;
}
long getEstimatedMemorySize() {
return estimatedMemorySize;
}
long getDiskUsage() {
if (diskUsage == 0) {
// si diskUsage == 0, le serveur a été démarré ce jour et la taille totale des fichiers
// n'a pas encore été calculée lors de la purge des fichiers obsolètes,
// donc on la calcule ici
final File storageDir = Parameters.getStorageDirectory(application);
long sum = 0;
final File[] files = storageDir.listFiles();
if (files != null) {
for (final File file : files) {
sum += file.length();
}
}
diskUsage = sum;
}
return diskUsage;
}
List<Counter> getRangeCounters(Range range) throws IOException {
if (range.getPeriod() == Period.TOUT) {
return new ArrayList<Counter>(counters);
}
final Collection<Counter> currentDayCounters = dayCountersByCounter.values();
final List<Counter> result = new ArrayList<Counter>(currentDayCounters.size());
for (final Counter dayCounter : currentDayCounters) {
final Counter counter = getRangeCounter(range, dayCounter);
result.add(counter);
}
return result;
}
private Counter getRangeCounter(Range range, Counter dayCounter) throws IOException {
final PeriodCounterFactory periodCounterFactory = new PeriodCounterFactory(dayCounter);
final Counter counter;
if (range.getPeriod() == null) {
counter = periodCounterFactory.getCustomCounter(range);
} else {
switch (range.getPeriod()) {
case JOUR:
counter = periodCounterFactory.getDayCounter();
break;
case SEMAINE:
counter = periodCounterFactory.getWeekCounter();
break;
case MOIS:
counter = periodCounterFactory.getMonthCounter();
break;
case ANNEE:
counter = periodCounterFactory.getYearCounter();
break;
case TOUT:
throw new IllegalStateException(range.getPeriod().toString());
default:
throw new IllegalArgumentException(range.getPeriod().toString());
}
}
return counter;
}
List<Counter> getRangeCountersToBeDisplayed(Range range) throws IOException {
final List<Counter> result = new ArrayList<Counter>(getRangeCounters(range));
final Iterator<Counter> it = result.iterator();
while (it.hasNext()) {
final Counter counter = it.next();
if (!counter.isDisplayed() || counter.isJobCounter()) {
it.remove();
}
}
return Collections.unmodifiableList(result);
}
Counter getRangeCounter(Range range, String counterName) throws IOException {
final Counter counter = getCounterByName(counterName);
if (counter == null) {
throw new IllegalArgumentException(counterName);
}
if (range.getPeriod() == Period.TOUT) {
return counter;
}
return getRangeCounter(range, dayCountersByCounter.get(counter));
}
void collectLocalContextWithoutErrors() {
// ici on n'inclue pas les informations de la bdd et des threads
// car on n'en a pas besoin pour la collecte et cela économise des requêtes sql
try {
final JavaInformations javaInformations = new JavaInformations(
Parameters.getServletContext(), false);
collectWithoutErrors(Collections.singletonList(javaInformations));
} catch (final Throwable t) { // NOPMD
// include cause in message for debugging logs in the report
LOG.warn("exception while collecting data: " + t.toString(), t);
}
}
void collectWithoutErrors(List<JavaInformations> javaInformationsList) {
assert javaInformationsList != null;
final long start = System.currentTimeMillis();
try {
estimatedMemorySize = collect(javaInformationsList);
} catch (final Throwable t) { // NOPMD
// include cause in message for debugging logs in the report
LOG.warn("exception while collecting data: " + t.toString(), t);
}
// note : on n'inclue pas "new JavaInformations" de collectLocalContextWithoutErrors
// dans la durée de la collecte mais il est inférieur à 1 ms (sans bdd)
lastCollectDuration = Math.max(0, System.currentTimeMillis() - start);
}
private long collect(List<JavaInformations> javaInformationsList) throws IOException {
synchronized (this) {
// si pas d'informations, on ne met pas 0 : on ne met rien
if (!javaInformationsList.isEmpty()) {
collectJavaInformations(javaInformationsList);
collectOtherJavaInformations(javaInformationsList);
collectTomcatInformations(javaInformationsList);
}
long memorySize = 0;
for (final Counter counter : counters) {
// counter.isDisplayed() peut changer pour spring, ejb, guice ou services selon l'utilisation
dayCountersByCounter.get(counter).setDisplayed(counter.isDisplayed());
// collecte pour chaque compteur (hits par minute, temps moyen, % d'erreurs système)
// Rq : il serait possible d'ajouter le débit total en Ko / minute (pour http)
// mais autant monitorer les vrais débits réseaux au niveau de l'OS
if (counter.isDisplayed()) {
// si le compteur n'est pas affiché (par ex ejb), pas de collecte
// et pas de persistance de fichiers jrobin ou du compteur
memorySize += collectCounterData(counter);
}
}
final Calendar calendar = Calendar.getInstance();
final int currentDayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
calendar.setTime(lastDateOfDeletedObsoleteFiles);
if (calendar.get(Calendar.DAY_OF_YEAR) != currentDayOfYear) {
// 1 fois par jour on supprime tous les fichiers .ser.gz obsolètes (modifiés il y a plus d'un an)
// et tous les fichiers .rrd obsolètes (modifiés il y a plus de 3 mois)
try {
deleteObsoleteFiles();
} finally {
lastDateOfDeletedObsoleteFiles = new Date();
}
}
if (!javaInformationsList.isEmpty()) {
final String webappVersion = javaInformationsList.get(0).getWebappVersion();
if (webappVersion != null && !datesByWebappVersions.containsKey(webappVersion)) {
addWebappVersion(webappVersion);
datesByWebappVersions.put(webappVersion, new Date());
}
}
return memorySize;
}
}
private void collectJavaInformations(List<JavaInformations> javaInformationsList)
throws IOException {
long usedMemory = 0;
long processesCpuTimeMillis = 0;
int availableProcessors = 0;
int sessionCount = 0;
int activeThreadCount = 0;
int activeConnectionCount = 0;
int usedConnectionCount = 0;
for (final JavaInformations javaInformations : javaInformationsList) {
final MemoryInformations memoryInformations = javaInformations.getMemoryInformations();
usedMemory = add(memoryInformations.getUsedMemory(), usedMemory);
sessionCount = add(javaInformations.getSessionCount(), sessionCount);
activeThreadCount = add(javaInformations.getActiveThreadCount(), activeThreadCount);
activeConnectionCount = add(javaInformations.getActiveConnectionCount(),
activeConnectionCount);
usedConnectionCount = add(javaInformations.getUsedConnectionCount(),
usedConnectionCount);
// il y a au moins 1 coeur
availableProcessors = add(Math.max(javaInformations.getAvailableProcessors(), 1),
availableProcessors);
// processesCpuTime n'est supporté que par le jdk sun
processesCpuTimeMillis = add(javaInformations.getProcessCpuTimeMillis(),
processesCpuTimeMillis);
}
collectJRobinValues(usedMemory, processesCpuTimeMillis, availableProcessors, sessionCount,
activeThreadCount, activeConnectionCount, usedConnectionCount);
}
// CHECKSTYLE:OFF
private void collectOtherJavaInformations(List<JavaInformations> javaInformationsList) // NOPMD
throws IOException {
// CHECKSTYLE:ON
long usedNonHeapMemory = 0;
long usedBufferedMemory = 0;
int loadedClassesCount = 0;
long garbageCollectionTimeMillis = 0;
long usedPhysicalMemorySize = 0;
long usedSwapSpaceSize = 0;
int availableProcessors = 0;
int sessionCount = 0;
long sessionAgeSum = 0;
int threadCount = 0;
long databaseTransactionCount = 0;
double systemLoadAverage = 0;
long unixOpenFileDescriptorCount = 0;
long freeDiskSpaceInTemp = Long.MAX_VALUE;
double systemCpuLoad = 0;
for (final JavaInformations javaInformations : javaInformationsList) {
final MemoryInformations memoryInformations = javaInformations.getMemoryInformations();
sessionCount = add(javaInformations.getSessionCount(), sessionCount);
sessionAgeSum = add(javaInformations.getSessionAgeSum(), sessionAgeSum);
threadCount = add(javaInformations.getThreadCount(), threadCount);
databaseTransactionCount = add(javaInformations.getTransactionCount(),
databaseTransactionCount);
// il y a au moins 1 coeur
availableProcessors = add(Math.max(javaInformations.getAvailableProcessors(), 1),
availableProcessors);
usedNonHeapMemory = add(memoryInformations.getUsedNonHeapMemory(), usedNonHeapMemory);
usedBufferedMemory = add(memoryInformations.getUsedBufferedMemory(),
usedBufferedMemory);
loadedClassesCount = add(memoryInformations.getLoadedClassesCount(),
loadedClassesCount);
usedPhysicalMemorySize = add(memoryInformations.getUsedPhysicalMemorySize(),
usedPhysicalMemorySize);
usedSwapSpaceSize = add(memoryInformations.getUsedSwapSpaceSize(), usedSwapSpaceSize);
garbageCollectionTimeMillis = add(memoryInformations.getGarbageCollectionTimeMillis(),
garbageCollectionTimeMillis);
// systemLoadAverage n'est supporté qu'à partir du jdk 1.6 sur linux ou unix
systemLoadAverage = add(javaInformations.getSystemLoadAverage(), systemLoadAverage);
// que sur linx ou unix
unixOpenFileDescriptorCount = add(javaInformations.getUnixOpenFileDescriptorCount(),
unixOpenFileDescriptorCount);
if (javaInformations.getFreeDiskSpaceInTemp() >= 0) {
// la valeur retenue est le minimum entre les serveurs
freeDiskSpaceInTemp = Math.min(javaInformations.getFreeDiskSpaceInTemp(),
freeDiskSpaceInTemp);
}
systemCpuLoad = add(javaInformations.getSystemCpuLoad(), systemCpuLoad);
}
// collecte du pourcentage de temps en ramasse-miette
if (garbageCollectionTimeMillis >= 0) {
if (this.gcTimeMillis != NOT_A_NUMBER) {
// %gc = delta(somme(Temps GC)) / période / nb total de coeurs
final int gcPercentage = Math
.min((int) ((garbageCollectionTimeMillis - this.gcTimeMillis) * 100
/ periodMillis / availableProcessors), 100);
getOtherJRobin("gc").addValue(gcPercentage);
} else {
getOtherJRobin("gc").addValue(0d);
}
this.gcTimeMillis = garbageCollectionTimeMillis;
}
final Map<String, Double> otherJRobinsValues = new LinkedHashMap<String, Double>();
otherJRobinsValues.put("threadCount", (double) threadCount);
otherJRobinsValues.put("loadedClassesCount", (double) loadedClassesCount);
otherJRobinsValues.put("usedBufferedMemory", (double) usedBufferedMemory);
otherJRobinsValues.put("usedNonHeapMemory", (double) usedNonHeapMemory);
otherJRobinsValues.put("usedPhysicalMemorySize", (double) usedPhysicalMemorySize);
otherJRobinsValues.put("usedSwapSpaceSize", (double) usedSwapSpaceSize);
otherJRobinsValues.put("systemLoad", systemLoadAverage);
otherJRobinsValues.put("systemCpuLoad", systemCpuLoad / javaInformationsList.size());
otherJRobinsValues.put("fileDescriptors", (double) unixOpenFileDescriptorCount);
for (final Map.Entry<String, Double> entry : otherJRobinsValues.entrySet()) {
if (entry.getValue() >= 0) {
getOtherJRobin(entry.getKey()).addValue(entry.getValue());
}
}
collectSessionsMeanAge(sessionAgeSum, sessionCount);
if (!noDatabase) {
// collecte du nombre de transactions base de données par minute
if (this.transactionCount != NOT_A_NUMBER) {
final double periodMinutes = periodMillis / 60000d;
getOtherJRobin("transactionsRate").addValue(
(databaseTransactionCount - this.transactionCount) / periodMinutes);
} else {
getOtherJRobin("transactionsRate").addValue(0d);
}
this.transactionCount = databaseTransactionCount;
}
if (freeDiskSpaceInTemp != Long.MAX_VALUE) {
getOtherJRobin("Free_disk_space").addValue(freeDiskSpaceInTemp);
}
// on pourrait collecter la valeur 100 dans jrobin pour qu'il fasse la moyenne
// du pourcentage de disponibilité, mais cela n'aurait pas de sens sans
// différenciation des indisponibilités prévues de celles non prévues
}
private void collectTomcatInformations(List<JavaInformations> javaInformationsList)
throws IOException {
int tomcatBusyThreads = 0;
long bytesReceived = 0;
long bytesSent = 0;
boolean tomcatUsed = false;
for (final JavaInformations javaInformations : javaInformationsList) {
for (final TomcatInformations tomcatInformations : javaInformations
.getTomcatInformationsList()) {
tomcatBusyThreads = add(tomcatInformations.getCurrentThreadsBusy(),
tomcatBusyThreads);
bytesReceived = add(tomcatInformations.getBytesReceived(), bytesReceived);
bytesSent = add(tomcatInformations.getBytesSent(), bytesSent);
tomcatUsed = true;
}
}
if (tomcatUsed) {
// collecte des informations de Tomcat
collectTomcatValues(tomcatBusyThreads, bytesReceived, bytesSent);
}
}
private void collectJRobinValues(long usedMemory, long processesCpuTimeMillis,
int availableProcessors, int sessionCount, int activeThreadCount,
int activeConnectionCount, int usedConnectionCount) throws IOException {
// collecte de la mémoire java
getCounterJRobin("usedMemory").addValue(usedMemory);
// collecte du pourcentage d'utilisation cpu
if (processesCpuTimeMillis >= 0) {
// processesCpuTimeMillis est la somme pour tous les serveurs (et pour tous les coeurs)
// donc ce temps peut être n fois supérieur à periodMillis
// où n est le nombre total de coeurs sur tous les serveurs (si cluster);
// et cpuPercentage s'approchera à pleine charge de 100
// quel que soit le nombre de serveurs ou de coeurs;
// cpuPercentage ne peut être supérieur à 100
// car ce serait une valeur aberrante due aux imprécisions de mesure
if (this.cpuTimeMillis != NOT_A_NUMBER) {
// en gros, %cpu = delta(somme(Temps cpu)) / période / nb total de coeurs
final int cpuPercentage = Math
.min((int) ((processesCpuTimeMillis - this.cpuTimeMillis) * 100
/ periodMillis / availableProcessors), 100);
getCounterJRobin("cpu").addValue(cpuPercentage);
} else {
getCounterJRobin("cpu").addValue(0d);
}
this.cpuTimeMillis = processesCpuTimeMillis;
}
// si ce collector est celui des nodes Jenkins, il n'y a pas de requêtes http
// donc il ne peut pas y avoir de sessions http ou de threads actifs
if (getCounterByName(Counter.HTTP_COUNTER_NAME) != null) {
// collecte du nombre de sessions http
if (sessionCount >= 0) {
getCounterJRobin("httpSessions").addValue(sessionCount);
}
// collecte du nombre de threads actifs (requêtes http en cours)
getCounterJRobin("activeThreads").addValue(activeThreadCount);
}
if (!noDatabase) {
// collecte du nombre de connexions jdbc actives et du nombre de connexions jdbc ouvertes
getCounterJRobin("activeConnections").addValue(activeConnectionCount);
getCounterJRobin("usedConnections").addValue(usedConnectionCount);
}
// si ce collector est celui des nodes Jenkins, on collecte le nombre de builds en cours
// pour le graphique
if (getCounterByName(Counter.BUILDS_COUNTER_NAME) != null) {
getCounterJRobin("runningBuilds").addValue(JdbcWrapper.getRunningBuildCount());
getCounterJRobin("buildQueueLength").addValue(JdbcWrapper.getBuildQueueLength());
}
}
private void collectTomcatValues(int tomcatBusyThreads, long bytesReceived, long bytesSent)
throws IOException {
getOtherJRobin("tomcatBusyThreads").addValue(tomcatBusyThreads);
if (this.tomcatBytesSent != NOT_A_NUMBER) {
final double periodMinutes = periodMillis / 60000d;
getOtherJRobin("tomcatBytesReceived")
.addValue((bytesReceived - this.tomcatBytesReceived) / periodMinutes);
getOtherJRobin("tomcatBytesSent")
.addValue((bytesSent - this.tomcatBytesSent) / periodMinutes);
} else {
getOtherJRobin("tomcatBytesReceived").addValue(0d);
getOtherJRobin("tomcatBytesSent").addValue(0d);
}
this.tomcatBytesReceived = bytesReceived;
this.tomcatBytesSent = bytesSent;
}
private void collectSessionsMeanAge(long sessionAgeSum, int sessionCount) throws IOException {
if (sessionCount >= 0 && getCounterByName(Counter.HTTP_COUNTER_NAME) != null) {
final long sessionAgeMeanInMinutes;
if (sessionCount > 0) {
sessionAgeMeanInMinutes = sessionAgeSum / sessionCount / 60000;
} else {
sessionAgeMeanInMinutes = -1;
}
getOtherJRobin("httpSessionsMeanAge").addValue(sessionAgeMeanInMinutes);
}
}
private static double add(double t1, double t2) {
// avec des serveurs monitorés sur des OS/JVM multiples (windows, linux par exemple),
// des valeurs peuvent être négatives ie non disponibles pour une JVM/OS
// et positives ie disponibles pour une autre JVM/OS
// (par exemple, systemLoadAverage est négatif sur windows et positif sur linux,
// et dans ce cas, si windows et linux alors on ne somme pas et on garde linux,
// si linux et windows alors on garde linux,
// si linux et linux alors on somme linux+linux qui est positif
// si windows et windows alors on somme windows+windows qui est négatif
if (t1 < 0d && t2 > 0d) {
return t2;
} else if (t1 > 0d && t2 < 0d) {
return t1;
}
return t1 + t2;
}
private static long add(long t1, long t2) {
if (t1 < 0L && t2 > 0L) {
return t2;
} else if (t1 > 0L && t2 < 0L) {
return t1;
}
return t1 + t2;
}
private static int add(int t1, int t2) {
if (t1 < 0 && t2 > 0) {
return t2;
} else if (t1 > 0 && t2 < 0) {
return t1;
}
return t1 + t2;
}
private long collectCounterData(Counter counter) throws IOException {
// counterName vaut http, sql ou ws par exemple
final String counterName = counter.getName();
final List<CounterRequest> requests = counter.getRequests();
if (!counter.isErrorCounter()) {
// on calcule les totaux depuis le départ
final CounterRequest newGlobalRequest = new CounterRequest(counterName + " global",
counterName);
for (final CounterRequest request : requests) {
// ici, pas besoin de synchronized sur request puisque ce sont des clones indépendants
newGlobalRequest.addHits(request);
}
// 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
// pour être sûr qu'elles soient initialisées (si pas instanciée alors pas de courbe)
final JRobin hitsJRobin;
final JRobin meanTimesJRobin;
final JRobin systemErrorsJRobin;
if (!counter.isJspOrStrutsCounter()) {
hitsJRobin = getCounterJRobin(counterName + "HitsRate");
meanTimesJRobin = getCounterJRobin(counterName + "MeanTimes");
systemErrorsJRobin = getCounterJRobin(counterName + "SystemErrors");
} else {
hitsJRobin = getOtherJRobin(counterName + "HitsRate");
meanTimesJRobin = getOtherJRobin(counterName + "MeanTimes");
systemErrorsJRobin = getOtherJRobin(counterName + "SystemErrors");
}
final CounterRequest globalRequest = globalRequestsByCounter.get(counter);
if (globalRequest != null) {
// on clone et on soustrait les précédents totaux
// pour obtenir les totaux sur la dernière période
// rq : s'il n'y a de précédents totaux (à l'initialisation)
// alors on n'inscrit pas de valeurs car les nouveaux hits
// ne seront connus (en delta) qu'au deuxième passage
// (au 1er passage, globalRequest contient déjà les données lues sur disque)
final CounterRequest lastPeriodGlobalRequest = newGlobalRequest.clone();
lastPeriodGlobalRequest.removeHits(globalRequest);
final long hits = lastPeriodGlobalRequest.getHits();
final long hitsParMinute = hits * 60 * 1000 / periodMillis;
// on remplit le stockage avec les données
hitsJRobin.addValue(hitsParMinute);
// s'il n'y a pas eu de hits, alors la moyenne vaut -1 : elle n'a pas de sens
if (hits > 0) {
meanTimesJRobin.addValue(lastPeriodGlobalRequest.getMean());
systemErrorsJRobin.addValue(lastPeriodGlobalRequest.getSystemErrorPercentage());
// s'il y a eu des requêtes, on persiste le compteur pour ne pas perdre les stats
// en cas de crash ou d'arrêt brutal (mais normalement ils seront aussi persistés
// lors de l'arrêt du serveur)
counter.writeToFile();
}
}
// on sauvegarde les nouveaux totaux pour la prochaine fois
globalRequestsByCounter.put(counter, newGlobalRequest);
}
// données de temps moyen pour les courbes par requête
final long dayCounterEstimatedMemorySize = collectCounterRequestsAndErrorsData(counter,
requests);
return counter.getEstimatedMemorySize() + dayCounterEstimatedMemorySize;
}
private long collectCounterRequestsAndErrorsData(Counter counter, List<CounterRequest> requests)
throws IOException {
final Counter dayCounter = getCurrentDayCounter(counter);
final boolean firstCollectDoneForCounter = Boolean.TRUE
.equals(firstCollectDoneByCounter.get(counter));
final List<CounterRequest> filteredRequests = filterRequestsIfOverflow(counter, requests);
for (final CounterRequest newRequest : filteredRequests) {
collectCounterRequestData(dayCounter, newRequest, firstCollectDoneForCounter);
}
if (dayCounter.getRequestsCount() > dayCounter.getMaxRequestsCount()) {
// issue 339: ne pas laisser dans dayCounter trop de requêtes si elles sont à chaque fois différentes
filterRequestsIfOverflow(dayCounter, dayCounter.getRequests());
}
if (dayCounter.isErrorCounter()) {
dayCounter.addErrors(getDeltaOfErrors(counter, dayCounter));
}
dayCounter.writeToFile();
if (!firstCollectDoneForCounter) {
firstCollectDoneByCounter.put(counter, Boolean.TRUE);
}
return dayCounter.getEstimatedMemorySize();
}
private List<CounterRequest> filterRequestsIfOverflow(Counter counter,
List<CounterRequest> requests) {
final int maxRequestsCount = counter.getMaxRequestsCount();
if (requests.size() <= maxRequestsCount) {
return requests;
}
final List<CounterRequest> result = new ArrayList<CounterRequest>(requests);
for (final CounterRequest request : requests) {
if (result.size() > maxRequestsCount && request.getHits() < 10) {
// Si le nombre de requêtes est supérieur à 10000
// on suppose que l'application a des requêtes sql non bindées
// (bien que cela ne soit en général pas conseillé).
// En tout cas, on essaye ici d'éviter de saturer
// la mémoire (et le disque dur) avec toutes ces requêtes
// différentes en éliminant celles ayant moins de 10 hits.
removeRequest(counter, request);
result.remove(request);
continue;
}
}
while (result.size() > maxRequestsCount && !result.isEmpty()) {
// cas extrême: si le nombre de requêtes est encore trop grand,
// on enlève n'importe quelle requête
final CounterRequest request = result.get(0);
removeRequest(counter, request);
result.remove(request);
}
return result;
}
private void collectCounterRequestData(Counter dayCounter, CounterRequest newRequest,
boolean firstCollectDoneForCounter) throws IOException {
final String requestStorageId = newRequest.getId();
final CounterRequest request = requestsById.get(requestStorageId);
if (request != null) {
// idem : on clone et on soustrait les requêtes précédentes
// sauf si c'est l'initialisation
final CounterRequest lastPeriodRequest = newRequest.clone();
lastPeriodRequest.removeHits(request);
// avec la condition getHits() > 1 au lieu de getHits() > 0, on évite de créer des fichiers RRD
// pour les toutes les requêtes appelées une seule fois sur la dernière période
// et notamment pour les requêtes http "écrites au hasard" (par exemple, pour tester les failles d'un site web) ;
// cela réduit la place occupée par de nombreux fichiers très peu utiles
// (et s'il y a eu 0 hit, alors la moyenne vaut -1 : elle n'a pas de sens)
if (lastPeriodRequest.getHits() > 1 && !dayCounter.isJspOrStrutsCounter()
&& (!dayCounter.isErrorCounter() || dayCounter.isJobCounter())) {
// on ne crée jamais de graphiques pour les "jsp", "error" et "job" car peu utiles
// et potentiellement lourd en usage disque et en mémoire utilisée
final JRobin requestJRobin = getRequestJRobin(requestStorageId,
newRequest.getName());
// plus nécessaire: if (dayCounter.isErrorCounter()) requestJRobin.addValue(lastPeriodRequest.getHits());
requestJRobin.addValue(lastPeriodRequest.getMean());
}
// agrégation de la requête sur le compteur pour le jour courant
dayCounter.addHits(lastPeriodRequest);
} else if (firstCollectDoneForCounter) {
// si c'est la première collecte pour ce compteur (!firstCollectDoneForCounter), alors on n'ajoute pas
// newRequest dans dayCounter car cela ajouterait la première fois tout le contenu
// de la période "tout" dans le dayCounter du jour,
// (attention : la collecte d'un compteur peut être plus tard, issue 242),
// mais si c'est une collecte suivante (firstCollectDoneForCounter), alors on ajoute
// newRequest dans dayCounter car il s'agit simplement d'une nouvelle requête
// qui n'avait pas encore été rencontrée dans la période "tout"
dayCounter.addHits(newRequest);
}
requestsById.put(requestStorageId, newRequest);
}
private List<CounterError> getDeltaOfErrors(Counter counter, Counter dayCounter) {
final List<CounterError> errors = counter.getErrors();
if (errors.isEmpty()) {
return Collections.emptyList();
}
final long lastErrorTime;
final List<CounterError> dayErrors = dayCounter.getErrors();
if (dayErrors.isEmpty()) {
lastErrorTime = dayCounter.getStartDate().getTime();
} else {
lastErrorTime = dayErrors.get(dayErrors.size() - 1).getTime();
}
final List<CounterError> errorsOfDay = new ArrayList<CounterError>();
for (final CounterError error : errors) {
// il peut arriver de manquer une erreur dans l'affichage par jour
// si on récupère la liste et qu'il y a une nouvelle erreur dans la même ms
// mais tant pis et il y a peu de chance que cela arrive
if (error.getTime() > lastErrorTime) {
errorsOfDay.add(error);
}
}
return errorsOfDay;
}
private Counter getCurrentDayCounter(Counter counter) throws IOException {
Counter dayCounter = dayCountersByCounter.get(counter);
final Calendar calendar = Calendar.getInstance();
final int currentDayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
calendar.setTime(dayCounter.getStartDate());
if (calendar.get(Calendar.DAY_OF_YEAR) != currentDayOfYear) {
// le jour a changé, on crée un compteur vide qui sera enregistré dans un nouveau fichier
dayCounter = new PeriodCounterFactory(dayCounter).buildNewDayCounter();
dayCountersByCounter.put(counter, dayCounter);
}
return dayCounter;
}
void deleteObsoleteFiles() throws IOException {
final long rrdDiskUsage = CounterStorage.deleteObsoleteCounterFiles(getApplication());
final long serGzDiskUsage = JRobin.deleteObsoleteJRobinFiles(getApplication());
diskUsage = rrdDiskUsage + serGzDiskUsage;
// il manque la taille du fichier "last_shutdown.html", mais on n'est pas à ça près
LOG.debug("Obsolete files deleted. JavaMelody disk usage: " + diskUsage / 1024 + " KB");
}
private void removeRequest(Counter counter, CounterRequest newRequest) {
counter.removeRequest(newRequest.getName());
requestsById.remove(newRequest.getId());
final JRobin requestJRobin = requestJRobinsById.remove(newRequest.getId());
if (requestJRobin != null) {
requestJRobin.deleteFile();
}
}
private JRobin getRequestJRobin(String requestId, String requestName) throws IOException {
JRobin jrobin = requestJRobinsById.get(requestId);
if (jrobin == null) {
jrobin = JRobin.createInstance(getApplication(), requestId, requestName);
requestJRobinsById.put(requestId, jrobin);
}
return jrobin;
}
private JRobin getCounterJRobin(String name) throws IOException {
JRobin jrobin = counterJRobins.get(name);
if (jrobin == null) {
jrobin = JRobin.createInstance(getApplication(), name, null);
counterJRobins.put(name, jrobin);
}
return jrobin;
}
private JRobin getOtherJRobin(String name) throws IOException {
JRobin jrobin = otherJRobins.get(name);
if (jrobin == null) {
jrobin = JRobin.createInstance(getApplication(), name, null);
otherJRobins.put(name, jrobin);
}
return jrobin;
}
JRobin getJRobin(String graphName) throws IOException {
JRobin jrobin = counterJRobins.get(graphName);
if (jrobin == null) {
jrobin = otherJRobins.get(graphName);
if (jrobin == null) {
jrobin = requestJRobinsById.get(graphName);
if (jrobin == null) {
// un graph n'est pas toujours de suite dans jrobin
// Use a temporary JRobin instance to create the graph.
// The real request name can not be got at this point, passing the graphName as the request name.
jrobin = JRobin.createInstanceIfFileExists(getApplication(), graphName,
graphName);
}
}
}
return jrobin;
}
Collection<JRobin> getCounterJRobins() {
return Collections.unmodifiableCollection(counterJRobins.values());
}
Collection<JRobin> getOtherJRobins() {
return Collections.unmodifiableCollection(otherJRobins.values());
}
Collection<JRobin> getDisplayedCounterJRobins() {
return getDisplayedJRobins(counterJRobins.values());
}
Collection<JRobin> getDisplayedOtherJRobins() {
return getDisplayedJRobins(otherJRobins.values());
}
private Collection<JRobin> getDisplayedJRobins(Collection<JRobin> jrobins) {
final List<JRobin> displayedJRobins = new ArrayList<JRobin>(jrobins.size());
for (final JRobin jrobin : jrobins) {
final String jrobinName = jrobin.getName();
boolean displayed = true;
// inutile car on ne génère pas les jrobin pour le counter de ce nom là
// if (jrobinName.startsWith(Counter.ERROR_COUNTER_NAME)) {
// displayed = false;
// } else {
for (final Counter counter : getCounters()) {
if (jrobinName.startsWith(counter.getName())) {
displayed = counter.isDisplayed();
break;
}
}
if (displayed) {
displayedJRobins.add(jrobin);
}
}
return Collections.unmodifiableCollection(displayedJRobins);
}
@SuppressWarnings("unchecked")
private Map<String, Date> readDatesByWebappVersions() {
final Map<String, Date> result = new HashMap<String, Date>();
final File storageDirectory = Parameters.getStorageDirectory(application);
final File versionsFile = new File(storageDirectory, VERSIONS_FILENAME);
if (versionsFile.exists()) {
final Properties versionsProperties = new Properties();
try {
final InputStream input = new FileInputStream(versionsFile);
try {
versionsProperties.load(input);
} finally {
input.close();
}
final List<String> propertyNames = (List<String>) Collections
.list(versionsProperties.propertyNames());
final SimpleDateFormat dateFormat = new SimpleDateFormat(VERSIONS_DATE_PATTERN,
Locale.US);
for (final String version : propertyNames) {
try {
final Date date = dateFormat.parse(versionsProperties.getProperty(version));
result.put(version, date);
} catch (final ParseException e) {
continue;
}
}
} catch (final IOException e) {
// lecture échouée, tant pis
LOG.warn("exception while reading versions in " + versionsFile, e);
}
}
return result;
}
private void addWebappVersion(String webappVersion) throws IOException {
assert webappVersion != null && !webappVersion.isEmpty();
final File storageDirectory = Parameters.getStorageDirectory(application);
final File versionsFile = new File(storageDirectory, VERSIONS_FILENAME);
final Properties versionsProperties = new Properties();
if (versionsFile.exists()) {
final InputStream input = new FileInputStream(versionsFile);
try {
versionsProperties.load(input);
} finally {
input.close();
}
}
assert versionsProperties.getProperty(webappVersion) == null;
final SimpleDateFormat dateFormat = new SimpleDateFormat(VERSIONS_DATE_PATTERN, Locale.US);
versionsProperties.setProperty(webappVersion, dateFormat.format(new Date()));
final OutputStream output = new FileOutputStream(versionsFile);
try {
versionsProperties.store(output, "Application deployments with versions and dates");
} finally {
output.close();
}
LOG.debug("New application version added: " + webappVersion);
}
/**
* Purge les données pour un compteur à partir de son nom.
* @param counterName Nom du compteur
*/
void clearCounter(String counterName) {
final Counter counter = getCounterByName(counterName);
if (counter != null) {
final List<CounterRequest> requests = counter.getRequests();
// on réinitialise le counter
counter.clear();
// et on purge les données correspondantes du collector utilisées pour les deltas
globalRequestsByCounter.remove(counter);
for (final CounterRequest request : requests) {
requestsById.remove(request.getId());
requestJRobinsById.remove(request.getId());
}
}
}
void stop() {
try {
// on persiste les compteurs pour les relire à l'initialisation et ne pas perdre les stats
for (final Counter counter : counters) {
counter.writeToFile();
}
} catch (final IOException e) {
// persistance échouée, tant pis
LOG.warn("exception while writing counters data to files", e);
} finally {
for (final Counter counter : counters) {
counter.clear();
}
stopped = true;
// ici on ne fait pas de nettoyage de la liste counters car cette méthode
// est appelée sur la webapp monitorée quand il y a un serveur de collecte
// et que cette liste est envoyée au serveur de collecte,
// et on ne fait pas de nettoyage des maps qui servent dans le cas
// où le monitoring de la webapp monitorée est appelée par un navigateur
// directement même si il y a par ailleurs un serveur de collecte
// (dans ce dernier cas les données sont bien sûr partielles)
}
}
boolean isStopped() {
return stopped;
}
static void stopJRobin() {
try {
JRobin.stop();
} catch (final Throwable t) { // NOPMD
LOG.warn("stopping jrobin failed", t);
}
}
static void detachVirtualMachine() {
try {
VirtualMachine.detach();
} catch (final Throwable t) { // NOPMD
LOG.warn("exception while detaching virtual machine", t);
}
}
/** {@inheritDoc} */
@Override
public String toString() {
return getClass().getSimpleName() + "[application=" + getApplication() + ", periodMillis="
+ periodMillis + ", counters=" + getCounters() + ']';
}
}