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

fish.payara.microprofile.metrics.writer.JsonExporter 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 java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonMap;

import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;

import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonWriter;
import javax.json.stream.JsonGenerator;

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.MetricRegistry;
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 MicroPrfile Metrics 2.3 standard for JSON format as defined in microprofile-metrics-spec-2.3.pdf.
 *
 * @author Jan Bernitt
 * @since 5.202
 */
public class JsonExporter implements MetricExporter {

    public enum Mode { GET, OPTIONS }

    private final MetricRegistry.Type scope;
    private final JsonWriter out;
    private final Mode mode;
    private final JsonObjectBuilder documentObj;
    private final JsonObjectBuilder scopeObj;
    private JsonObjectBuilder groupObj;
    private JsonArrayBuilder tagsArray;
    private MetricID exportedBefore;
    private Metadata exportedBeforeMetadata;

    public JsonExporter(Writer out, Mode mode, boolean prettyPrint) {
        this(null, writer(out, prettyPrint), mode, Json.createObjectBuilder(), null);
    }

    private static JsonWriter writer(Writer out, boolean prettyPrint) {
        return Json.createWriterFactory(singletonMap(JsonGenerator.PRETTY_PRINTING, prettyPrint)).createWriter(out);
    }

    private JsonExporter(MetricRegistry.Type scope, JsonWriter out, Mode mode, JsonObjectBuilder documentObj,
            JsonObjectBuilder scopeObj) {
        this.scope = scope;
        this.out = out;
        this.mode = mode;
        this.documentObj = documentObj;
        this.scopeObj = scopeObj;
    }

    @Override
    public MetricExporter in(Type scope, boolean asNode) {
        completeScope();
        return new JsonExporter(scope, out, mode, documentObj, asNode ? Json.createObjectBuilder() : null);
    }

    @Override
    public void export(MetricID metricID, Counter counter, Metadata metadata) {
        completeGroup(metricID, metadata);
        appendMember(metricID, counter.getCount());
    }

    @Override
    public void export(MetricID metricID, ConcurrentGauge gauge, Metadata metadata) {
        completeOrUpdateGroup(metricID, metadata);
        appendMember(metricID, "current", gauge.getCount());
        appendMember(metricID, "min", gauge.getMin());
        appendMember(metricID, "max", gauge.getMax());
    }

    @Override
    public void export(MetricID metricID, Gauge gauge, Metadata metadata) {
        completeGroup(metricID, metadata);
        if (mode == Mode.OPTIONS) {
            return;
        }
        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;
        }
        appendMember(metricID, (Number) value);
    }

    @Override
    public void export(MetricID metricID, Histogram histogram, Metadata metadata) {
        completeOrUpdateGroup(metricID, metadata);
        appendMember(metricID, "count", histogram.getCount());
        appendMember(metricID, "sum", histogram.getSum());
        exportSampling(metricID, histogram);
    }

    private void exportSampling(MetricID metricID, Sampling sampling) {
        Snapshot snapshot = sampling.getSnapshot();
        appendMember(metricID, "min", snapshot.getMin());
        appendMember(metricID, "max", snapshot.getMax());
        appendMember(metricID, "mean", snapshot.getMean());
        appendMember(metricID, "stddev", snapshot.getStdDev());
        appendMember(metricID, "p50", snapshot.getMedian());
        appendMember(metricID, "p75", snapshot.get75thPercentile());
        appendMember(metricID, "p95", snapshot.get95thPercentile());
        appendMember(metricID, "p98", snapshot.get98thPercentile());
        appendMember(metricID, "p99", snapshot.get99thPercentile());
        appendMember(metricID, "p999", snapshot.get999thPercentile());
    }

    @Override
    public void export(MetricID metricID, Meter meter, Metadata metadata) {
        completeOrUpdateGroup(metricID, metadata);
        exportMetered(metricID, meter);
    }

    private void exportMetered(MetricID metricID, Metered metered) {
        appendMember(metricID, "count", metered.getCount());
        appendMember(metricID, "meanRate", metered.getMeanRate());
        appendMember(metricID, "oneMinRate", metered.getOneMinuteRate());
        appendMember(metricID, "fiveMinRate", metered.getFiveMinuteRate());
        appendMember(metricID, "fifteenMinRate", metered.getFifteenMinuteRate());
    }

    @Override
    public void export(MetricID metricID, SimpleTimer timer, Metadata metadata) {
        completeOrUpdateGroup(metricID, metadata);
        appendMember(metricID, "count", timer.getCount());
        appendMember(metricID, "elapsedTime", timer.getElapsedTime().toMillis());
        appendMember(metricID, "maxTimeDuration", millisOrNull(timer.getMaxTimeDuration()));
        appendMember(metricID, "minTimeDuration", millisOrNull(timer.getMinTimeDuration()));
    }

    private static Long millisOrNull(Duration d) {
        return d == null ? null : d.toMillis();
    }

    @Override
    public void export(MetricID metricID, Timer timer, Metadata metadata) {
        completeOrUpdateGroup(metricID, metadata);
        appendMember(metricID, "elapsedTime", millisOrNull(timer.getElapsedTime()));
        exportMetered(metricID, timer);
        exportSampling(metricID, timer);
    }

    @Override
    public void exportComplete() {
        completeGroup(null, null);
        completeScope();
        out.write(documentObj.build());
    }

    private void exportMetadata() {
        if (exportedBefore == null) {
            return;
        }
        JsonObjectBuilder target = scopeObj != null ? scopeObj : documentObj;
        JsonObjectBuilder metadataObj = Json.createObjectBuilder();
        Metadata metadata = exportedBeforeMetadata;
        metadataObj.add("unit", metadata.unit().orElse(MetricUnits.NONE));
        metadataObj.add("type", metadata.getTypeRaw().toString());
        if (metadata.description().isPresent()) {
            String desc = metadata.getDescription();
            if (!desc.isEmpty()) {
                metadataObj.add("description", desc);
            }
        }
        String displayName = metadata.getDisplayName();
        String name = exportedBefore.getName();
        if (!displayName.isEmpty() && !displayName.equals(name)) {
            metadataObj.add("displayName", displayName);
        }
        if (tagsArray != null) {
            metadataObj.add("tags", tagsArray.build());
            tagsArray = null;
        }
        target.add(name, metadataObj.build());
    }

    private void completeScope() {
        completeGroup(null, null);
        if (scopeObj != null) {
            JsonObject obj = scopeObj.build();
            if (obj.size() > 0) {
                documentObj.add(scope.getName(), obj);
            }
        }
    }

    private void completeGroup(MetricID current, Metadata metadata) {
        if (mode == Mode.GET && groupObj != null) {
            JsonObjectBuilder target = scopeObj != null ? scopeObj : documentObj;
            target.add(exportedBefore.getName(), groupObj);
            groupObj = null;
        }
        if (mode == Mode.OPTIONS) {
            if (isNameChange(current)) {
                exportMetadata();
            }
            List tags = tagsAlphabeticallySorted(current);
            if (!tags.isEmpty()) {
                JsonArrayBuilder currentTags = Json.createArrayBuilder();
                for (Tag tag : tags) {
                    currentTags.add(tagAsString(tag));
                }
                if (tagsArray == null) {
                    tagsArray = Json.createArrayBuilder();
                }
                tagsArray.add(currentTags.build());
            }
        }
        exportedBefore = current;
        exportedBeforeMetadata = metadata;
    }

    private void completeOrUpdateGroup(MetricID current, Metadata metadata) {
        if (mode == Mode.OPTIONS || isNameChange(current)) {
            completeGroup(current, metadata);
        }
        if (mode == Mode.GET && groupObj == null) {
            groupObj = Json.createObjectBuilder();
        }
    }

    private boolean isNameChange(MetricID current) {
        return current == null || exportedBefore == null || !exportedBefore.getName().equals(current.getName());
    }

    private void appendMember(MetricID metricID, Number value) {
        appendMember(metricID, null, value);
    }

    private void appendMember(MetricID metricID, String field, Number value) {
        if (mode == Mode.OPTIONS) {
            return; // nothing to do, metadata written in connection with group update
        }
        JsonObjectBuilder target = groupObj != null ? groupObj : scopeObj != null ? scopeObj : documentObj;
        String name = field != null ? field : metricID.getName();
        List tags = tagsAlphabeticallySorted(metricID);
        if (!tags.isEmpty()) {
            for (Tag tag : tags) {
                name += ';' + tagAsString(tag);
            }
        }
        if (value instanceof Float || value instanceof Double) {
            target.add(name, value.doubleValue());
        } else if (value instanceof BigDecimal) {
            target.add(name, (BigDecimal) value);
        } else if (value instanceof BigInteger) {
            target.add(name, (BigInteger) value);
        } else if (value == null) {
            target.addNull(name);
        } else {
            target.add(name, value.longValue());
        }
    }

    private static String tagAsString(Tag tag) {
        return tag.getTagName() + '=' + tag.getTagValue().replace(';', '_');
    }

    private static List tagsAlphabeticallySorted(MetricID metricID) {
        if (metricID == null) {
            return emptyList();
        }
        Tag[] tags = metricID.getTagsAsArray();
        if(tags.length == 0) {
            return emptyList();
        }
        Arrays.sort(tags, (a, b) -> a.getTagName().compareTo(b.getTagName()));
        return asList(tags);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy