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.web.html;
19
20 import java.io.IOException;
21 import java.io.Writer;
22 import java.text.DecimalFormat;
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.regex.Pattern;
27
28 import net.bull.javamelody.internal.common.I18N;
29 import net.bull.javamelody.internal.model.Counter;
30 import net.bull.javamelody.internal.model.CounterRequest;
31 import net.bull.javamelody.internal.model.CounterRequestAggregation;
32 import net.bull.javamelody.internal.model.Period;
33 import net.bull.javamelody.internal.model.Range;
34
35 /**
36  * Partie du rapport html pour un compteur.
37  * @author Emeric Vernat
38  */

39 public class HtmlCounterReport extends HtmlAbstractReport {
40     private static final Pattern SQL_KEYWORDS_PATTERN = Pattern.compile(
41             "\\b(select|from|where|order by|group by|update|delete|insert into|values)\\b",
42             Pattern.CASE_INSENSITIVE);
43     private final Counter counter;
44     private final Range range;
45     private final CounterRequestAggregation counterRequestAggregation;
46     private final HtmlCounterRequestGraphReport htmlCounterRequestGraphReport;
47     private final DecimalFormat systemErrorFormat = I18N.createPercentFormat();
48     private final DecimalFormat integerFormat = I18N.createIntegerFormat();
49
50     HtmlCounterReport(Counter counter, Range range, Writer writer) {
51         super(writer);
52         assert counter != null;
53         assert range != null;
54         this.counter = counter;
55         this.range = range;
56         this.counterRequestAggregation = new CounterRequestAggregation(counter);
57         this.htmlCounterRequestGraphReport = new HtmlCounterRequestGraphReport(range, writer);
58     }
59
60     @Override
61     void toHtml() throws IOException {
62         final List<CounterRequest> requests = counterRequestAggregation.getRequests();
63         if (requests.isEmpty()) {
64             writeNoRequests();
65             return;
66         }
67         final String counterName = counter.getName();
68         final CounterRequest globalRequest = counterRequestAggregation.getGlobalRequest();
69         // 1. synthèse
70         if (isErrorAndNotJobCounter()) {
71             // il y a au moins une "request" d'erreur puisque la liste n'est pas vide
72             assert !requests.isEmpty();
73             final List<CounterRequest> summaryRequest = Collections.singletonList(requests.get(0));
74             writeRequests(counterName, counter.getChildCounterName(), summaryRequest, falsetrue,
75                     false);
76         } else {
77             final List<CounterRequest> summaryRequests = Arrays.asList(globalRequest,
78                     counterRequestAggregation.getWarningRequest(),
79                     counterRequestAggregation.getSevereRequest());
80             writeRequests(globalRequest.getName(), counter.getChildCounterName(), summaryRequests,
81                     falsefalsefalse);
82         }
83
84         // 2. débit et liens
85         writeSizeAndLinks(requests, globalRequest);
86
87         // 3. détails par requêtes (non visible par défaut)
88         writeln("<div id='details" + counterName + "' class='displayNone'>");
89         writeRequests(counterName, counter.getChildCounterName(), requests,
90                 isRequestGraphDisplayed(counter), truefalse);
91         writeln("</div>");
92
93         // 4. logs (non visible par défaut)
94         if (isErrorCounter()) {
95             writeln("<div id='logs" + counterName + "' class='displayNone'><div>");
96             new HtmlCounterErrorReport(counter, getWriter()).toHtml();
97             writeln("</div></div>");
98         }
99     }
100
101     private void writeSizeAndLinks(List<CounterRequest> requests, CounterRequest globalRequest)
102             throws IOException {
103         final long end;
104         if (range.getEndDate() != null) {
105             // l'utilisateur a choisi une période personnalisée de date à date,
106             // donc la fin est peut-être avant la date du jour
107             end = Math.min(range.getEndDate().getTime(), System.currentTimeMillis());
108         } else {
109             end = System.currentTimeMillis();
110         }
111         // delta ni négatif ni à 0
112         final long deltaMillis = Math.max(end - counter.getStartDate().getTime(), 1);
113         final long hitsParMinute = 60 * 1000 * globalRequest.getHits() / deltaMillis;
114         writeln("<div align='right'>");
115         // Rq : si serveur utilisé de 8h à 20h (soit 12h) on peut multiplier par 2 ces hits par minute indiqués
116         // pour avoir une moyenne sur les heures d'activité sans la nuit
117         final String nbKey;
118         if (isJobCounter()) {
119             nbKey = "nb_jobs";
120         } else if (isErrorCounter()) {
121             nbKey = "nb_erreurs";
122         } else {
123             nbKey = "nb_requetes";
124         }
125         writeln(getFormattedString(nbKey, integerFormat.format(hitsParMinute),
126                 integerFormat.format(requests.size())));
127         final String separator = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
128         if (counter.isBusinessFacadeCounter()) {
129             writeln(separator);
130             writeln("<a href='?part=counterSummaryPerClass&amp;counter=" + counter.getName()
131                     + "' class='noPrint'>#Resume_par_classe#</a>");
132             if (isPdfEnabled()) {
133                 writeln(separator);
134                 writeln("<a href='?part=runtimeDependencies&amp;format=pdf&amp;counter="
135                         + counter.getName() + "' class='noPrint'>#Dependencies#</a>");
136             }
137         }
138         writeln(separator);
139         writeShowHideLink("details" + counter.getName(), "#Details#");
140         if (isErrorCounter()) {
141             writeln(separator);
142             writeShowHideLink("logs" + counter.getName(), "#Dernieres_erreurs#");
143         }
144         writeln(separator);
145         if (range.getPeriod() == Period.TOUT) {
146             writeln("<a href='?action=clear_counter&amp;counter=" + counter.getName()
147                     + getCsrfTokenUrlPart() + "' title='"
148                     + getFormattedString("Vider_stats", counter.getName()) + '\'');
149             writeln("class='confirm noPrint' data-confirm='"
150                     + htmlEncodeButNotSpaceAndNewLine(
151                             getFormattedString("confirm_vider_stats", counter.getName()))
152                     + "'>#Reinitialiser#</a>");
153         }
154         writeln("</div>");
155     }
156
157     private void writeNoRequests() throws IOException {
158         if (isJobCounter()) {
159             writeln("#Aucun_job#");
160         } else if (isErrorCounter()) {
161             writeln("#Aucune_erreur#");
162         } else {
163             writeln("#Aucune_requete#");
164         }
165     }
166
167     private boolean isErrorCounter() {
168         return counter.isErrorCounter();
169     }
170
171     private boolean isJobCounter() {
172         return counter.isJobCounter();
173     }
174
175     private boolean isErrorAndNotJobCounter() {
176         return isErrorCounter() && !isJobCounter();
177     }
178
179     public static boolean isRequestGraphDisplayed(Counter parentCounter) {
180         return !(parentCounter.isErrorCounter() && !parentCounter.isJobCounter())
181                 && !parentCounter.isJspOrStrutsCounter();
182     }
183
184     void writeRequestsAggregatedOrFilteredByClassName(String requestId) throws IOException {
185         final List<CounterRequest> requestList = counterRequestAggregation
186                 .getRequestsAggregatedOrFilteredByClassName(requestId);
187         final boolean includeSummaryPerClassLink = requestId == null;
188         final boolean includeDetailLink = !includeSummaryPerClassLink;
189         writeRequests(counter.getName(), counter.getChildCounterName(), requestList,
190                 includeDetailLink, includeDetailLink, includeSummaryPerClassLink);
191     }
192
193     private void writeRequests(String tableName, String childCounterName,
194             List<CounterRequest> requestList, boolean includeGraph, boolean includeDetailLink,
195             boolean includeSummaryPerClassLink) throws IOException {
196         assert requestList != null;
197         final HtmlTable table = new HtmlTable();
198         table.beginTable(tableName);
199         writeTableHead(childCounterName);
200         for (final CounterRequest request : requestList) {
201             table.nextRow();
202             writeRequest(request, includeGraph, includeDetailLink, includeSummaryPerClassLink);
203         }
204         table.endTable();
205     }
206
207     private void writeTableHead(String childCounterName) throws IOException {
208         if (isJobCounter()) {
209             write("<th>#Job#</th>");
210         } else if (isErrorCounter()) {
211             write("<th>#Erreur#</th>");
212         } else {
213             write("<th>#Requete#</th>");
214         }
215         if (counterRequestAggregation.isTimesDisplayed()) {
216             write("<th class='sorttable_numeric'>#temps_cumule#</th>");
217             write("<th class='sorttable_numeric'>#Hits#</th>");
218             write("<th class='sorttable_numeric'>#Temps_moyen#</th>");
219             write("<th class='sorttable_numeric'>#Temps_max#</th>");
220             write("<th class='sorttable_numeric'>#Ecart_type#</th>");
221         } else {
222             write("<th class='sorttable_numeric'>#Hits#</th>");
223         }
224         if (counterRequestAggregation.isCpuTimesDisplayed()) {
225             write("<th class='sorttable_numeric'>#temps_cpu_cumule#</th>");
226             write("<th class='sorttable_numeric'>#Temps_cpu_moyen#</th>");
227         }
228         if (counterRequestAggregation.isAllocatedKBytesDisplayed()) {
229             write("<th class='sorttable_numeric'>#Ko_alloues_moyens#</th>");
230         }
231         if (!isErrorAndNotJobCounter()) {
232             write("<th class='sorttable_numeric'>#erreur_systeme#</th>");
233         }
234         if (counterRequestAggregation.isResponseSizeDisplayed()) {
235             write("<th class='sorttable_numeric'>#Taille_moyenne#</th>");
236         }
237         if (counterRequestAggregation.isChildHitsDisplayed()) {
238             write("<th class='sorttable_numeric'>"
239                     + getFormattedString("hits_fils_moyens", childCounterName));
240             write("</th><th class='sorttable_numeric'>"
241                     + getFormattedString("temps_fils_moyen", childCounterName) + "</th>");
242         }
243     }
244
245     private void writeRequest(CounterRequest request, boolean includeGraph,
246             boolean includeDetailLink, boolean includeSummaryPerClassLink) throws IOException {
247         final String nextColumn = "</td> <td align='right'>";
248         write("<td class='wrappedText'>");
249         writeRequestName(request.getId(), request.getName(), includeGraph, includeDetailLink,
250                 includeSummaryPerClassLink);
251         final CounterRequest globalRequest = counterRequestAggregation.getGlobalRequest();
252         if (counterRequestAggregation.isTimesDisplayed()) {
253             write(nextColumn);
254             writePercentage(request.getDurationsSum(), globalRequest.getDurationsSum());
255             write(nextColumn);
256             write(integerFormat.format(request.getHits()));
257             write(nextColumn);
258             final int mean = request.getMean();
259             write("<span class='");
260             write(getSlaHtmlClass(mean));
261             write("'>");
262             write(integerFormat.format(mean));
263             write("</span>");
264             write(nextColumn);
265             write(integerFormat.format(request.getMaximum()));
266             write(nextColumn);
267             write(integerFormat.format(request.getStandardDeviation()));
268         } else {
269             write(nextColumn);
270             write(integerFormat.format(request.getHits()));
271         }
272         if (counterRequestAggregation.isCpuTimesDisplayed()) {
273             write(nextColumn);
274             writePercentage(request.getCpuTimeSum(), globalRequest.getCpuTimeSum());
275             write(nextColumn);
276             final int cpuTimeMean = request.getCpuTimeMean();
277             write("<span class='");
278             write(getSlaHtmlClass(cpuTimeMean));
279             write("'>");
280             write(integerFormat.format(cpuTimeMean));
281             write("</span>");
282         }
283         if (counterRequestAggregation.isAllocatedKBytesDisplayed()) {
284             write(nextColumn);
285             final int allocatedKBytesMean = request.getAllocatedKBytesMean();
286             write(integerFormat.format(allocatedKBytesMean));
287         }
288         if (!isErrorAndNotJobCounter()) {
289             write(nextColumn);
290             write(systemErrorFormat.format(request.getSystemErrorPercentage()));
291         }
292         if (counterRequestAggregation.isResponseSizeDisplayed()) {
293             write(nextColumn);
294             write(integerFormat.format(request.getResponseSizeMean() / 1024L));
295         }
296         if (counterRequestAggregation.isChildHitsDisplayed()) {
297             write(nextColumn);
298             write(integerFormat.format(request.getChildHitsMean()));
299             write(nextColumn);
300             write(integerFormat.format(request.getChildDurationsMean()));
301         }
302         write("</td>");
303     }
304
305     void writeRequestName(String requestId, String requestName, boolean includeGraph,
306             boolean includeDetailLink, boolean includeSummaryPerClassLink) throws IOException {
307         if (includeGraph) {
308             assert includeDetailLink;
309             assert !includeSummaryPerClassLink;
310             htmlCounterRequestGraphReport.writeRequestGraph(requestId, requestName);
311         } else if (includeDetailLink) {
312             assert !includeSummaryPerClassLink;
313             write("<a href='?part=graph&amp;graph=");
314             write(requestId);
315             write("'>");
316             // writeDirectly pour ne pas gérer de traductions si le nom contient '#'
317             writeDirectly(htmlEncodeRequestName(requestId, requestName));
318             write("</a>");
319         } else if (includeSummaryPerClassLink) {
320             write("<a href='?part=counterSummaryPerClass&amp;counter=");
321             write(counter.getName());
322             write("&amp;graph=");
323             write(requestId);
324             write("'>");
325             // writeDirectly pour ne pas gérer de traductions si le nom contient '#'
326             writeDirectly(htmlEncodeRequestName(requestId, requestName));
327             write("</a> ");
328         } else {
329             // writeDirectly pour ne pas gérer de traductions si le nom contient '#'
330             writeDirectly(htmlEncodeRequestName(requestId, requestName));
331         }
332     }
333
334     String getSlaHtmlClass(int mean) {
335         final String color;
336         if (mean < counterRequestAggregation.getWarningThreshold() || mean == 0) {
337             // si cette moyenne est < à la moyenne globale + 1 écart-type (paramétrable), c'est bien
338             // (si severeThreshold ou warningThreshold sont à 0 et mean à 0, c'est "info" et non "severe")
339             color = "info";
340         } else if (mean < counterRequestAggregation.getSevereThreshold()) {
341             // sinon, si cette moyenne est < à la moyenne globale + 2 écart-types (paramétrable),
342             // attention à cette requête qui est plus longue que les autres
343             color = "warning";
344         } else {
345             // sinon, (cette moyenne est > à la moyenne globale + 2 écart-types),
346             // cette requête est très longue par rapport aux autres ;
347             // il peut être opportun de l'optimiser si possible
348             color = "severe";
349         }
350         return color;
351     }
352
353     private void writePercentage(long dividende, long diviseur) throws IOException {
354         if (diviseur == 0) {
355             write("0");
356         } else {
357             write(integerFormat.format(100 * dividende / diviseur));
358         }
359     }
360
361     /**
362      * Encode le nom d'une requête pour affichage en html, sans encoder les espaces en nbsp (insécables),
363      * et highlight les mots clés SQL.
364      * @param requestId Id de la requête
365      * @param requestName Nom de la requête à encoder
366      * @return String
367      */

368     static String htmlEncodeRequestName(String requestId, String requestName) {
369         if (requestId.startsWith(Counter.SQL_COUNTER_NAME)) {
370             final String htmlEncoded = htmlEncodeButNotSpace(requestName);
371             // highlight SQL keywords
372             return SQL_KEYWORDS_PATTERN.matcher(htmlEncoded)
373                     .replaceAll("<span class='sqlKeyword'>$1</span>");
374         }
375
376         return htmlEncodeButNotSpace(requestName);
377     }
378 }
379