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.IOException;
21 import java.io.Serializable;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.Comparator;
25 import java.util.Date;
26 import java.util.LinkedList;
27 import java.util.List;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.ConcurrentMap;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32
33 import javax.servlet.http.HttpServletRequest;
34 import javax.servlet.http.HttpSession;
35
36 import net.bull.javamelody.SessionListener;
37 import net.bull.javamelody.internal.common.LOG;
38
39 /**
40 * Données statistiques des requêtes pour un compteur nommé comme http ou sql.
41 * Ces données sont accumulées au fil du temps selon les requêtes dans l'application.
42 * Elles correspondent soit aux statistiques courantes depuis une date initiale,
43 * soit à une période donnée pour un jour, une semaine, un mois ou une année.
44 *
45 * Toutes les méthodes sur une instance de cette classe sont conçues pour être thread-safe,
46 * c'est-à-dire qu'elles gère un état qui est non modifiable
47 * ou alors synchronisé pour être accessible et modifiable depuis plusieurs threads.
48 * Les instances sont sérialisables pour pouvoir être persistées sur disque
49 * et transmises au serveur de collecte.
50 * @author Emeric Vernat
51 */
52 public class Counter implements Cloneable, Serializable { // NOPMD
53 /**
54 * Nom du counter des requêtes http.
55 */
56 public static final String HTTP_COUNTER_NAME = "http";
57 /**
58 * Nom du counter des erreurs systèmes http.
59 */
60 public static final String ERROR_COUNTER_NAME = "error";
61 /**
62 * Nom du counter des logs d'erreurs systèmes.
63 */
64 public static final String LOG_COUNTER_NAME = "log";
65 /**
66 * Nom du counter des JSPs.
67 */
68 public static final String JSP_COUNTER_NAME = "jsp";
69 /**
70 * Nom du counter des actions Struts.
71 */
72 public static final String STRUTS_COUNTER_NAME = "struts";
73 /**
74 * Nom du counter des actions JSF RI (Mojarra).
75 */
76 public static final String JSF_COUNTER_NAME = "jsf";
77 /**
78 * Nom du counter des requêtes SQL.
79 */
80 public static final String SQL_COUNTER_NAME = "sql";
81 /**
82 * Nom du counter des jobs.
83 */
84 public static final String JOB_COUNTER_NAME = "job";
85 /**
86 * Nom du counter des builds Jenkins.
87 */
88 public static final String BUILDS_COUNTER_NAME = "builds";
89 /**
90 * Nombre max d'erreurs conservées par le counter (si counter d'erreurs http ou de log d'erreurs).
91 */
92 public static final int MAX_ERRORS_COUNT = 100;
93 /**
94 * Caractère de remplacement s'il y a des paramètres *-transform-pattern.
95 */
96 static final char TRANSFORM_REPLACEMENT_CHAR = '$';
97 /**
98 * Nombre max par défaut de requêtes conservées par counter, <br/>
99 * mais peut être redéfini par exemple pour le counter des erreurs http ou celui des logs.
100 */
101 static final int MAX_REQUESTS_COUNT = 10000;
102 private static final String TRANSFORM_REPLACEMENT = "\\" + TRANSFORM_REPLACEMENT_CHAR;
103 private static final long serialVersionUID = 6759729262180992976L;
104 private String application;
105 private boolean displayed = true;
106 private transient boolean used;
107 private final String name;
108 private final boolean errorCounter;
109 private final String storageName;
110 private final String iconName;
111 // on conserve childCounterName et pas childCounter pour assurer la synchronisation/clone et la sérialisation
112 private final String childCounterName;
113 @SuppressWarnings("all")
114 private final ConcurrentMap<String, CounterRequest> requests = new ConcurrentHashMap<>();
115 // note : même si rootCurrentContextsByThreadId n'est pas transient la map est normalement vide avant sérialisation
116 // (on garde en non transient pour ne pas avoir null après désérialisation ce qui pourrait donner des NPE)
117 @SuppressWarnings("all")
118 private final ConcurrentMap<Long, CounterRequestContext> rootCurrentContextsByThreadId = new ConcurrentHashMap<>();
119 private final LinkedList<CounterError> errors; // NOPMD
120 private Date startDate = new Date();
121 private int maxRequestsCount = MAX_REQUESTS_COUNT;
122 private long estimatedMemorySize;
123 // Pour les contextes, on utilise un ThreadLocal et pas un InheritableThreadLocal
124 // puisque si on crée des threads alors la requête parente peut se terminer avant les threads
125 // et le contexte serait incomplet.
126 private final transient ThreadLocal<CounterRequestContext> contextThreadLocal;
127 private transient Pattern requestTransformPattern;
128
129 /**
130 * Comparateur pour ordonner les requêtes par sommes des durées.
131 */
132 static final class CounterRequestComparator
133 implements Comparator<CounterRequest>, Serializable {
134 private static final long serialVersionUID = 1L;
135
136 /** {@inheritDoc} */
137 @Override
138 public int compare(CounterRequest request1, CounterRequest request2) {
139 return Long.compare(request1.getDurationsSum(), request2.getDurationsSum());
140 }
141 }
142
143 /**
144 * Comparateur pour ordonner les requêtes par nombre d'exécutions.
145 */
146 static final class CounterRequestByHitsComparator
147 implements Comparator<CounterRequest>, Serializable {
148 private static final long serialVersionUID = 1L;
149
150 /** {@inheritDoc} */
151 @Override
152 public int compare(CounterRequest request1, CounterRequest request2) {
153 return Long.compare(request1.getHits(), request2.getHits());
154 }
155 }
156
157 /**
158 * Comparateur pour ordonner les erreurs par heures d'exécution.
159 */
160 static final class CounterErrorComparator implements Comparator<CounterError>, Serializable {
161 private static final long serialVersionUID = 1L;
162
163 /** {@inheritDoc} */
164 @Override
165 public int compare(CounterError error1, CounterError error2) {
166 if (error1.getTime() < error2.getTime()) {
167 return -1;
168 } else if (error1.getTime() > error2.getTime()) {
169 return 1;
170 }
171 return 0;
172 }
173 }
174
175 /**
176 * Comparateur pour ordonner les requêtes en cours par durées écoulées.
177 */
178 public static final class CounterRequestContextComparator
179 implements Comparator<CounterRequestContext>, Serializable {
180 private static final long serialVersionUID = 1L;
181 private final long timeOfSnapshot;
182
183 public CounterRequestContextComparator(long timeOfSnapshot) {
184 super();
185 this.timeOfSnapshot = timeOfSnapshot;
186 }
187
188 /** {@inheritDoc} */
189 @Override
190 public int compare(CounterRequestContext context1, CounterRequestContext context2) {
191 return Integer.compare(context1.getDuration(timeOfSnapshot),
192 context2.getDuration(timeOfSnapshot));
193 }
194 }
195
196 /**
197 * Constructeur d'un compteur.
198 * @param name Nom du compteur (par exemple: sql...)
199 * @param iconName Icône du compteur (par exemple: db.png)
200 */
201 public Counter(String name, String iconName) {
202 // ici, pas de compteur fils
203 this(name, name, iconName, null, new ThreadLocal<CounterRequestContext>());
204 }
205
206 /**
207 * Constructeur d'un compteur.
208 * @param name Nom du compteur (par exemple: sql...)
209 * @param storageName Nom unique du compteur pour le stockage (par exemple: sql_20080724)
210 * @param iconName Icône du compteur (par exemple: db.png)
211 * @param childCounterName Nom du compteur fils (par exemple: sql)
212 */
213 public Counter(String name, String storageName, String iconName, String childCounterName) {
214 this(name, storageName, iconName, childCounterName,
215 new ThreadLocal<CounterRequestContext>());
216 }
217
218 /**
219 * Constructeur d'un compteur.
220 * @param name Nom du compteur (par exemple: http...)
221 * @param iconName Icône du compteur (par exemple: db.png)
222 * @param childCounter Compteur fils (par exemple: sqlCounter)
223 */
224 public Counter(String name, String iconName, Counter childCounter) {
225 this(name, name, iconName, childCounter.getName(), childCounter.contextThreadLocal);
226 }
227
228 private Counter(String name, String storageName, String iconName, String childCounterName,
229 ThreadLocal<CounterRequestContext> contextThreadLocal) {
230 super();
231 assert name != null;
232 assert storageName != null;
233 this.name = name;
234 this.storageName = storageName;
235 this.errorCounter = ERROR_COUNTER_NAME.equals(name) || LOG_COUNTER_NAME.equals(name)
236 || JOB_COUNTER_NAME.equals(name);
237 this.iconName = iconName;
238 this.childCounterName = childCounterName;
239 this.contextThreadLocal = contextThreadLocal;
240 if (errorCounter) {
241 this.errors = new LinkedList<>();
242 } else {
243 this.errors = null;
244 }
245 }
246
247 /**
248 * Définit le code de l'application de ce counter (non null).
249 * @param application String
250 */
251 void setApplication(String application) {
252 assert application != null;
253 this.application = application;
254 }
255
256 /**
257 * Retourne le code de l'application.
258 * @return String
259 */
260 String getApplication() {
261 return application;
262 }
263
264 /**
265 * Retourne le nom de ce counter (non null).
266 * @return String
267 */
268 public String getName() {
269 return name;
270 }
271
272 /**
273 * Retourne le nom de ce counter quand il est stocké sur disque (non null).
274 * @return String
275 */
276 public String getStorageName() {
277 return storageName;
278 }
279
280 /**
281 * Retourne le nom de l'icône de ce counter (peut être null).
282 * @return String
283 */
284 public String getIconName() {
285 return iconName;
286 }
287
288 /**
289 * Retourne le nom de l'éventuel counter fils (peut être null).
290 * @return String
291 */
292 public String getChildCounterName() {
293 return childCounterName;
294 }
295
296 boolean hasChildHits() {
297 for (final CounterRequest request : requests.values()) {
298 if (request.hasChildHits()) {
299 return true;
300 }
301 }
302 return false;
303 }
304
305 /**
306 * Retourne la date et l'heure de début (non null).
307 * @return Date
308 */
309 public Date getStartDate() {
310 return startDate;
311 }
312
313 /**
314 * Définit la date et l'heure de début (non null).
315 * @param startDate Date
316 */
317 void setStartDate(Date startDate) {
318 assert startDate != null;
319 this.startDate = startDate;
320 }
321
322 /**
323 * Retourne true si ce counter est affiché dans les rapports.
324 * @return boolean
325 */
326 public boolean isDisplayed() {
327 return displayed;
328 }
329
330 /**
331 * Définit si ce counter est affiché dans les rapports.
332 * @param displayed boolean
333 */
334 public void setDisplayed(boolean displayed) {
335 this.displayed = displayed;
336 }
337
338 /**
339 * Retourne true si ce counter est utilisé
340 * (servira éventuellement à initialiser displayed dans FilterContext).
341 * @return boolean
342 */
343 public boolean isUsed() {
344 return used;
345 }
346
347 /**
348 * Définit si ce counter est utilisé
349 * (servira éventuellement à initialiser displayed dans FilterContext).
350 * @param used boolean
351 */
352 public void setUsed(boolean used) {
353 this.used = used;
354 }
355
356 /**
357 * Retourne l'expression régulière permettant de transformer les requêtes de ce counter
358 * avant agrégation dans les statistiques (peut être null).
359 * @return Pattern
360 */
361 Pattern getRequestTransformPattern() {
362 return requestTransformPattern;
363 }
364
365 /**
366 * Définit l'expression régulière permettant de transformer les requêtes de ce counter
367 * avant agrégation dans les statistiques.
368 * @param requestTransformPattern Pattern
369 */
370 public void setRequestTransformPattern(Pattern requestTransformPattern) {
371 this.requestTransformPattern = requestTransformPattern;
372 }
373
374 /**
375 * Retourne le nombre maximum de requêtes dans ce counter (entier positif).
376 * @return int
377 */
378 int getMaxRequestsCount() {
379 return maxRequestsCount;
380 }
381
382 /**
383 * Définit le nombre maximum de requêtes dans ce counter (entier positif).
384 * @param maxRequestsCount int
385 */
386 public void setMaxRequestsCount(int maxRequestsCount) {
387 assert maxRequestsCount > 0;
388 this.maxRequestsCount = maxRequestsCount;
389 }
390
391 /**
392 * Retourne l'estimation pessimiste de l'occupation mémoire de counter
393 * (c'est-à-dire la dernière taille sérialisée non compressée de ce counter)
394 * @return long
395 */
396 long getEstimatedMemorySize() {
397 return estimatedMemorySize;
398 }
399
400 public void bindContextIncludingCpu(String requestName) {
401 bindContext(requestName, requestName, null, ThreadInformations.getCurrentThreadCpuTime(),
402 ThreadInformations.getCurrentThreadAllocatedBytes());
403 }
404
405 public void bindContext(String requestName, String completeRequestName,
406 HttpServletRequest httpRequest, long startCpuTime, long startAllocatedBytes) {
407 String remoteUser = null;
408 String sessionId = null;
409 if (httpRequest != null) {
410 remoteUser = httpRequest.getRemoteUser();
411 final HttpSession session = httpRequest.getSession(false);
412 if (session != null) {
413 sessionId = session.getId();
414 if (remoteUser == null) {
415 final Object userAttribute = session
416 .getAttribute(SessionListener.SESSION_REMOTE_USER);
417 if (userAttribute instanceof String) {
418 remoteUser = (String) userAttribute;
419 }
420 }
421 }
422 }
423 // requestName est la même chose que ce qui sera utilisée dans addRequest,
424 // completeRequestName est la même chose éventuellement complétée
425 // pour cette requête à destination de l'affichage dans les requêtes courantes
426 // (sinon mettre 2 fois la même chose)
427 final CounterRequestContext context = new CounterRequestContext(this,
428 contextThreadLocal.get(), requestName, completeRequestName, httpRequest, remoteUser,
429 startCpuTime, startAllocatedBytes, sessionId);
430 contextThreadLocal.set(context);
431 if (context.getParentContext() == null) {
432 rootCurrentContextsByThreadId.put(context.getThreadId(), context);
433 }
434 }
435
436 public void unbindContext() {
437 try {
438 contextThreadLocal.remove();
439 } finally {
440 rootCurrentContextsByThreadId.remove(Thread.currentThread().getId());
441 }
442 }
443
444 public void addRequestForCurrentContext(boolean systemError) {
445 final CounterRequestContext context = contextThreadLocal.get();
446 if (context != null) {
447 final long duration = context.getDuration(System.currentTimeMillis());
448 final int cpuUsedMillis = context.getCpuTime();
449 final int allocatedKBytes = context.getAllocatedKBytes();
450 addRequest(context.getRequestName(), duration, cpuUsedMillis, allocatedKBytes,
451 systemError, -1);
452 }
453 }
454
455 public void addRequestForCurrentContext(String systemErrorStackTrace) {
456 assert errorCounter;
457 final CounterRequestContext context = contextThreadLocal.get();
458 // context peut être null (depuis JobGlobalListener, cf issue 34)
459 if (context != null) {
460 final long duration = context.getDuration(System.currentTimeMillis());
461 final int cpuUsedMillis = context.getCpuTime();
462 final int allocatedKBytes = context.getAllocatedKBytes();
463 addRequest(context.getRequestName(), duration, cpuUsedMillis, allocatedKBytes,
464 systemErrorStackTrace != null, systemErrorStackTrace, -1);
465 }
466 }
467
468 public void addRequest(String requestName, long duration, int cpuTime, int allocatedKBytes,
469 boolean systemError, long responseSize) {
470 addRequest(requestName, duration, cpuTime, allocatedKBytes, systemError, null,
471 responseSize);
472 }
473
474 private void addRequest(String requestName, long duration, int cpuTime, int allocatedKBytes,
475 boolean systemError, String systemErrorStackTrace, long responseSize) {
476 // la méthode addRequest n'est pas synchronisée pour ne pas avoir
477 // de synchronisation globale à l'application sur cette instance d'objet
478 // ce qui pourrait faire une contention et des ralentissements,
479 // par contre la map requests est synchronisée pour les modifications concurrentes
480
481 assert requestName != null;
482 assert duration >= 0;
483 assert cpuTime >= -1; // -1 pour requêtes sql
484 assert allocatedKBytes >= -1; // -1 pour requêtes sql
485 assert responseSize >= -1L; // -1 pour requêtes sql
486
487 final String aggregateRequestName = getAggregateRequestName(requestName);
488
489 final CounterRequestContext context = contextThreadLocal.get();
490 final CounterRequest request = getCounterRequestInternal(aggregateRequestName);
491 synchronized (request) {
492 // on synchronise par l'objet request pour éviter de mélanger des ajouts de hits
493 // concurrents entre plusieurs threads pour le même type de requête.
494 // Rq : on pourrait remplacer ce bloc synchronized par un synchronized
495 // sur les méthodes addHit et addChildHits dans la classe CounterRequest.
496 request.addHit(duration, cpuTime, allocatedKBytes, systemError, systemErrorStackTrace,
497 responseSize);
498
499 if (context != null) {
500 // on ajoute dans la requête parente toutes les requêtes filles du contexte
501 if (context.getParentCounter() == this) {
502 request.addChildHits(context);
503 }
504 request.addChildRequests(context.getChildRequestsExecutionsByRequestId());
505 }
506 }
507 // perf: on fait le reste hors du synchronized sur request
508 if (context != null) {
509 if (context.getParentCounter() == this) {
510 final CounterRequestContext parentContext = context.getParentContext();
511 if (parentContext == null) {
512 // enlève du threadLocal le contexte que j'ai créé
513 // si je suis le counter parent et s'il n'y a pas de contexte parent
514 unbindContext();
515 } else {
516 // on ajoute une requête fille dans le contexte
517 context.addChildRequest(this, aggregateRequestName, request.getId(), duration,
518 systemError, responseSize);
519 // et reporte les requêtes filles dans le contexte parent et rebinde celui-ci
520 parentContext.closeChildContext();
521 contextThreadLocal.set(parentContext);
522 }
523 } else {
524 // on ajoute une requête fille dans le contexte
525 // (à priori il s'agit d'une requête sql)
526 context.addChildRequest(this, aggregateRequestName, request.getId(), duration,
527 systemError, responseSize);
528 }
529 }
530 if (systemErrorStackTrace != null) {
531 assert errorCounter;
532 synchronized (errors) {
533 errors.addLast(new CounterError(requestName, systemErrorStackTrace));
534 if (errors.size() > MAX_ERRORS_COUNT) {
535 errors.removeFirst();
536 }
537 }
538 }
539 }
540
541 public void addRequestForSystemError(String requestName, long duration, int cpuTime,
542 int allocatedKBytes, String stackTrace) {
543 // comme la méthode addRequest, cette méthode n'est pas synchronisée pour ne pas avoir
544 // de synchronisation globale à l'application sur cette instance d'objet
545 // ce qui pourrait faire une contention et des ralentissements,
546 // par contre on synchronise request et errors
547 assert requestName != null;
548 assert duration >= -1; // -1 pour le counter de log
549 assert cpuTime >= -1;
550 // on ne doit conserver les stackTraces que pour les compteurs d'erreurs et de logs plus limités en taille
551 // car sinon cela risquerait de donner des compteurs trop gros en mémoire et sur disque
552 assert errorCounter;
553 // le code ci-après suppose qu'il n'y a pas de contexte courant pour les erreurs systèmes
554 // contrairement à la méthode addRequest
555 assert contextThreadLocal.get() == null;
556 final String aggregateRequestName = getAggregateRequestName(requestName);
557 final CounterRequest request = getCounterRequestInternal(aggregateRequestName);
558 synchronized (request) {
559 request.addHit(duration, cpuTime, allocatedKBytes, true, stackTrace, -1);
560 }
561 synchronized (errors) {
562 errors.addLast(new CounterError(requestName, stackTrace));
563 if (errors.size() > MAX_ERRORS_COUNT) {
564 errors.removeFirst();
565 }
566 }
567 }
568
569 public void addRumHit(String requestName, long networkTime, long domProcessing,
570 long pageRendering) {
571 assert HTTP_COUNTER_NAME.equals(name);
572 final String aggregateRequestName = getAggregateRequestName(requestName);
573 final CounterRequest request = requests.get(aggregateRequestName);
574 if (request != null) {
575 synchronized (request) {
576 request.addRumHit(networkTime, domProcessing, pageRendering);
577 }
578 }
579 }
580
581 /**
582 * Retourne true si ce counter est un counter d'error
583 * (c'est-à-dire si son nom est "error", "log" ou "job").
584 * @return boolean
585 */
586 public boolean isErrorCounter() {
587 return errorCounter;
588 }
589
590 /**
591 * Retourne true si ce counter est un counter de job
592 * (c'est-à-dire si son nom est "job").
593 * @return boolean
594 */
595 public boolean isJobCounter() {
596 return JOB_COUNTER_NAME.equals(name);
597 }
598
599 /**
600 * Retourne true si ce counter est un counter de jsp ou d'actions Struts
601 * (c'est-à-dire si son nom est "jsp").
602 * @return boolean
603 */
604 public boolean isJspOrStrutsCounter() {
605 return JSP_COUNTER_NAME.equals(name) || STRUTS_COUNTER_NAME.equals(name);
606 }
607
608 /**
609 * Retourne true si ce counter est un counter de "façades métiers" ou "business façades"
610 * (c'est-à-dire si son nom est "ejb", "spring", "guice" ou "services").
611 * @return boolean
612 */
613 public boolean isBusinessFacadeCounter() {
614 return "services".equals(name) || "ejb".equals(name) || "spring".equals(name)
615 || "guice".equals(name);
616 }
617
618 public boolean isRequestIdFromThisCounter(String requestId) {
619 // cela marche car requestId commence par counter.getName() selon CounterRequest.buildId
620 return requestId.startsWith(getName());
621 }
622
623 private String getAggregateRequestName(String requestName) {
624 final String aggregateRequestName;
625 if (requestTransformPattern == null) {
626 aggregateRequestName = requestName;
627 } else {
628 // ce pattern optionnel permet de transformer la description de la requête
629 // pour supprimer des parties variables (identifiant d'objet par exemple)
630 // et pour permettre l'agrégation sur cette requête
631 final Matcher matcher = requestTransformPattern.matcher(requestName);
632 try {
633 aggregateRequestName = matcher.replaceAll(TRANSFORM_REPLACEMENT);
634 } catch (final StackOverflowError e) {
635 // regexp can throw StackOverflowError for (A|B)*
636 // see https://github.com/javamelody/javamelody/issues/480
637 LOG.warn(e.toString(), e);
638 return requestName;
639 }
640 }
641 return aggregateRequestName;
642 }
643
644 void addRequestsAndErrors(Counter newCounter) {
645 assert getName().equals(newCounter.getName());
646
647 // Pour toutes les requêtes du compteur en paramètre,
648 // on ajoute les hits aux requêtes de ce compteur
649 // (utilisée dans serveur de collecte).
650
651 // Rq: cette méthode est thread-safe comme les autres méthodes dans cette classe,
652 // bien que cela ne soit à priori pas nécessaire telle qu'elle est utilisée dans CollectorServlet
653 for (final CounterRequest newRequest : newCounter.getRequests()) {
654 if (newRequest.getHits() > 0) {
655 final CounterRequest request = getCounterRequestInternal(newRequest.getName());
656 synchronized (request) {
657 request.addHits(newRequest);
658 }
659 }
660 }
661
662 int size = requests.size();
663 final int maxRequests = getMaxRequestsCount();
664 if (size > maxRequests) {
665 // Si le nombre de requêtes est supérieur à 10000 (sql non bindé par ex.),
666 // on essaye ici d'éviter de saturer la mémoire (et le disque dur)
667 // avec toutes ces requêtes différentes en éliminant celles ayant moins de 10 hits.
668 // (utile pour une agrégation par année dans PeriodCounterFactory par ex.)
669 // Mais inutile de le faire dans d'autres méthodes de Counter
670 // car ce serait mauvais pour les perfs, cela ne laisserait aucune chance
671 // à une nouvelle requête et car cela sera fait par la classe collector
672 for (final CounterRequest request : requests.values()) {
673 if (request.getHits() < 10) {
674 removeRequest(request.getName());
675 size--;
676 if (size <= maxRequests) {
677 break;
678 }
679 }
680 }
681 }
682
683 if (isErrorCounter()) {
684 addErrors(newCounter.getErrors());
685 }
686 }
687
688 void addHits(CounterRequest counterRequest) {
689 if (counterRequest.getHits() > 0) {
690 // clone pour être thread-safe ici
691 final CounterRequest newRequest = counterRequest.clone();
692 final CounterRequest request = getCounterRequestInternal(newRequest.getName());
693 synchronized (request) {
694 request.addHits(newRequest);
695 }
696 }
697 }
698
699 public void addErrors(List<CounterError> counterErrorList) {
700 assert errorCounter;
701 if (counterErrorList.isEmpty()) {
702 return;
703 }
704 synchronized (errors) {
705 if (!errors.isEmpty() && errors.get(0).getTime() > counterErrorList.get(0).getTime()) {
706 // tant que faire se peut on les met à peu près dans l'ordre pour le sort ci après
707 errors.addAll(0, counterErrorList);
708 } else {
709 errors.addAll(counterErrorList);
710 }
711 if (errors.size() > 1) {
712 // "sort" a les mêmes performances sur LinkedList que sur ArrayList car il y a un tableau intermédiaire
713 // (selon Implementation Patterns, Kent Beck)
714 Collections.sort(errors, new CounterErrorComparator());
715
716 while (errors.size() > MAX_ERRORS_COUNT) {
717 errors.removeFirst();
718 }
719 }
720 }
721 }
722
723 void removeRequest(String requestName) {
724 assert requestName != null;
725 requests.remove(requestName);
726 }
727
728 /**
729 * Retourne l'objet {@link CounterRequest} correspondant au contexte de requête en cours en paramètre.
730 * @param context CounterRequestContext
731 * @return CounterRequest
732 */
733 public CounterRequest getCounterRequest(CounterRequestContext context) {
734 return getCounterRequestByName(context.getRequestName(), false);
735 }
736
737 /**
738 * Retourne l'objet {@link CounterRequest} correspondant au nom sans agrégation en paramètre.
739 * @param requestName Nom de la requête sans agrégation par requestTransformPattern
740 * @param saveRequestIfAbsent true except for current requests because the requestName may not be yet bestMatchingPattern
741 * @return CounterRequest
742 */
743 public CounterRequest getCounterRequestByName(String requestName, boolean saveRequestIfAbsent) {
744 // l'instance de CounterRequest retournée est clonée
745 // (nécessaire pour protéger la synchronisation interne du counter),
746 // son état peut donc être lu sans synchronisation
747 // mais toute modification de cet état ne sera pas conservée
748 final String aggregateRequestName = getAggregateRequestName(requestName);
749 final CounterRequest request = getCounterRequestInternal(aggregateRequestName,
750 saveRequestIfAbsent);
751 synchronized (request) {
752 return request.clone();
753 }
754 }
755
756 private CounterRequest getCounterRequestInternal(String requestName) {
757 return getCounterRequestInternal(requestName, true);
758 }
759
760 private CounterRequest getCounterRequestInternal(String requestName,
761 boolean saveRequestIfAbsent) {
762 CounterRequest request = requests.get(requestName);
763 if (request == null) {
764 request = new CounterRequest(requestName, getName());
765 if (saveRequestIfAbsent) {
766 // putIfAbsent a l'avantage d'être garanti atomique, même si ce n'est pas indispensable
767 final CounterRequest precedentRequest = requests.putIfAbsent(requestName, request);
768 if (precedentRequest != null) {
769 request = precedentRequest;
770 }
771 }
772 }
773 return request;
774 }
775
776 /**
777 * Retourne l'objet {@link CounterRequest} correspondant à l'id en paramètre ou null sinon.
778 * @param requestId Id de la requête
779 * @return CounterRequest
780 */
781 public CounterRequest getCounterRequestById(String requestId) {
782 if (isRequestIdFromThisCounter(requestId)) {
783 for (final CounterRequest request : requests.values()) {
784 if (request.getId().equals(requestId)) {
785 synchronized (request) {
786 return request.clone();
787 }
788 }
789 }
790 }
791 return null;
792 }
793
794 /**
795 * Retourne le nombre de requêtes dans ce counter.
796 * @return int
797 */
798 public int getRequestsCount() {
799 return requests.size();
800 }
801
802 /**
803 * @return Liste des requêtes non triées,
804 * la liste et ses objets peuvent être utilisés sans synchronized et sans crainte d'accès concurrents.
805 */
806 public List<CounterRequest> getRequests() {
807 // thread-safe :
808 // on crée une copie de la collection et on clone ici chaque CounterRequest de manière synchronisée
809 // de manière à ce que l'appelant n'ai pas à se préoccuper des synchronisations nécessaires
810 // Rq : l'Iterator sur ConcurrentHashMap.values() est garanti ne pas lancer ConcurrentModificationException
811 // même s'il y a des ajouts concurrents
812 final List<CounterRequest> result = new ArrayList<>(requests.size());
813 for (final CounterRequest request : requests.values()) {
814 // on synchronize sur request en cas d'ajout en parallèle d'un hit sur cette request
815 synchronized (request) {
816 result.add(request.clone());
817 }
818 }
819 return result;
820 }
821
822 /**
823 * @return Liste des requêtes triées par durée cumulée décroissante,
824 * la liste et ses objets peuvent être utilisés sans synchronized et sans crainte d'accès concurrents.
825 */
826 public List<CounterRequest> getOrderedRequests() {
827 final List<CounterRequest> requestList = getRequests();
828 if (requestList.size() > 1) {
829 Collections.sort(requestList, Collections.reverseOrder(new CounterRequestComparator()));
830 }
831 return requestList;
832 }
833
834 /**
835 * @return Liste des requêtes triées par hits décroissants,
836 * la liste et ses objets peuvent être utilisés sans synchronized et sans crainte d'accès concurrents.
837 */
838 List<CounterRequest> getOrderedByHitsRequests() {
839 final List<CounterRequest> requestList = getRequests();
840 if (requestList.size() > 1) {
841 Collections.sort(requestList,
842 Collections.reverseOrder(new CounterRequestByHitsComparator()));
843 }
844 return requestList;
845 }
846
847 /**
848 * @return Liste des contextes de requêtes courantes triées par durée écoulée décroissante,
849 * la liste peut être utilisée sans synchronized et sans crainte d'accès concurrents,
850 * toutefois les contextes ne sont pas actuellement clonés dans cette méthode.
851 */
852 List<CounterRequestContext> getOrderedRootCurrentContexts() {
853 final List<CounterRequestContext> contextList = new ArrayList<>(
854 rootCurrentContextsByThreadId.size());
855 for (final CounterRequestContext rootCurrentContext : rootCurrentContextsByThreadId
856 .values()) {
857 contextList.add(rootCurrentContext.clone());
858 }
859 if (contextList.size() > 1) {
860 Collections.sort(contextList, Collections
861 .reverseOrder(new CounterRequestContextComparator(System.currentTimeMillis())));
862 }
863 return contextList;
864 }
865
866 /**
867 * @return Liste des erreurs triée par date croissante,
868 * la liste et ses objets peuvent être utilisés sans synchronized et sans crainte d'accès concurrents.
869 */
870 public List<CounterError> getErrors() {
871 if (errors == null) {
872 return Collections.emptyList();
873 }
874 synchronized (errors) {
875 return new ArrayList<>(errors);
876 }
877 }
878
879 /**
880 * Retourne le nombre d'erreurs dans ce counter.
881 * @return int
882 */
883 public int getErrorsCount() {
884 if (errors == null) {
885 return 0;
886 }
887 synchronized (errors) {
888 return errors.size();
889 }
890 }
891
892 /**
893 * Purge les requêtes et erreurs puis positionne la date et heure de début à l'heure courante,
894 * mais sans toucher aux requêtes en cours pour qu'elles restent affichées,
895 * par exemple dans le serveur de collecte (#871).
896 */
897 public void clear() {
898 requests.clear();
899 if (errors != null) {
900 synchronized (errors) {
901 errors.clear();
902 }
903 }
904 startDate = new Date();
905 }
906
907 /** {@inheritDoc} */
908 @Override
909 //CHECKSTYLE:OFF
910 public Counter clone() { // NOPMD
911 //CHECKSTYLE:ON
912 final Counter clone = new Counter(getName(), getStorageName(), getIconName(),
913 getChildCounterName(), new ThreadLocal<CounterRequestContext>());
914 clone.application = getApplication();
915 clone.startDate = getStartDate();
916 clone.maxRequestsCount = getMaxRequestsCount();
917 clone.displayed = isDisplayed();
918 clone.requestTransformPattern = getRequestTransformPattern();
919 // on ne copie pas rootCurrentContextsByThreadId car on ne fournit pas les requêtes en cours
920 // qui sont très rapidement obsolètes au serveur de collecte (et sinon cela poserait la question
921 // des clones de parentCounter, de l'agrégation, de la synchro d'horloge pour la durée
922 // et des threadId pour la stack-trace),
923 // et on ne copie pas contextThreadLocal,
924 // et la méthode getRequests() clone les instances de CounterRequest
925 for (final CounterRequest request : getRequests()) {
926 clone.requests.put(request.getName(), request);
927 }
928 if (errors != null) {
929 clone.errors.addAll(getErrors());
930 }
931 return clone;
932 }
933
934 /**
935 * Enregistre le counter.
936 * @throws IOException e
937 */
938 void writeToFile() throws IOException {
939 // on clone le counter avant de le sérialiser pour ne pas avoir de problèmes de concurrences d'accès
940 final Counter counter = this.clone();
941 // on n'écrit pas rootCurrentContextsByThreadId en fichier
942 // puisque ces données ne seront plus vraies dans quelques secondes (clear pour être sûr ici)
943 counter.rootCurrentContextsByThreadId.clear();
944 estimatedMemorySize = new CounterStorage(counter).writeToFile();
945 }
946
947 /**
948 * Lecture du counter depuis son fichier.
949 * @throws IOException e
950 */
951 void readFromFile() throws IOException {
952 final Counter counter = new CounterStorage(this).readFromFile();
953 if (counter != null) {
954 final Counter newCounter = clone();
955 startDate = counter.getStartDate();
956 requests.clear();
957 for (final CounterRequest request : counter.getRequests()) {
958 requests.put(request.getName(), request);
959 }
960 if (errors != null) {
961 errors.clear();
962 errors.addAll(counter.getErrors());
963 }
964 // on ajoute les nouvelles requêtes enregistrées avant de lire le fichier
965 // (par ex. les premières requêtes collectées par le serveur de collecte lors de l'initialisation)
966 addRequestsAndErrors(newCounter);
967 }
968 }
969
970 /** {@inheritDoc} */
971 @Override
972 public String toString() {
973 return getClass().getSimpleName() + "[application=" + getApplication() + ", name="
974 + getName() + ", storageName=" + getStorageName() + ", startDate=" + getStartDate()
975 + ", childCounterName=" + getChildCounterName() + ", " + requests.size()
976 + " requests, " + (errors == null ? "" : errors.size() + " errors, ")
977 + "maxRequestsCount=" + getMaxRequestsCount() + ", displayed=" + isDisplayed()
978 + ']';
979 }
980 }
981