001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.util; 020 021import org.HdrHistogram.Histogram; 022 023import java.io.*; 024import java.util.Map; 025import java.util.concurrent.ConcurrentHashMap; 026import java.util.stream.Collectors; 027 028/** HdrHistogram-backed metrics aggregator with on-demand histogram creation per name. */ 029public class Metrics implements Loggeable { 030 private Histogram template; 031 private Map<String,Histogram> metrics = new ConcurrentHashMap<>(); 032 private double conversion = 1; 033 034 /** Constructs a Metrics instance using the given Histogram as a template for new buckets. 035 * @param template the Histogram template; may be {@code null} 036 */ 037 public Metrics(Histogram template) { 038 super(); 039 this.template = template; 040 if (template != null && template.getStartTimeStamp() == Long.MAX_VALUE) { 041 template.setStartTimeStamp(System.currentTimeMillis()); 042 } 043 } 044 045 /** Returns a snapshot copy of all recorded histograms. 046 * @return map of metric name to histogram copy 047 */ 048 public Map<String, Histogram> metrics() { 049 return metrics.entrySet() 050 .stream() 051 .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().copy())); 052 } 053 054 /** Returns a snapshot copy of all histograms whose name starts with the given prefix. 055 * @param prefix the metric name prefix to filter by 056 * @return map of matching metric name to histogram copy 057 */ 058 public Map<String, Histogram> metrics(String prefix) { 059 return metrics.entrySet() 060 .stream() 061 .filter(e -> e.getKey().startsWith(prefix)) 062 .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().copy())); 063 } 064 065 /** Records an elapsed time observation for the named metric. 066 * @param name the metric name 067 * @param elapsed the elapsed value to record 068 */ 069 public void record(String name, long elapsed) { 070 Histogram h = getHistogram(name); 071 long l = Math.min(elapsed, h.getHighestTrackableValue()); 072 if (l > 0) 073 h.recordValue(l); 074 } 075 076 private Histogram getHistogram(String p) { 077 Histogram h = metrics.get(p); 078 if (h == null) { 079 Histogram hist = new Histogram(template); 080 hist.setTag(p); 081 metrics.putIfAbsent(p, hist); 082 h = metrics.get(p); 083 } 084 return h; 085 } 086 087 /** Dumps all metric percentile summaries to the given stream. 088 * @param ps the output stream 089 * @param indent indent prefix 090 */ 091 public void dump(PrintStream ps, String indent) { 092 metrics.entrySet() 093 .stream() 094 .sorted(Map.Entry.comparingByKey()) 095 .forEach(e -> dumpPercentiles(ps, indent, e.getKey(), e.getValue().copy())); 096 } 097 098 private void dumpPercentiles (PrintStream ps, String indent, String key, Histogram h) { 099 ps.printf("%s%s min=%.7f, max=%.7f, mean=%.7f stddev=%.7f P50=%.7f, P90=%.7f, P99=%.7f, P99.9=%.7f, P99.99=%.7f tot=%d size=%d%n", 100 indent, 101 key, 102 h.getMinValue()/conversion, 103 h.getMaxValue()/conversion, 104 h.getMean()/conversion, 105 h.getStdDeviation()/conversion, 106 h.getValueAtPercentile(50.0)/conversion, 107 h.getValueAtPercentile(90.0)/conversion, 108 h.getValueAtPercentile(99.0)/conversion, 109 h.getValueAtPercentile(99.9)/conversion, 110 h.getValueAtPercentile(99.99)/conversion, 111 h.getTotalCount(), 112 h.getEstimatedFootprintInBytes() 113 ); 114 } 115 116 private void dumpHistogram(File dir, String key, Histogram h) { 117 try (FileOutputStream fos = new FileOutputStream(new File(dir, key + ".hgrm"))) { 118 h.outputPercentileDistribution(new PrintStream(fos), 1.0); 119 } catch (IOException e) { 120 e.printStackTrace(); 121 } 122 } 123 124 /** Writes HDR histogram files for all metrics to the given directory. 125 * @param dir output directory 126 * @param prefix filename prefix for histogram files 127 */ 128 public void dumpHistograms(File dir, String prefix) { 129 metrics.entrySet() 130 .stream() 131 .sorted(Map.Entry.comparingByKey()) 132 .forEach(e -> dumpHistogram(dir, prefix + e.getKey(), e.getValue().copy())); 133 } 134 135 /** 136 * Sets the conversion divisor applied to percentile values during dump. 137 * @param conversion 138 * This is used to divide the percentile values while dumping. 139 * If you are using nano seconds to record and want to display the numbers in millis then conversion can be set to 1000000. 140 * By default conversion is set to 1. 141 */ 142 public void setConversion(double conversion) { 143 this.conversion = conversion; 144 } 145}