1
18 package net.bull.javamelody.internal.model;
19
20 import java.awt.Color;
21 import java.awt.Font;
22 import java.awt.GradientPaint;
23 import java.awt.Paint;
24 import java.io.File;
25 import java.io.FileNotFoundException;
26 import java.io.FilenameFilter;
27 import java.io.IOException;
28 import java.io.OutputStream;
29 import java.nio.charset.StandardCharsets;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Calendar;
33 import java.util.Collections;
34 import java.util.Comparator;
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Map;
39 import java.util.Timer;
40
41 import javax.imageio.ImageIO;
42
43 import org.jrobin.core.ConsolFuns;
44 import org.jrobin.core.FetchRequest;
45 import org.jrobin.core.RrdBackendFactory;
46 import org.jrobin.core.RrdDb;
47 import org.jrobin.core.RrdDbPool;
48 import org.jrobin.core.RrdDef;
49 import org.jrobin.core.RrdException;
50 import org.jrobin.core.Sample;
51 import org.jrobin.core.Util;
52 import org.jrobin.data.DataProcessor;
53 import org.jrobin.data.Plottable;
54 import org.jrobin.graph.RrdGraph;
55 import org.jrobin.graph.RrdGraphDef;
56
57 import net.bull.javamelody.Parameter;
58 import net.bull.javamelody.internal.common.I18N;
59 import net.bull.javamelody.internal.common.LOG;
60 import net.bull.javamelody.internal.common.Parameters;
61
62
69 public final class JRobin {
70 public static final int SMALL_HEIGHT = 50;
71 private static final Color PERCENTILE_COLOR = new Color(200, 50, 50);
72 private static final Color LIGHT_RED = Color.RED.brighter().brighter();
73 private static final Paint SMALL_GRADIENT = new GradientPaint(0, 0, LIGHT_RED, 0, SMALL_HEIGHT,
74 Color.GREEN, false);
75 private static final int HOUR = 60 * 60;
76 private static final int DAY = 24 * HOUR;
77 private static final int DEFAULT_OBSOLETE_GRAPHS_DAYS = 90;
78 private static final int DEFAULT_MAX_RRD_DISK_USAGE_MB = 20;
79
80
81 private final RrdDbPool rrdPool = getRrdDbPool();
82 private final String application;
83 private final String name;
84 private final String rrdFileName;
85 private final int step;
86 private final String requestName;
87
88 private static final class AppContextClassLoaderLeakPrevention {
89 private AppContextClassLoaderLeakPrevention() {
90 super();
91 }
92
93 static {
94
95
96 final ClassLoader loader = Thread.currentThread().getContextClassLoader();
97 try {
98
99 Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
100
101
102
103 ImageIO.getCacheDirectory();
104 } catch (final Throwable t) {
105 LOG.info("prevention of AppContext ClassLoader leak failed, skipping");
106 } finally {
107 Thread.currentThread().setContextClassLoader(loader);
108 }
109 }
110
111 static void dummy() {
112
113 }
114 }
115
116 private JRobin(String application, String name, File rrdFile, int step, String requestName)
117 throws RrdException, IOException {
118 super();
119 assert application != null;
120 assert name != null;
121 assert rrdFile != null;
122 assert step > 0;
123
124
125 this.application = application;
126 this.name = name;
127 this.rrdFileName = rrdFile.getPath();
128 this.step = step;
129 this.requestName = requestName;
130
131 init();
132 }
133
134 public static void stop() {
135 if (RrdNioBackend.getFileSyncTimer() != null) {
136 RrdNioBackend.getFileSyncTimer().cancel();
137 }
138 }
139
140
146 public static void initBackendFactory(Timer timer) throws IOException {
147 RrdNioBackend.setFileSyncTimer(timer);
148
149 try {
150 if (!RrdBackendFactory.getDefaultFactory().getFactoryName()
151 .equals(RrdNioBackendFactory.FACTORY_NAME)) {
152 RrdBackendFactory.registerAndSetAsDefaultFactory(new RrdNioBackendFactory());
153 }
154 } catch (final RrdException e) {
155 throw createIOException(e);
156 }
157 }
158
159 static JRobin createInstance(String application, String name, String requestName)
160 throws IOException {
161 final File rrdFile = getRrdFile(application, name);
162 final int step = Parameters.getResolutionSeconds();
163 try {
164 return new JRobin(application, name, rrdFile, step, requestName);
165 } catch (final RrdException e) {
166 throw createIOException(e);
167 }
168 }
169
170 static JRobin createInstanceIfFileExists(String application, String name, String requestName)
171 throws IOException {
172 final File rrdFile = getRrdFile(application, name);
173 if (rrdFile.exists()) {
174 final int step = Parameters.getResolutionSeconds();
175 try {
176 return new JRobin(application, name, rrdFile, step, requestName);
177 } catch (final RrdException e) {
178 throw createIOException(e);
179 }
180 }
181 return null;
182 }
183
184 private static File getRrdFile(String application, String name) {
185 final File dir = Parameters.getStorageDirectory(application);
186 return new File(dir, name + ".rrd");
187 }
188
189 private void init() throws IOException, RrdException {
190 final File rrdFile = new File(rrdFileName);
191 final File rrdDirectory = rrdFile.getParentFile();
192 if (!rrdDirectory.mkdirs() && !rrdDirectory.exists()) {
193 throw new IOException(
194 "JavaMelody directory can't be created: " + rrdDirectory.getPath());
195 }
196
197
198 if (!rrdFile.exists() || rrdFile.length() == 0) {
199
200 final RrdDef rrdDef = new RrdDef(rrdFileName, step);
201
202
203
204 rrdDef.setStartTime(Util.getTime() - step);
205
206 final String dsType = "GAUGE";
207
208 final int heartbeat = step * 2;
209 rrdDef.addDatasource(getDataSourceName(), dsType, heartbeat, 0, Double.NaN);
210
211 final String average = ConsolFuns.CF_AVERAGE;
212 final String max = ConsolFuns.CF_MAX;
213
214 rrdDef.addArchive(average, 0.25, 1, DAY / step);
215 rrdDef.addArchive(max, 0.25, 1, DAY / step);
216
217 rrdDef.addArchive(average, 0.25, HOUR / step, 7 * 24);
218 rrdDef.addArchive(max, 0.25, HOUR / step, 7 * 24);
219
220 rrdDef.addArchive(average, 0.25, 6 * HOUR / step, 31 * 4);
221 rrdDef.addArchive(max, 0.25, 6 * HOUR / step, 31 * 4);
222
223 rrdDef.addArchive(average, 0.25, 8 * 6 * HOUR / step, 2 * 12 * 15);
224 rrdDef.addArchive(max, 0.25, 8 * 6 * HOUR / step, 2 * 12 * 15);
225
226 final RrdDb rrdDb = rrdPool.requestRrdDb(rrdDef);
227 rrdPool.release(rrdDb);
228 }
229 }
230
231 private void resetFile() throws IOException {
232 deleteFile();
233 try {
234 init();
235 } catch (final RrdException e) {
236 throw createIOException(e);
237 }
238 }
239
240 public byte[] graph(Range range, int width, int height) throws IOException {
241 return graph(range, width, height, false);
242 }
243
244 public byte[] graph(Range range, int width, int height, boolean maxHidden) throws IOException {
245
246 AppContextClassLoaderLeakPrevention.dummy();
247
248 try {
249
250
251
252
253 final RrdGraphDef graphDef = new RrdGraphDef();
254 if (Locale.CHINESE.getLanguage()
255 .equals(I18N.getResourceBundle().getLocale().getLanguage())) {
256 graphDef.setSmallFont(new Font(Font.MONOSPACED, Font.PLAIN, 10));
257 graphDef.setLargeFont(new Font(Font.MONOSPACED, Font.BOLD, 12));
258 }
259
260 initGraphSource(graphDef, height, maxHidden, range);
261
262 initGraphPeriodAndSize(range, width, height, graphDef);
263
264 graphDef.setImageFormat("png");
265 graphDef.setFilename("-");
266
267
268
269 graphDef.setPoolUsed(true);
270 return new RrdGraph(graphDef).getRrdGraphInfo().getBytes();
271 } catch (final RrdException e) {
272 throw createIOException(e);
273 }
274 }
275
276 private void initGraphPeriodAndSize(Range range, int width, int height, RrdGraphDef graphDef) {
277
278
279 final long endTime = range.getJRobinEndTime();
280 final long startTime = range.getJRobinStartTime();
281 final String label = getLabel();
282 final String titleStart;
283 if (label.length() > 31 && width <= 200) {
284
285 titleStart = label;
286 } else {
287 titleStart = label + " - " + range.getLabel();
288 }
289 final String titleEnd;
290 if (width > 400) {
291 if (range.getPeriod() == null) {
292 titleEnd = " - " + I18N.getFormattedString("sur", getApplication());
293 } else {
294 titleEnd = " - " + I18N.getCurrentDate() + ' '
295 + I18N.getFormattedString("sur", getApplication());
296 }
297 } else {
298 titleEnd = "";
299 if (range.getPeriod() == null) {
300
301
302 graphDef.setLargeFont(graphDef.getLargeFont()
303 .deriveFont(graphDef.getLargeFont().getSize2D() - 2f));
304 }
305 }
306 graphDef.setStartTime(startTime);
307 graphDef.setEndTime(endTime);
308 graphDef.setTitle(titleStart + titleEnd);
309 graphDef.setFirstDayOfWeek(
310 Calendar.getInstance(I18N.getCurrentLocale()).getFirstDayOfWeek());
311
312
313
314
315
316
317 graphDef.setWidth(width);
318 graphDef.setHeight(height);
319 if (width <= 100) {
320 graphDef.setNoLegend(true);
321 graphDef.setUnitsLength(0);
322 graphDef.setShowSignature(false);
323 graphDef.setTitle(null);
324 }
325
326
327
328 }
329
330 private void initGraphSource(RrdGraphDef graphDef, int height, boolean maxHidden, Range range)
331 throws IOException {
332 final String dataSourceName = getDataSourceName();
333 final String average = "average";
334 graphDef.datasource(average, rrdFileName, dataSourceName, ConsolFuns.CF_AVERAGE);
335 graphDef.setMinValue(0);
336 final String moyenneLabel = I18N.getString("Moyenne");
337 graphDef.area(average, getPaint(height), moyenneLabel);
338 graphDef.gprint(average, ConsolFuns.CF_AVERAGE, moyenneLabel + ": %9.0f %S\\r");
339
340 if (!maxHidden) {
341 final String max = "max";
342 graphDef.datasource(max, rrdFileName, dataSourceName, ConsolFuns.CF_MAX);
343 final String maximumLabel = I18N.getString("Maximum");
344 graphDef.line(max, Color.BLUE, maximumLabel);
345 graphDef.gprint(max, ConsolFuns.CF_MAX, maximumLabel + ": %9.0f %S\\r");
346 }
347 if (height > 200) {
348 final double percentileValue = get95PercentileValue(range);
349 final Plottable constantDataSource = new Plottable() {
350 @Override
351 public double getValue(long timestamp) {
352 return percentileValue;
353 }
354 };
355 final String percentile = "percentile";
356 graphDef.datasource(percentile, constantDataSource);
357 final String percentileLabel = I18N.getString("95percentile");
358 graphDef.line(percentile, PERCENTILE_COLOR, percentileLabel);
359 graphDef.gprint(percentile, ConsolFuns.CF_MAX, ":%9.0f %S\\r");
360 }
361
362 }
363
364 private static Paint getPaint(int height) {
365
366
367
368 if (height == SMALL_HEIGHT) {
369
370
371 return SMALL_GRADIENT;
372 }
373 return new GradientPaint(0, 0, LIGHT_RED, 0, height, Color.GREEN, false);
374 }
375
376 void addValue(double value) throws IOException {
377 try {
378
379 final RrdDb rrdDb = rrdPool.requestRrdDb(rrdFileName);
380 synchronized (rrdDb) {
381 try {
382
383 final Sample sample = rrdDb.createSample();
384
385
386
387 if (sample.getTime() > rrdDb.getLastUpdateTime()) {
388
389 sample.setValue(getDataSourceName(), value);
390
391 sample.update();
392 }
393 } finally {
394
395 rrdPool.release(rrdDb);
396 }
397 }
398 } catch (final FileNotFoundException e) {
399 if (e.getMessage() != null && e.getMessage().endsWith("[non existent]")) {
400
401 LOG.debug("A JRobin file was deleted and created again: "
402 + new File(rrdFileName).getPath());
403 resetFile();
404 addValue(value);
405 }
406 } catch (final RrdException e) {
407 if (e.getMessage() != null && e.getMessage().startsWith("Invalid file header")) {
408
409
410
411 LOG.debug("A JRobin file was found corrupted and was reset: "
412 + new File(rrdFileName).getPath());
413 resetFile();
414 addValue(value);
415 }
416 throw createIOException(e);
417 } catch (final IllegalArgumentException | ArithmeticException e) {
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438 LOG.debug("A JRobin file was found corrupted and was reset: "
439 + new File(rrdFileName).getPath());
440 resetFile();
441 addValue(value);
442 throw createIOException(e);
443 }
444 }
445
446 public double getLastValue() throws IOException {
447 try {
448
449 final RrdDb rrdDb = rrdPool.requestRrdDb(rrdFileName);
450 try {
451 return rrdDb.getLastDatasourceValue(getDataSourceName());
452 } finally {
453
454 rrdPool.release(rrdDb);
455 }
456 } catch (final RrdException e) {
457 throw createIOException(e);
458 }
459 }
460
461 public void dumpXml(OutputStream output, Range range) throws IOException {
462 try {
463
464 final RrdDb rrdDb = rrdPool.requestRrdDb(rrdFileName);
465 try {
466 if (range.getPeriod() == Period.TOUT) {
467 rrdDb.exportXml(output);
468 } else {
469 final FetchRequest fetchRequest = rrdDb.createFetchRequest(
470 ConsolFuns.CF_AVERAGE, range.getJRobinStartTime(),
471 range.getJRobinEndTime());
472 final String xml = fetchRequest.fetchData().exportXml()
473 .replaceFirst("<file>.*</file>", "");
474 output.write(xml.getBytes(StandardCharsets.UTF_8));
475 }
476 } finally {
477
478 rrdPool.release(rrdDb);
479 }
480 } catch (final RrdException e) {
481 throw createIOException(e);
482 }
483 }
484
485 public String dumpTxt(Range range) throws IOException {
486 try {
487
488 final RrdDb rrdDb = rrdPool.requestRrdDb(rrdFileName);
489 try {
490 if (range.getPeriod() == Period.TOUT) {
491 return rrdDb.dump();
492 }
493 final FetchRequest fetchRequest = rrdDb.createFetchRequest(ConsolFuns.CF_AVERAGE,
494 range.getJRobinStartTime(), range.getJRobinEndTime());
495 return fetchRequest.fetchData().dump();
496 } finally {
497
498 rrdPool.release(rrdDb);
499 }
500 } catch (final RrdException e) {
501 throw createIOException(e);
502 }
503 }
504
505 double getMeanValue(Range range) throws IOException {
506 assert range.getPeriod() == null;
507 try {
508 final DataProcessor dproc = processData(range);
509 return dproc.getAggregate("average", ConsolFuns.CF_AVERAGE);
510 } catch (final RrdException e) {
511 throw createIOException(e);
512 }
513 }
514
515 private double get95PercentileValue(Range range) throws IOException {
516 try {
517 final DataProcessor dproc = processData(range);
518
519 return dproc.get95Percentile("average");
520 } catch (final RrdException e) {
521 throw createIOException(e);
522 }
523 }
524
525 private DataProcessor processData(Range range) throws IOException, RrdException {
526 final String dataSourceName = getDataSourceName();
527 final long endTime = range.getJRobinEndTime();
528 final long startTime = range.getJRobinStartTime();
529 final DataProcessor dproc = new DataProcessor(startTime, endTime);
530 dproc.addDatasource("average", rrdFileName, dataSourceName, ConsolFuns.CF_AVERAGE);
531 dproc.setPoolUsed(true);
532 dproc.processData();
533 return dproc;
534 }
535
536 boolean deleteFile() {
537 return new File(rrdFileName).delete();
538 }
539
540 private String getApplication() {
541 return application;
542 }
543
544 public String getName() {
545 return name;
546 }
547
548 private String getDataSourceName() {
549
550 return name.substring(0, Math.min(20, name.length()));
551 }
552
553 public String getLabel() {
554 if (requestName == null) {
555
556 return I18N.getString(getName());
557 }
558
559 final String shortRequestName = requestName.substring(0,
560 Math.min(30, requestName.length()));
561
562
563
564 return I18N.getFormattedString("Temps_moyens_de", shortRequestName);
565 }
566
567 private static IOException createIOException(Exception e) {
568
569 return new IOException(e.getMessage(), e);
570 }
571
572 static long deleteObsoleteJRobinFiles(String application) {
573 final Calendar nowMinusThreeMonthsAndADay = Calendar.getInstance();
574 nowMinusThreeMonthsAndADay.add(Calendar.DAY_OF_YEAR, -getObsoleteGraphsDays());
575 nowMinusThreeMonthsAndADay.add(Calendar.DAY_OF_YEAR, -1);
576 final long timestamp = Util.getTimestamp(nowMinusThreeMonthsAndADay);
577 final int counterRequestIdLength = new CounterRequest("", "").getId().length();
578 long diskUsage = 0;
579 final Map<String, Long> lastUpdateTimesByPath = new HashMap<>();
580 final List<File> rrdFiles = new ArrayList<>(listRrdFiles(application));
581 for (final File file : rrdFiles) {
582
583 if (file.getName().length() > counterRequestIdLength
584 && file.lastModified() < nowMinusThreeMonthsAndADay.getTimeInMillis()) {
585 final long lastUpdateTime = getLastUpdateTime(file);
586 lastUpdateTimesByPath.put(file.getPath(), lastUpdateTime);
587 final boolean obsolete = lastUpdateTime < timestamp;
588 boolean deleted = false;
589 if (obsolete) {
590 deleted = file.delete();
591 }
592 if (!deleted) {
593 diskUsage += file.length();
594 }
595 } else {
596 diskUsage += file.length();
597 }
598 }
599 final long maxRrdDiskUsage = getMaxRrdDiskUsageMb() * 1024L * 1024L;
600 if (diskUsage > maxRrdDiskUsage) {
601
602 for (final File file : rrdFiles) {
603 if (lastUpdateTimesByPath.get(file.getPath()) == null) {
604 lastUpdateTimesByPath.put(file.getPath(), getLastUpdateTime(file));
605 }
606 }
607 final Comparator<File> comparatorByLastUpdateTime = new Comparator<File>() {
608 @Override
609 public int compare(File o1, File o2) {
610 return lastUpdateTimesByPath.get(o1.getPath())
611 .compareTo(lastUpdateTimesByPath.get(o2.getPath()));
612 }
613 };
614 Collections.sort(rrdFiles, comparatorByLastUpdateTime);
615
616 for (final File file : rrdFiles) {
617 if (diskUsage < maxRrdDiskUsage) {
618 break;
619 }
620 if (file.getName().length() > counterRequestIdLength) {
621 final long length = file.length();
622 if (file.delete()) {
623 diskUsage -= length;
624 }
625 }
626 }
627 }
628
629 return diskUsage;
630 }
631
632 private static long getLastUpdateTime(File file) {
633 try {
634 final RrdDbPool rrdPool = getRrdDbPool();
635 final RrdDb rrdDb = rrdPool.requestRrdDb(file.getPath());
636 final long lastUpdateTime = rrdDb.getLastUpdateTime();
637 rrdPool.release(rrdDb);
638 return lastUpdateTime;
639 } catch (final IOException | RrdException e) {
640 return file.lastModified() / 1000L;
641 }
642 }
643
644 private static long getMaxRrdDiskUsageMb() {
645 final String param = Parameters.getParameterValue(Parameter.MAX_RRD_DISK_USAGE_MB);
646 if (param != null) {
647
648 final int result = Integer.parseInt(param);
649 if (result <= 0) {
650 throw new IllegalStateException(
651 "The parameter max-rrd-disk-usage-mb should be > 0 (20 recommended)");
652 }
653 return result;
654 }
655 return DEFAULT_MAX_RRD_DISK_USAGE_MB;
656 }
657
658
662 private static int getObsoleteGraphsDays() {
663 final String param = Parameter.OBSOLETE_GRAPHS_DAYS.getValue();
664 if (param != null) {
665
666 final int result = Integer.parseInt(param);
667 if (result <= 0) {
668 throw new IllegalStateException(
669 "The parameter obsolete-graphs-days should be > 0 (90 recommended)");
670 }
671 return result;
672 }
673 return DEFAULT_OBSOLETE_GRAPHS_DAYS;
674 }
675
676 private static List<File> listRrdFiles(String application) {
677 final File storageDir = Parameters.getStorageDirectory(application);
678
679 final FilenameFilter filenameFilter = new FilenameFilter() {
680
681 @Override
682 public boolean accept(File dir, String fileName) {
683 return fileName.endsWith(".rrd");
684 }
685 };
686 final File[] files = storageDir.listFiles(filenameFilter);
687 if (files == null) {
688 return Collections.emptyList();
689 }
690 return Arrays.asList(files);
691 }
692
693 private static RrdDbPool getRrdDbPool() throws IOException {
694 try {
695 return RrdDbPool.getInstance();
696 } catch (final RrdException e) {
697 throw createIOException(e);
698 }
699 }
700
701 @Override
702 public String toString() {
703 return getClass().getSimpleName() + "[application=" + getApplication() + ", name="
704 + getName() + ']';
705 }
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731 }
732