All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fish.payara.microprofile.metrics.writer.OpenMetricsExporter Maven / Gradle / Ivy

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2020 Payara Foundation and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://github.com/payara/Payara/blob/master/LICENSE.txt
 * See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * The Payara Foundation designates this particular file as subject to the "Classpath"
 * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package fish.payara.microprofile.metrics.writer;

import static fish.payara.microprofile.metrics.MetricUnitsUtils.scaleToBaseUnit;

import java.io.PrintWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
import java.util.logging.Level;

import org.eclipse.microprofile.metrics.ConcurrentGauge;
import org.eclipse.microprofile.metrics.Counter;
import org.eclipse.microprofile.metrics.Gauge;
import org.eclipse.microprofile.metrics.Histogram;
import org.eclipse.microprofile.metrics.Metadata;
import org.eclipse.microprofile.metrics.Meter;
import org.eclipse.microprofile.metrics.Metered;
import org.eclipse.microprofile.metrics.Metric;
import org.eclipse.microprofile.metrics.MetricID;
import org.eclipse.microprofile.metrics.MetricUnits;
import org.eclipse.microprofile.metrics.Sampling;
import org.eclipse.microprofile.metrics.SimpleTimer;
import org.eclipse.microprofile.metrics.Snapshot;
import org.eclipse.microprofile.metrics.Tag;
import org.eclipse.microprofile.metrics.Timer;

import org.eclipse.microprofile.metrics.MetricRegistry.Type;

/**
 * Writes {@link Metric}s according to the OpenMetrics standard as defined in microprofile-metrics-spec-2.3.pdf.
 *
 * The append method code is organised so that its output is reflected in the use of
 * {@link #appendHELP(String, Metadata)}, {@link #appendTYPE(String, OpenMetricsType)} and
 * {@link #appendValue(String, Tag[], Number)} which each emit a single output line.
 *
 * @author Jan Bernitt
 * @since 5.202
 */
public class OpenMetricsExporter implements MetricExporter {

    protected enum OpenMetricsType {
        counter, gauge, summary
    }

    protected final Type scope;
    protected final PrintWriter out;
    protected final Set typeWrittenByGlobalName;
    protected final Set helpWrittenByGlobalName;

    public OpenMetricsExporter(Writer out) {
        this(null, out instanceof PrintWriter ? (PrintWriter) out : new PrintWriter(out), new HashSet<>(), new HashSet<>());
    }

    protected OpenMetricsExporter(Type scope, PrintWriter out, Set typeWrittenByGlobalName,
            Set helpWrittenByGlobalName) {
        this.scope = scope;
        this.out = out;
        this.typeWrittenByGlobalName = typeWrittenByGlobalName;
        this.helpWrittenByGlobalName = helpWrittenByGlobalName;
    }

    @Override
    public MetricExporter in(Type scope, boolean asNode) {
        return new OpenMetricsExporter(scope, out, typeWrittenByGlobalName, helpWrittenByGlobalName);
    }

    @Override
    public void exportComplete() {
        // noop
    }

    @Override
    public void export(MetricID metricID, Counter counter, Metadata metadata) {
        String total = globalName(metricID, "_total");
        appendTYPE(total, OpenMetricsType.counter);
        appendHELP(total, metadata);
        appendValue(total, metricID.getTagsAsArray(), counter.getCount());
    }

    @Override
    public void export(MetricID metricID, ConcurrentGauge gauge, Metadata metadata) {
        Tag[] tags = metricID.getTagsAsArray();
        String current = globalName(metricID, "_current");
        appendTYPE(current, OpenMetricsType.gauge);
        appendHELP(current, metadata);
        appendValue(current, tags, gauge.getCount());
        String min = globalName(metricID, "_min");
        appendTYPE(min, OpenMetricsType.gauge);
        appendValue(min, tags, gauge.getMin());
        String max = globalName(metricID, "_max");
        appendTYPE(max, OpenMetricsType.gauge);
        appendValue(max, tags, gauge.getMax());
    }

    @Override
    public void export(MetricID metricID, Gauge gauge, Metadata metadata) {
        Object value = null;
        try {
            value = gauge.getValue();
        } catch (IllegalStateException ex) {
            // The forwarding gauge is unloaded
            return;
        }
        if (!(value instanceof Number)) {
            LOGGER.log(Level.FINER, "Skipping OpenMetrics output for Gauge: {0} of type {1}",
                    new Object[] { metricID, value.getClass() });
            return;
        }
        String valueName = globalName(metricID, metadata);
        appendTYPE(valueName, OpenMetricsType.gauge);
        appendHELP(valueName, metadata);
        appendValue(valueName, metricID.getTagsAsArray(), scaleToBaseUnit((Number) value, metadata));
    }

    @Override
    public void export(MetricID metricID, Histogram histogram, Metadata metadata) {
        exportSampling(metricID, histogram, histogram::getCount, histogram::getSum, metadata);
    }

    private void exportSampling(MetricID metricID, Sampling sampling, LongSupplier count, Supplier sum, Metadata metadata) {
        Tag[] tags = metricID.getTagsAsArray();
        Snapshot snapshot = sampling.getSnapshot();
        String mean = globalName(metricID, "_mean", metadata);
        appendTYPE(mean, OpenMetricsType.gauge);
        appendValue(mean, tags, scaleToBaseUnit(snapshot.getMean(), metadata));
        String max = globalName(metricID, "_max", metadata);
        appendTYPE(max, OpenMetricsType.gauge);
        appendValue(max, tags, scaleToBaseUnit(snapshot.getMax(), metadata));
        String min = globalName(metricID, "_min", metadata);
        appendTYPE(min, OpenMetricsType.gauge);
        appendValue(min, tags, scaleToBaseUnit(snapshot.getMin(), metadata));
        String stddev = globalName(metricID, "_stddev", metadata);
        appendTYPE(stddev, OpenMetricsType.gauge);
        appendValue(stddev, tags, scaleToBaseUnit(snapshot.getStdDev(), metadata));
        String summary = globalName(metricID, metadata);
        appendTYPE(summary, OpenMetricsType.summary);
        appendHELP(summary, metadata);
        appendValue(globalName(metricID, metadata, "_count"), tags, count.getAsLong());
        appendValue(globalName(metricID, metadata, "_sum"), tags, sum.get());
        appendValue(summary, tags("quantile", "0.5", tags), scaleToBaseUnit(snapshot.getMedian(), metadata));
        appendValue(summary, tags("quantile", "0.75", tags), scaleToBaseUnit(snapshot.get75thPercentile(), metadata));
        appendValue(summary, tags("quantile", "0.95", tags), scaleToBaseUnit(snapshot.get95thPercentile(), metadata));
        appendValue(summary, tags("quantile", "0.98", tags), scaleToBaseUnit(snapshot.get98thPercentile(), metadata));
        appendValue(summary, tags("quantile", "0.99", tags), scaleToBaseUnit(snapshot.get99thPercentile(), metadata));
        appendValue(summary, tags("quantile", "0.999", tags), scaleToBaseUnit(snapshot.get999thPercentile(), metadata));
    }

    @Override
    public void export(MetricID metricID, Meter meter, Metadata metadata) {
        Tag[] tags = metricID.getTagsAsArray();
        String total = globalName(metricID, "_total");
        appendTYPE(total, OpenMetricsType.counter);
        appendHELP(total, metadata);
        appendValue(total, tags, meter.getCount());
        exportMetered(metricID, meter);
    }

    private void exportMetered(MetricID metricID, Metered metered) {
        Tag[] tags = metricID.getTagsAsArray();
        String rate = globalName(metricID, "_rate_per_second");
        appendTYPE(rate, OpenMetricsType.gauge);
        appendValue(rate, tags, metered.getMeanRate());
        String oneMinRate = globalName(metricID, "_one_min_rate_per_second");
        appendTYPE(oneMinRate, OpenMetricsType.gauge);
        appendValue(oneMinRate, tags, metered.getOneMinuteRate());
        String fiveMinRate = globalName(metricID, "_five_min_rate_per_second");
        appendTYPE(fiveMinRate, OpenMetricsType.gauge);
        appendValue(fiveMinRate, tags, metered.getFiveMinuteRate());
        String fifteenMinRate = globalName(metricID, "_fifteen_min_rate_per_second");
        appendTYPE(fifteenMinRate, OpenMetricsType.gauge);
        appendValue(fifteenMinRate, tags, metered.getFifteenMinuteRate());
    }

    @Override
    public void export(MetricID metricID, SimpleTimer timer, Metadata metadata) {
        Tag[] tags = metricID.getTagsAsArray();
        String total = globalName(metricID, "_total");
        appendTYPE(total, OpenMetricsType.counter);
        appendHELP(total, metadata);
        appendValue(total, tags, timer.getCount());
        String elapsedTime = globalName(metricID, "_elapsedTime_seconds");
        appendTYPE(elapsedTime, OpenMetricsType.gauge);
        appendValue(elapsedTime, tags, toSeconds(timer.getElapsedTime()));
        String maxTime = globalName(metricID, "_maxTimeDuration_seconds");
        appendTYPE(maxTime, OpenMetricsType.gauge);
        appendValue(maxTime, tags, toSeconds(timer.getMaxTimeDuration()));
        String minTime = globalName(metricID, "_minTimeDuration_seconds");
        appendTYPE(minTime, OpenMetricsType.gauge);
        appendValue(minTime, tags, toSeconds(timer.getMinTimeDuration()));
    }

    @Override
    public void export(MetricID metricID, Timer timer, Metadata metadata) {
        exportMetered(metricID, timer);
        exportSampling(metricID, timer, timer::getCount, () -> toSeconds(timer.getElapsedTime()), metadata);
    }

    protected void appendTYPE(String globalName, OpenMetricsType type) {
        if (typeWrittenByGlobalName.contains(globalName)) {
            return;
        }
        typeWrittenByGlobalName.add(globalName);
        out.append("# TYPE ").append(globalName).append(' ').append(type.name()).append('\n');
    }

    protected void appendHELP(String globalName, Metadata metadata) {
        if (helpWrittenByGlobalName.contains(globalName)) {
            return;
        }
        helpWrittenByGlobalName.add(globalName);
        Optional description = metadata.description();
        if (!description.isPresent()) {
            return;
        }
        String text = description.get();
        if (text.isEmpty()) {
            return;
        }
        out.append("# HELP ").append(globalName).append(' ').append(text).append('\n');
    }

    protected void appendValue(String globalName, Tag[] tags, Number value) {
        out.append(globalName);
        out.append(tagsToString(tags));
        out.append(' ').append(value == null ? "NaN" : roundValue(value)).append('\n');
    }

    private void appendValue(String globalName, Tag[] tags, long value) {
        appendValue(globalName, tags, Long.valueOf(value));
    }

    private void appendValue(String globalName, Tag[] tags, double value) {
        appendValue(globalName, tags, Double.valueOf(value));
    }

    protected String roundValue(Number value) {
        String valString = value.toString();
        if (valString.endsWith(".0")) {
            valString = valString.substring(0, valString.length() - 2); // avoid decimal NNN.0 => NNN
        }
        if (valString.endsWith("000000001")) {
            valString = valString.substring(0, valString.length() - 9); // cut off double representation error
        }
        if (valString.contains("000000001E")) {
            valString = valString.replace("000000001E", "E"); // cut off double representation error for exponential form
        }
        return valString;
    }

    protected static String tagsToString(Tag[] tags) {
        if (tags.length == 0) {
            return "";
        }
        String result = "";
        result += "{";
        for (int i = 0; i < tags.length; i++) {
            if (i > 0) {
                result += ",";
            }
            result += sanitizeMetricName(tags[i].getTagName())
                    + "=\""
                    + escapeTagValue(tags[i].getTagValue())
                    + '"';
        }
        result += "}";
        return result;
    }

    private String globalName(MetricID metricID, Metadata unit) {
        return globalName(metricID, "", unit, "");
    }

    private String globalName(MetricID metricID, String infix, Metadata unit) {
        return globalName(metricID, infix, unit, "");
    }

    private String globalName(MetricID metricID, Metadata unit, String suffix) {
            return globalName(metricID, "", unit, suffix);
    }

    private String globalName(MetricID metricID, String infix, Metadata metadata, String suffix) {
        if (!metadata.unit().isPresent()) {
            return globalName(metricID, infix + suffix);
        }
        String unit = metadata.getUnit();
        switch (unit) {
        case MetricUnits.NANOSECONDS:
        case MetricUnits.MICROSECONDS:
        case MetricUnits.MILLISECONDS:
        case MetricUnits.SECONDS:
        case MetricUnits.MINUTES:
        case MetricUnits.HOURS:
        case MetricUnits.DAYS:
            return globalName(metricID, infix + "_seconds" + suffix);
        case MetricUnits.BITS:
        case MetricUnits.BYTES:
        case MetricUnits.KILOBITS:
        case MetricUnits.MEGABITS:
        case MetricUnits.GIGABITS:
        case MetricUnits.KIBIBITS:
        case MetricUnits.MEBIBITS:
        case MetricUnits.GIBIBITS:
        case MetricUnits.KILOBYTES:
        case MetricUnits.MEGABYTES:
        case MetricUnits.GIGABYTES:
            return globalName(metricID, infix + "_bytes" + suffix);
        case MetricUnits.PERCENT:
            return globalName(metricID, infix + "_ratio" + suffix);
        case MetricUnits.PER_SECOND:
            return globalName(metricID, infix + "_per_second" + suffix);
        case MetricUnits.NONE:
            return globalName(metricID, infix + suffix);
        default:
            return globalName(metricID, infix + "_" + unit + suffix);
        }
    }

    private String globalName(MetricID metricID, String suffix) {
        String name = metricID.getName();
        return sanitizeMetricName(!suffix.isEmpty() && name.endsWith(suffix)
                ? scope.getName() + '_' + name
                : scope.getName() + '_' + name + suffix);
    }

    private static CharSequence escapeTagValue(String name) {
        StringBuilder str = new StringBuilder(name.length());
        for (int i = 0; i < name.length(); i++) {
            char c = name.charAt(i);
            if (c == '\n') {
                str.append("\\n");
            } else {
                if (c == '\\' || c == '"') {
                    str.append('\\');
                }
                str.append(c);
            }
        }
        return str;
    }

    public static String sanitizeMetricName(String name) {
        //Translation rules :
        //All characters not in the range a-z A-Z or 0-9 are translated to underscore (_)
        //Double underscore is translated to single underscore
        String out = name.replaceAll("[^a-zA-Z0-9_]+", "_");
        //Colon-underscore (:_) is translated to single colon
        return out.replaceAll(":_", ":");
    }

    private static Tag[] tags(String name, String value, Tag[] rest) {
        Tag tag = new Tag(name, value);
        if (rest.length == 0) {
            return new Tag[] { tag };
        }
        Tag[] res = Arrays.copyOf(rest, rest.length + 1);
        res[rest.length] = tag;
        return res;
    }

    private static final BigDecimal NANOS_IN_SECOND = BigDecimal.valueOf(1000000000L);

    private static Number toSeconds(Duration d) {
        if (d == null) {
            return null;
        }
        if (d.getNano() == 0) {
            return d.getSeconds() / 1000d;
        }
        BigDecimal nanos = BigDecimal.valueOf(d.getSeconds()).multiply(NANOS_IN_SECOND).add(BigDecimal.valueOf(d.getNano()));
        return nanos.divide(NANOS_IN_SECOND).doubleValue();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy