blob: f9f0b3f7d7181f927f9295345e5ecac8074bdde1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.geronimo.microprofile.metrics.common.prometheus;
import static java.lang.Math.pow;
import static java.util.Collections.singletonMap;
import static java.util.Locale.ROOT;
import static java.util.Optional.of;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
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.Metric;
import org.eclipse.microprofile.metrics.MetricRegistry;
import org.eclipse.microprofile.metrics.MetricUnits;
import org.eclipse.microprofile.metrics.Snapshot;
import org.eclipse.microprofile.metrics.Timer;
// this is so weird to have this format built-in but not mainstream ones,
// todo: pby make it dropped from the spec
// note: this is a simplified serialization flavor and it can need some more love
// todo: cache all the keys, can easily be done decorating the registry and enriching metadata (ExtendedMetadata)
public class PrometheusFormatter {
private final Map<String, String> keyMapping = new HashMap<>();
private Predicate<String> prefixFilter = null;
public PrometheusFormatter enableOverriding(final Properties properties) {
properties.stringPropertyNames().forEach(k -> keyMapping.put(k, properties.getProperty(k)));
afterOverride();
return this;
}
public PrometheusFormatter enableOverriding() {
try (final InputStream source = Thread.currentThread().getContextClassLoader()
.getResourceAsStream("META-INF/geronimo-metrics/prometheus-mapping.properties")) {
if (source != null) {
final Properties properties = new Properties();
properties.load(source);
enableOverriding(properties);
}
} catch (final IOException e) {
// no-op
}
System.getProperties().stringPropertyNames().stream()
.filter(it -> it.startsWith("geronimo.metrics.prometheus.mapping."))
.forEach(k -> keyMapping.put(k.substring("geronimo.metrics.prometheus.mapping.".length()), System.getProperty(k)));
afterOverride();
return this;
}
private void afterOverride() {
final String prefix = keyMapping.get("geronimo.metrics.filter.prefix");
if (prefix == null) {
prefixFilter = null;
} else {
final List<String> prefixes = Stream.of(prefix.split(","))
.map(String::trim)
.filter(it -> !it.isEmpty())
.collect(toList());
final Predicate<String> directPredicate = name -> prefixes.stream().anyMatch(name::startsWith);
prefixFilter = name -> directPredicate.test(name) || directPredicate.test(keyMapping.getOrDefault(name, name));
}
}
public StringBuilder toText(final MetricRegistry registry,
final String registryKey,
final Map<String, Metric> entries) {
final Map<String, Metadata> metadatas = registry.getMetadata();
return entries.entrySet().stream()
.map(it -> new Entry(metadatas.get(it.getKey()), registryKey + ':' + toPrometheusKey(metadatas.get(it.getKey())), it.getValue()))
.filter(it -> prefixFilter == null || prefixFilter.test(it.prometheusKey))
.map(entry -> {
switch (entry.metadata.getTypeRaw()) {
case COUNTER: {
final String key = toPrometheusKey(entry.metadata);
return new StringBuilder()
.append(value(registryKey, key, Counter.class.cast(entry.metric).getCount(), entry.metadata));
}
case GAUGE: {
final Object val = Gauge.class.cast(entry.metric).getValue();
if (Number.class.isInstance(val)) {
final String key = toPrometheusKey(entry.metadata);
return new StringBuilder()
.append(value(registryKey, key, Number.class.cast(val).doubleValue(), entry.metadata));
}
return new StringBuilder();
}
case METERED: {
final String keyBase = toPrometheus(entry.metadata);
final String key = keyBase + toUnitSuffix(entry.metadata);
final Meter meter = Meter.class.cast(entry.metric);
return new StringBuilder()
.append(value(registryKey, key + "_count", meter.getCount(), entry.metadata))
.append(value(registryKey, keyBase + "_rate_per_second", meter.getMeanRate(), entry.metadata))
.append(value(registryKey, keyBase + "_one_min_rate_per_second", meter.getOneMinuteRate(), entry.metadata))
.append(value(registryKey, keyBase + "_five_min_rate_per_second", meter.getFiveMinuteRate(), entry.metadata))
.append(value(registryKey, keyBase + "_fifteen_min_rate_per_second", meter.getFifteenMinuteRate(), entry.metadata));
}
case TIMER: {
final String keyBase = toPrometheus(entry.metadata);
final String keyUnit = toUnitSuffix(entry.metadata);
final Timer timer = Timer.class.cast(entry.metric);
return new StringBuilder()
.append(type(registryKey, keyBase + keyUnit + " summary", entry.metadata))
.append(value(registryKey, keyBase + keyUnit + "_count", timer.getCount(), entry.metadata))
.append(value(registryKey, keyBase + "_rate_per_second", timer.getMeanRate(), entry.metadata))
.append(value(registryKey, keyBase + "_one_min_rate_per_second", timer.getOneMinuteRate(), entry.metadata))
.append(value(registryKey, keyBase + "_five_min_rate_per_second", timer.getFiveMinuteRate(), entry.metadata))
.append(value(registryKey, keyBase + "_fifteen_min_rate_per_second", timer.getFifteenMinuteRate(), entry.metadata))
.append(toPrometheus(registryKey, keyBase, keyUnit, timer.getSnapshot(), entry.metadata));
}
case HISTOGRAM:
final String keyBase = toPrometheus(entry.metadata);
final String keyUnit = toUnitSuffix(entry.metadata);
final Histogram histogram = Histogram.class.cast(entry.metric);
return new StringBuilder()
.append(type(registryKey, keyBase + keyUnit + " summary", entry.metadata))
.append(value(registryKey, keyBase + keyUnit + "_count", histogram.getCount(), entry.metadata))
.append(toPrometheus(registryKey, keyBase, keyUnit, histogram.getSnapshot(), entry.metadata));
default:
return new StringBuilder();
}
})
.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append);
}
private StringBuilder toPrometheus(final String registryKey, final String keyBase, final String keyUnit, final Snapshot snapshot, final Metadata metadata) {
final Function<Map<String, String>, Metadata> metaFactory = tag -> new Metadata(metadata.getName(), metadata.getDisplayName(), metadata.getDescription(), metadata.getTypeRaw(), metadata.getUnit(),
Stream.concat(metadata.getTags().entrySet().stream(), tag.entrySet().stream())
.map(e -> e.getKey() + '=' + e.getValue())
.collect(joining(",")));
final String completeKey = keyBase + keyUnit;
return new StringBuilder()
.append(value(registryKey, keyBase + "_min" + keyUnit, snapshot.getMin(), metadata))
.append(value(registryKey, keyBase + "_max" + keyUnit, snapshot.getMax(), metadata))
.append(value(registryKey, keyBase + "_mean" + keyUnit, snapshot.getMean(), metadata))
.append(value(registryKey, keyBase + "_stddev" + keyUnit, snapshot.getStdDev(), metadata))
.append(value(registryKey, completeKey, snapshot.getMedian(), metaFactory.apply(singletonMap("quantile", "0.5"))))
.append(value(registryKey, completeKey, snapshot.get75thPercentile(), metaFactory.apply(singletonMap("quantile", "0.75"))))
.append(value(registryKey, completeKey, snapshot.get95thPercentile(), metaFactory.apply(singletonMap("quantile", "0.95"))))
.append(value(registryKey, completeKey, snapshot.get98thPercentile(), metaFactory.apply(singletonMap("quantile", "0.98"))))
.append(value(registryKey, completeKey, snapshot.get99thPercentile(), metaFactory.apply(singletonMap("quantile", "0.99"))))
.append(value(registryKey, completeKey, snapshot.get999thPercentile(), metaFactory.apply(singletonMap("quantile", "0.999"))));
}
private String toPrometheusKey(final Metadata metadata) {
return toPrometheus(metadata) + toUnitSuffix(metadata);
}
private String toUnitSuffix(final Metadata metadata) {
return metadata.getUnit().equalsIgnoreCase("none") ?
"" : ("_" + toPrometheusUnit(metadata.getUnit()));
}
private StringBuilder value(final String registryKey, final String key, final double value,
final Metadata metadata) {
final String builtKey = registryKey + ':' + key;
return new StringBuilder()
.append(type(registryKey, key, metadata))
.append(keyMapping.getOrDefault(builtKey, builtKey))
.append(of(metadata.getTags())
.filter(t -> !t.isEmpty())
.map(t -> t.entrySet().stream()
.map(e -> e.getKey() + "=\"" + e.getValue() + "\"")
.collect(joining(",", "{", "}")))
.orElse(""))
.append(' ').append(toPrometheusValue(metadata.getUnit(), value)).append("\n");
}
private StringBuilder type(final String registryKey, final String key, final Metadata metadata) {
final String builtKey = registryKey + ':' + key;
final StringBuilder builder = new StringBuilder()
.append("# TYPE ").append(keyMapping.getOrDefault(builtKey, builtKey));
if (metadata != null) {
builder.append(' ').append(metadata.getType());
}
return builder.append("\n");
}
private String toPrometheusUnit(final String unit) {
if (unit == null) {
return null;
}
switch (unit) {
case MetricUnits.BITS:
case MetricUnits.KILOBITS:
case MetricUnits.MEGABITS:
case MetricUnits.GIGABITS:
case MetricUnits.KIBIBITS:
case MetricUnits.MEBIBITS:
case MetricUnits.GIBIBITS:
case MetricUnits.BYTES:
case MetricUnits.KILOBYTES:
case MetricUnits.MEGABYTES:
case MetricUnits.GIGABYTES:
return "bytes";
case MetricUnits.NANOSECONDS:
case MetricUnits.MICROSECONDS:
case MetricUnits.MILLISECONDS:
case MetricUnits.SECONDS:
case MetricUnits.MINUTES:
case MetricUnits.HOURS:
case MetricUnits.DAYS:
return "seconds";
default:
return unit;
}
}
private double toPrometheusValue(final String unit, final double value) {
if (unit == null) {
return value;
}
switch (unit) {
case MetricUnits.BITS:
return value / 8;
case MetricUnits.KILOBITS:
return value * 1000 / 8;
case MetricUnits.MEGABITS:
return value * pow(1000, 2) / 8;
case MetricUnits.GIGABITS:
return value * pow(1000, 3) / 8;
case MetricUnits.KIBIBITS:
return value * 128;
case MetricUnits.MEBIBITS:
return value * pow(1024, 2);
case MetricUnits.GIBIBITS:
return value * pow(1024, 3);
case MetricUnits.BYTES:
return value;
case MetricUnits.KILOBYTES:
return value * 1000;
case MetricUnits.MEGABYTES:
return value * pow(1000, 2);
case MetricUnits.GIGABYTES:
return value * pow(1000, 3);
case MetricUnits.NANOSECONDS:
return value;
case MetricUnits.MICROSECONDS:
return value / 1000;
case MetricUnits.MILLISECONDS:
return value / pow(1000, 2);
case MetricUnits.SECONDS:
return value / pow(1000, 3);
case MetricUnits.MINUTES:
return value * 60 / pow(1000, 3);
case MetricUnits.HOURS:
return value * pow(60, 2) / pow(1000, 3);
case MetricUnits.DAYS:
return value * pow(60, 2) * 24 / pow(1000, 3);
default:
return value;
}
}
private String toPrometheus(final Metadata metadata) {
return metadata.getName()
.replaceAll("[^\\w]+", "_")
.replaceAll("(.)(\\p{Upper})", "$1_$2")
.replace("__", "_")
.replace(":_", ":")
.toLowerCase(ROOT);
}
private static class Entry {
private final Metadata metadata;
private final String prometheusKey;
private final Metric metric;
private Entry(final Metadata metadata, final String prometheusKey, final Metric metric) {
this.metadata = metadata;
this.prometheusKey = prometheusKey;
this.metric = metric;
}
}
}