/* * 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.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Données statistiques des requêtes pour un compteur nommé comme http ou sql. * Ces données sont accumulées au fil du temps selon les requêtes dans l'application. * Elles correspondent soit aux statistiques courantes depuis une date initiale, * soit à une période donnée pour un jour, une semaine, un mois ou une année. * * Toutes les méthodes sur une instance de cette classe sont conçues pour être thread-safe, * c'est-à-dire qu'elles gère un état qui est non modifiable * ou alors synchronisé pour être accessible et modifiable depuis plusieurs threads. * Les instances sont sérialisables pour pouvoir être persistées sur disque * et transmises au serveur de collecte. * @author Emeric Vernat */ class Counter implements Cloneable, Serializable { // NOPMD /** * Nom du counter des requêtes http. */ static final String HTTP_COUNTER_NAME = "http"; /** * Nom du counter des erreurs systèmes http. */ static final String ERROR_COUNTER_NAME = "error"; /** * Nom du counter des logs d'erreurs systèmes. */ static final String LOG_COUNTER_NAME = "log"; /** * Nom du counter des JSPs. */ static final String JSP_COUNTER_NAME = "jsp"; /** * Nom du counter des actions Struts. */ static final String STRUTS_COUNTER_NAME = "struts"; /** * Nom du counter des actions JSF RI (Mojarra). */ static final String JSF_COUNTER_NAME = "jsf"; /** * Nom du counter des requêtes SQL. */ static final String SQL_COUNTER_NAME = "sql"; /** * Nom du counter des jobs. */ static final String JOB_COUNTER_NAME = "job"; /** * Nom du counter des builds Jenkins. */ static final String BUILDS_COUNTER_NAME = "builds"; /** * Caractère de remplacement s'il y a des paramètres *-transform-pattern. */ static final char TRANSFORM_REPLACEMENT_CHAR = '$'; /** * Nombre max d'erreurs conservées par le counter (si counter d'erreurs http ou de log d'erreurs). */ static final int MAX_ERRORS_COUNT = 100; /** * Nombre max par défaut de requêtes conservées par counter, <br/> * mais peut être redéfini par exemple pour le counter des erreurs http ou celui des logs. */ static final int MAX_REQUESTS_COUNT = 10000; private static final String TRANSFORM_REPLACEMENT = "\\" + TRANSFORM_REPLACEMENT_CHAR; private static final long serialVersionUID = 6759729262180992976L; private String application; private boolean displayed = true; private transient boolean used; private final String name; private final boolean errorCounter; private final String storageName; private final String iconName; // on conserve childCounterName et pas childCounter pour assurer la synchronisation/clone et la sérialisation private final String childCounterName; @SuppressWarnings("all") private final ConcurrentMap<String, CounterRequest> requests = new ConcurrentHashMap<String, CounterRequest>(); // note : même si rootCurrentContextsByThreadId n'est pas transient la map est normalement vide avant sérialisation // (on garde en non transient pour ne pas avoir null après désérialisation ce qui pourrait donner des NPE) @SuppressWarnings("all") private final ConcurrentMap<Long, CounterRequestContext> rootCurrentContextsByThreadId = new ConcurrentHashMap<Long, CounterRequestContext>(); private final LinkedList<CounterError> errors; // NOPMD private Date startDate = new Date(); private int maxRequestsCount = MAX_REQUESTS_COUNT; private long estimatedMemorySize; // Pour les contextes, on utilise un ThreadLocal et pas un InheritableThreadLocal // puisque si on crée des threads alors la requête parente peut se terminer avant les threads // et le contexte serait incomplet. private final transient ThreadLocal<CounterRequestContext> contextThreadLocal; private transient Pattern requestTransformPattern; /** * Comparateur pour ordonner les requêtes par sommes des durées. */ static final class CounterRequestComparator implements Comparator<CounterRequest>, Serializable { private static final long serialVersionUID = 1L; /** {@inheritDoc} */ @Override public int compare(CounterRequest request1, CounterRequest request2) { if (request1.getDurationsSum() > request2.getDurationsSum()) { return 1; } else if (request1.getDurationsSum() < request2.getDurationsSum()) { return -1; } else { return 0; } } } /** * Comparateur pour ordonner les requêtes par nombre d'exécutions. */ static final class CounterRequestByHitsComparator implements Comparator<CounterRequest>, Serializable { private static final long serialVersionUID = 1L; /** {@inheritDoc} */ @Override public int compare(CounterRequest request1, CounterRequest request2) { if (request1.getHits() > request2.getHits()) { return 1; } else if (request1.getHits() < request2.getHits()) { return -1; } else { return 0; } } } /** * Comparateur pour ordonner les erreurs par heures d'exécution. */ static final class CounterErrorComparator implements Comparator<CounterError>, Serializable { private static final long serialVersionUID = 1L; /** {@inheritDoc} */ @Override public int compare(CounterError error1, CounterError error2) { if (error1.getTime() < error2.getTime()) { return -1; } else if (error1.getTime() > error2.getTime()) { return 1; } return 0; } } /** * Comparateur pour ordonner les requêtes en cours par durées écoulées. */ static final class CounterRequestContextComparator implements Comparator<CounterRequestContext>, Serializable { private static final long serialVersionUID = 1L; private final long timeOfSnapshot; CounterRequestContextComparator(long timeOfSnapshot) { super(); this.timeOfSnapshot = timeOfSnapshot; } /** {@inheritDoc} */ @Override public int compare(CounterRequestContext context1, CounterRequestContext context2) { if (context1.getDuration(timeOfSnapshot) > context2.getDuration(timeOfSnapshot)) { return 1; } else if (context1.getDuration(timeOfSnapshot) < context2 .getDuration(timeOfSnapshot)) { return -1; } else { return 0; } } } /** * Constructeur d'un compteur. * @param name Nom du compteur (par exemple: sql...) * @param iconName Icône du compteur (par exemple: db.png) */ Counter(String name, String iconName) { // ici, pas de compteur fils this(name, name, iconName, null, new ThreadLocal<CounterRequestContext>()); } /** * Constructeur d'un compteur. * @param name Nom du compteur (par exemple: sql...) * @param storageName Nom unique du compteur pour le stockage (par exemple: sql_20080724) * @param iconName Icône du compteur (par exemple: db.png) * @param childCounterName Nom du compteur fils (par exemple: sql) */ Counter(String name, String storageName, String iconName, String childCounterName) { this(name, storageName, iconName, childCounterName, new ThreadLocal<CounterRequestContext>()); } /** * Constructeur d'un compteur. * @param name Nom du compteur (par exemple: http...) * @param iconName Icône du compteur (par exemple: db.png) * @param childCounter Compteur fils (par exemple: sqlCounter) */ Counter(String name, String iconName, Counter childCounter) { this(name, name, iconName, childCounter.getName(), childCounter.contextThreadLocal); } private Counter(String name, String storageName, String iconName, String childCounterName, ThreadLocal<CounterRequestContext> contextThreadLocal) { super(); assert name != null; assert storageName != null; this.name = name; this.storageName = storageName; this.errorCounter = ERROR_COUNTER_NAME.equals(name) || LOG_COUNTER_NAME.equals(name) || JOB_COUNTER_NAME.equals(name); this.iconName = iconName; this.childCounterName = childCounterName; this.contextThreadLocal = contextThreadLocal; if (errorCounter) { this.errors = new LinkedList<CounterError>(); } else { this.errors = null; } } /** * Définit le code de l'application de ce counter (non null). * @param application String */ void setApplication(String application) { assert application != null; this.application = application; } /** * Retourne le code de l'application. * @return String */ String getApplication() { return application; } /** * Retourne le nom de ce counter (non null). * @return String */ String getName() { return name; } /** * Retourne le nom de ce counter quand il est stocké sur disque (non null). * @return String */ String getStorageName() { return storageName; } /** * Retourne le nom de l'icône de ce counter (peut être null). * @return String */ String getIconName() { return iconName; } /** * Retourne le nom de l'éventuel counter fils (peut être null). * @return String */ String getChildCounterName() { return childCounterName; } boolean hasChildHits() { for (final CounterRequest request : requests.values()) { if (request.hasChildHits()) { return true; } } return false; } /** * Retourne la date et l'heure de début (non null). * @return Date */ Date getStartDate() { return startDate; } /** * Définit la date et l'heure de début (non null). * @param startDate Date */ void setStartDate(Date startDate) { assert startDate != null; this.startDate = startDate; } /** * Retourne true si ce counter est affiché dans les rapports. * @return boolean */ boolean isDisplayed() { return displayed; } /** * Définit si ce counter est affiché dans les rapports. * @param displayed boolean */ void setDisplayed(boolean displayed) { this.displayed = displayed; } /** * Retourne true si ce counter est utilisé * (servira éventuellement à initialiser displayed dans FilterContext). * @return boolean */ boolean isUsed() { return used; } /** * Définit si ce counter est utilisé * (servira éventuellement à initialiser displayed dans FilterContext). * @param used boolean */ void setUsed(boolean used) { this.used = used; } /** * Retourne l'expression régulière permettant de transformer les requêtes de ce counter * avant agrégation dans les statistiques (peut être null). * @return Pattern */ Pattern getRequestTransformPattern() { return requestTransformPattern; } /** * Définit l'expression régulière permettant de transformer les requêtes de ce counter * avant agrégation dans les statistiques. * @param requestTransformPattern Pattern */ void setRequestTransformPattern(Pattern requestTransformPattern) { this.requestTransformPattern = requestTransformPattern; } /** * Retourne le nombre maximum de requêtes dans ce counter (entier positif). * @return int */ int getMaxRequestsCount() { return maxRequestsCount; } /** * Définit le nombre maximum de requêtes dans ce counter (entier positif). * @param maxRequestsCount int */ void setMaxRequestsCount(int maxRequestsCount) { assert maxRequestsCount > 0; this.maxRequestsCount = maxRequestsCount; } /** * Retourne l'estimation pessimiste de l'occupation mémoire de counter * (c'est-à-dire la dernière taille sérialisée non compressée de ce counter) * @return long */ long getEstimatedMemorySize() { return estimatedMemorySize; } void bindContextIncludingCpu(String requestName) { bindContext(requestName, requestName, null, ThreadInformations.getCurrentThreadCpuTime()); } void bindContext(String requestName, String completeRequestName, String remoteUser, long startCpuTime) { // requestName est la même chose que ce qui sera utilisée dans addRequest, // completeRequestName est la même chose éventuellement complétée // pour cette requête à destination de l'affichage dans les requêtes courantes // (sinon mettre 2 fois la même chose) final CounterRequestContext context = new CounterRequestContext(this, contextThreadLocal.get(), requestName, completeRequestName, remoteUser, startCpuTime); contextThreadLocal.set(context); if (context.getParentContext() == null) { rootCurrentContextsByThreadId.put(context.getThreadId(), context); } } void unbindContext() { try { contextThreadLocal.remove(); } finally { rootCurrentContextsByThreadId.remove(Thread.currentThread().getId()); } } void addRequestForCurrentContext(boolean systemError) { final CounterRequestContext context = contextThreadLocal.get(); if (context != null) { final long duration = context.getDuration(System.currentTimeMillis()); final long cpuUsedMillis = context.getCpuTime(); addRequest(context.getRequestName(), duration, cpuUsedMillis, systemError, -1); } } void addRequestForCurrentContext(String systemErrorStackTrace) { assert errorCounter; final CounterRequestContext context = contextThreadLocal.get(); // context peut être null (depuis JobGlobalListener, cf issue 34) if (context != null) { final long duration = context.getDuration(System.currentTimeMillis()); final long cpuUsedMillis = context.getCpuTime(); addRequest(context.getRequestName(), duration, cpuUsedMillis, systemErrorStackTrace != null, systemErrorStackTrace, -1); } } void addRequest(String requestName, long duration, long cpuTime, boolean systemError, int responseSize) { addRequest(requestName, duration, cpuTime, systemError, null, responseSize); } private void addRequest(String requestName, long duration, long cpuTime, boolean systemError, String systemErrorStackTrace, int responseSize) { // la méthode addRequest n'est pas synchronisée pour ne pas avoir // de synchronisation globale à l'application sur cette instance d'objet // ce qui pourrait faire une contention et des ralentissements, // par contre la map requests est synchronisée pour les modifications concurrentes assert requestName != null; assert duration >= 0; assert cpuTime >= -1; // -1 pour requêtes sql assert responseSize >= -1; // -1 pour requêtes sql final String aggregateRequestName = getAggregateRequestName(requestName); final CounterRequestContext context = contextThreadLocal.get(); final CounterRequest request = getCounterRequestInternal(aggregateRequestName); synchronized (request) { // on synchronise par l'objet request pour éviter de mélanger des ajouts de hits // concurrents entre plusieurs threads pour le même type de requête. // Rq : on pourrait remplacer ce bloc synchronized par un synchronized // sur les méthodes addHit et addChildHits dans la classe CounterRequest. request.addHit(duration, cpuTime, systemError, systemErrorStackTrace, responseSize); if (context != null) { // on ajoute dans la requête parente toutes les requêtes filles du contexte if (context.getParentCounter() == this) { request.addChildHits(context); } request.addChildRequests(context.getChildRequestsExecutionsByRequestId()); } } // perf: on fait le reste hors du synchronized sur request if (context != null) { if (context.getParentCounter() == this) { final CounterRequestContext parentContext = context.getParentContext(); if (parentContext == null) { // enlève du threadLocal le contexte que j'ai créé // si je suis le counter parent et s'il n'y a pas de contexte parent unbindContext(); } else { // on ajoute une requête fille dans le contexte context.addChildRequest(this, aggregateRequestName, request.getId(), duration, systemError, responseSize); // et reporte les requêtes filles dans le contexte parent et rebinde celui-ci parentContext.closeChildContext(); contextThreadLocal.set(parentContext); } } else { // on ajoute une requête fille dans le contexte // (à priori il s'agit d'une requête sql) context.addChildRequest(this, aggregateRequestName, request.getId(), duration, systemError, responseSize); } } if (systemErrorStackTrace != null) { assert errorCounter; synchronized (errors) { errors.addLast(new CounterError(requestName, systemErrorStackTrace)); if (errors.size() > MAX_ERRORS_COUNT) { errors.removeFirst(); } } } } void addRequestForSystemError(String requestName, long duration, long cpuTime, String stackTrace) { // comme la méthode addRequest, cette méthode n'est pas synchronisée pour ne pas avoir // de synchronisation globale à l'application sur cette instance d'objet // ce qui pourrait faire une contention et des ralentissements, // par contre on synchronise request et errors assert requestName != null; assert duration >= -1; // -1 pour le counter de log assert cpuTime >= -1; // on ne doit conserver les stackTraces que pour les compteurs d'erreurs et de logs plus limités en taille // car sinon cela risquerait de donner des compteurs trop gros en mémoire et sur disque assert errorCounter; // le code ci-après suppose qu'il n'y a pas de contexte courant pour les erreurs systèmes // contrairement à la méthode addRequest assert contextThreadLocal.get() == null; final String aggregateRequestName = getAggregateRequestName(requestName); final CounterRequest request = getCounterRequestInternal(aggregateRequestName); synchronized (request) { request.addHit(duration, cpuTime, true, stackTrace, -1); } synchronized (errors) { errors.addLast(new CounterError(requestName, stackTrace)); if (errors.size() > MAX_ERRORS_COUNT) { errors.removeFirst(); } } } void addRumHit(String requestName, long networkTime, long domProcessing, long pageRendering) { assert HTTP_COUNTER_NAME.equals(name); final String aggregateRequestName = getAggregateRequestName(requestName); final CounterRequest request = requests.get(aggregateRequestName); if (request != null) { synchronized (request) { request.addRumHit(networkTime, domProcessing, pageRendering); } } } /** * Retourne true si ce counter est un counter d'error * (c'est-à-dire si son nom est "error", "log" ou "job") * @return boolean */ boolean isErrorCounter() { return errorCounter; } /** * Retourne true si ce counter est un counter de job * (c'est-à-dire si son nom est "job") * @return boolean */ boolean isJobCounter() { return JOB_COUNTER_NAME.equals(name); } /** * Retourne true si ce counter est un counter de jsp ou d'actions Struts * (c'est-à-dire si son nom est "jsp") * @return boolean */ boolean isJspOrStrutsCounter() { return JSP_COUNTER_NAME.equals(name) || STRUTS_COUNTER_NAME.equals(name); } /** * Retourne true si ce counter est un counter de "façades métiers" ou "business façades" * (c'est-à-dire si son nom est "ejb", "spring", "guice" ou "services") * @return boolean */ boolean isBusinessFacadeCounter() { return "services".equals(name) || "ejb".equals(name) || "spring".equals(name) || "guice".equals(name); } boolean isRequestIdFromThisCounter(String requestId) { // cela marche car requestId commence par counter.getName() selon CounterRequest.buildId return requestId.startsWith(getName()); } private String getAggregateRequestName(String requestName) { final String aggregateRequestName; if (requestTransformPattern == null) { aggregateRequestName = requestName; } else { // ce pattern optionnel permet de transformer la description de la requête // pour supprimer des parties variables (identifiant d'objet par exemple) // et pour permettre l'agrégation sur cette requête final Matcher matcher = requestTransformPattern.matcher(requestName); try { aggregateRequestName = matcher.replaceAll(TRANSFORM_REPLACEMENT); } catch (final StackOverflowError e) { // regexp can throw StackOverflowError for (A|B)* // see https://github.com/javamelody/javamelody/issues/480 LOG.warn(e.toString(), e); return requestName; } } return aggregateRequestName; } void addRequestsAndErrors(Counter newCounter) { assert getName().equals(newCounter.getName()); // Pour toutes les requêtes du compteur en paramètre, // on ajoute les hits aux requêtes de ce compteur // (utilisée dans serveur de collecte). // Rq: cette méthode est thread-safe comme les autres méthodes dans cette classe, // bien que cela ne soit à priori pas nécessaire telle qu'elle est utilisée dans CollectorServlet for (final CounterRequest newRequest : newCounter.getRequests()) { if (newRequest.getHits() > 0) { final CounterRequest request = getCounterRequestInternal(newRequest.getName()); synchronized (request) { request.addHits(newRequest); } } } int size = requests.size(); final int maxRequests = getMaxRequestsCount(); if (size > maxRequests) { // Si le nombre de requêtes est supérieur à 10000 (sql non bindé par ex.), // 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. // (utile pour une agrégation par année dans PeriodCounterFactory par ex.) // Mais inutile de le faire dans d'autres méthodes de Counter // car ce serait mauvais pour les perfs, cela ne laisserait aucune chance // à une nouvelle requête et car cela sera fait par la classe collector for (final CounterRequest request : requests.values()) { if (request.getHits() < 10) { removeRequest(request.getName()); size--; if (size <= maxRequests) { break; } } } } if (isErrorCounter()) { addErrors(newCounter.getErrors()); } } void addHits(CounterRequest counterRequest) { if (counterRequest.getHits() > 0) { // clone pour être thread-safe ici final CounterRequest newRequest = counterRequest.clone(); final CounterRequest request = getCounterRequestInternal(newRequest.getName()); synchronized (request) { request.addHits(newRequest); } } } void addErrors(List<CounterError> counterErrorList) { assert errorCounter; if (counterErrorList.isEmpty()) { return; } synchronized (errors) { if (!errors.isEmpty() && errors.get(0).getTime() > counterErrorList.get(0).getTime()) { // tant que faire se peut on les met à peu près dans l'ordre pour le sort ci après errors.addAll(0, counterErrorList); } else { errors.addAll(counterErrorList); } if (errors.size() > 1) { // "sort" a les mêmes performances sur LinkedList que sur ArrayList car il y a un tableau intermédiaire // (selon Implementation Patterns, Kent Beck) Collections.sort(errors, new CounterErrorComparator()); while (errors.size() > MAX_ERRORS_COUNT) { errors.removeFirst(); } } } } void removeRequest(String requestName) { assert requestName != null; requests.remove(requestName); } /** * Retourne l'objet CounterRequest correspondant au contexte de requête en cours en paramètre. * @param context CounterRequestContext * @return CounterRequest */ CounterRequest getCounterRequest(CounterRequestContext context) { return getCounterRequestByName(context.getRequestName()); } /** * Retourne l'objet CounterRequest correspondant au nom sans agrégation en paramètre. * @param requestName Nom de la requête sans agrégation par requestTransformPattern * @return CounterRequest */ CounterRequest getCounterRequestByName(String requestName) { // l'instance de CounterRequest retournée est clonée // (nécessaire pour protéger la synchronisation interne du counter), // son état peut donc être lu sans synchronisation // mais toute modification de cet état ne sera pas conservée final String aggregateRequestName = getAggregateRequestName(requestName); final CounterRequest request = getCounterRequestInternal(aggregateRequestName); synchronized (request) { return request.clone(); } } private CounterRequest getCounterRequestInternal(String requestName) { CounterRequest request = requests.get(requestName); if (request == null) { request = new CounterRequest(requestName, getName()); // putIfAbsent a l'avantage d'être garanti atomique, même si ce n'est pas indispensable final CounterRequest precedentRequest = requests.putIfAbsent(requestName, request); if (precedentRequest != null) { request = precedentRequest; } } return request; } /** * Retourne le nombre de requêtes dans ce counter. * @return int */ int getRequestsCount() { return requests.size(); } /** * @return Liste des requêtes non triées, * la liste et ses objets peuvent être utilisés sans synchronized et sans crainte d'accès concurrents. */ List<CounterRequest> getRequests() { // thread-safe : // on crée une copie de la collection et on clone ici chaque CounterRequest de manière synchronisée // de manière à ce que l'appelant n'ai pas à se préoccuper des synchronisations nécessaires // Rq : l'Iterator sur ConcurrentHashMap.values() est garanti ne pas lancer ConcurrentModificationException // même s'il y a des ajouts concurrents final List<CounterRequest> result = new ArrayList<CounterRequest>(requests.size()); for (final CounterRequest request : requests.values()) { // on synchronize sur request en cas d'ajout en parallèle d'un hit sur cette request synchronized (request) { result.add(request.clone()); } } return result; } /** * @return Liste des requêtes triées par durée cumulée décroissante, * la liste et ses objets peuvent être utilisés sans synchronized et sans crainte d'accès concurrents. */ List<CounterRequest> getOrderedRequests() { final List<CounterRequest> requestList = getRequests(); if (requestList.size() > 1) { Collections.sort(requestList, Collections.reverseOrder(new CounterRequestComparator())); } return requestList; } /** * @return Liste des requêtes triées par hits décroissants, * la liste et ses objets peuvent être utilisés sans synchronized et sans crainte d'accès concurrents. */ List<CounterRequest> getOrderedByHitsRequests() { final List<CounterRequest> requestList = getRequests(); if (requestList.size() > 1) { Collections.sort(requestList, Collections.reverseOrder(new CounterRequestByHitsComparator())); } return requestList; } /** * @return Liste des contextes de requêtes courantes triées par durée écoulée décroissante, * la liste peut être utilisée sans synchronized et sans crainte d'accès concurrents, * toutefois les contextes ne sont pas actuellement clonés dans cette méthode. */ List<CounterRequestContext> getOrderedRootCurrentContexts() { final List<CounterRequestContext> contextList = new ArrayList<CounterRequestContext>( rootCurrentContextsByThreadId.size()); for (final CounterRequestContext rootCurrentContext : rootCurrentContextsByThreadId .values()) { contextList.add(rootCurrentContext.clone()); } if (contextList.size() > 1) { Collections.sort(contextList, Collections .reverseOrder(new CounterRequestContextComparator(System.currentTimeMillis()))); } return contextList; } /** * @return Liste des erreurs triée par date croissante, * la liste et ses objets peuvent être utilisés sans synchronized et sans crainte d'accès concurrents. */ List<CounterError> getErrors() { if (errors == null) { return Collections.emptyList(); } synchronized (errors) { return new ArrayList<CounterError>(errors); } } /** * Retourne le nombre d'erreurs dans ce counter. * @return int */ int getErrorsCount() { if (errors == null) { return 0; } synchronized (errors) { return errors.size(); } } /** * Purge les requêtes, requêtes en cours et erreurs puis positionne la date et heure de début * à l'heure courante. */ void clear() { requests.clear(); rootCurrentContextsByThreadId.clear(); if (errors != null) { synchronized (errors) { errors.clear(); } } startDate = new Date(); } /** {@inheritDoc} */ @Override //CHECKSTYLE:OFF public Counter clone() { // NOPMD //CHECKSTYLE:ON final Counter clone = new Counter(getName(), getStorageName(), getIconName(), getChildCounterName(), new ThreadLocal<CounterRequestContext>()); clone.application = getApplication(); clone.startDate = getStartDate(); clone.maxRequestsCount = getMaxRequestsCount(); clone.displayed = isDisplayed(); clone.requestTransformPattern = getRequestTransformPattern(); // on ne copie pas rootCurrentContextsByThreadId car on ne fournit pas les requêtes en cours // qui sont très rapidement obsolètes au serveur de collecte (et sinon cela poserait la question // des clones de parentCounter, de l'agrégation, de la synchro d'horloge pour la durée // et des threadId pour la stack-trace), // et on ne copie pas contextThreadLocal, // et la méthode getRequests() clone les instances de CounterRequest for (final CounterRequest request : getRequests()) { clone.requests.put(request.getName(), request); } if (errors != null) { clone.errors.addAll(getErrors()); } return clone; } /** * Enregistre le counter. * @throws IOException e */ void writeToFile() throws IOException { // on clone le counter avant de le sérialiser pour ne pas avoir de problèmes de concurrences d'accès final Counter counter = this.clone(); // on n'écrit pas rootCurrentContextsByThreadId en fichier // puisque ces données ne seront plus vraies dans quelques secondes (clear pour être sûr ici) counter.rootCurrentContextsByThreadId.clear(); estimatedMemorySize = new CounterStorage(counter).writeToFile(); } /** * Lecture du counter depuis son fichier. * @throws IOException e */ void readFromFile() throws IOException { final Counter counter = new CounterStorage(this).readFromFile(); if (counter != null) { final Counter newCounter = clone(); startDate = counter.getStartDate(); requests.clear(); for (final CounterRequest request : counter.getRequests()) { requests.put(request.getName(), request); } if (errors != null) { errors.clear(); errors.addAll(counter.getErrors()); } // on ajoute les nouvelles requêtes enregistrées avant de lire le fichier // (par ex. les premières requêtes collectées par le serveur de collecte lors de l'initialisation) addRequestsAndErrors(newCounter); } } /** {@inheritDoc} */ @Override public String toString() { return getClass().getSimpleName() + "[application=" + getApplication() + ", name=" + getName() + ", storageName=" + getStorageName() + ", startDate=" + getStartDate() + ", childCounterName=" + getChildCounterName() + ", " + requests.size() + " requests, " + (errors == null ? "" : errors.size() + " errors, ") + "maxRequestsCount=" + getMaxRequestsCount() + ", displayed=" + isDisplayed() + ']'; } }