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}