Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.opentelemetry.exporter.prometheus.Otel2PrometheusConverter Maven / Gradle / Ivy
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.exporter.prometheus;
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeLabelName;
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeMetricName;
import static java.util.Objects.requireNonNull;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.internal.ThrottlingLogger;
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
import io.opentelemetry.sdk.metrics.data.DoubleExemplarData;
import io.opentelemetry.sdk.metrics.data.DoublePointData;
import io.opentelemetry.sdk.metrics.data.ExemplarData;
import io.opentelemetry.sdk.metrics.data.ExponentialHistogramBuckets;
import io.opentelemetry.sdk.metrics.data.ExponentialHistogramData;
import io.opentelemetry.sdk.metrics.data.ExponentialHistogramPointData;
import io.opentelemetry.sdk.metrics.data.HistogramData;
import io.opentelemetry.sdk.metrics.data.HistogramPointData;
import io.opentelemetry.sdk.metrics.data.LongExemplarData;
import io.opentelemetry.sdk.metrics.data.LongPointData;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.metrics.data.SumData;
import io.opentelemetry.sdk.metrics.data.SummaryPointData;
import io.opentelemetry.sdk.metrics.data.ValueAtQuantile;
import io.opentelemetry.sdk.resources.Resource;
import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets;
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
import io.prometheus.metrics.model.snapshots.CounterSnapshot.CounterDataPointSnapshot;
import io.prometheus.metrics.model.snapshots.Exemplar;
import io.prometheus.metrics.model.snapshots.Exemplars;
import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
import io.prometheus.metrics.model.snapshots.GaugeSnapshot.GaugeDataPointSnapshot;
import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
import io.prometheus.metrics.model.snapshots.HistogramSnapshot.HistogramDataPointSnapshot;
import io.prometheus.metrics.model.snapshots.InfoSnapshot;
import io.prometheus.metrics.model.snapshots.InfoSnapshot.InfoDataPointSnapshot;
import io.prometheus.metrics.model.snapshots.Labels;
import io.prometheus.metrics.model.snapshots.MetricMetadata;
import io.prometheus.metrics.model.snapshots.MetricSnapshot;
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets;
import io.prometheus.metrics.model.snapshots.Quantile;
import io.prometheus.metrics.model.snapshots.Quantiles;
import io.prometheus.metrics.model.snapshots.SummarySnapshot;
import io.prometheus.metrics.model.snapshots.SummarySnapshot.SummaryDataPointSnapshot;
import io.prometheus.metrics.model.snapshots.Unit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/** Convert OpenTelemetry {@link MetricData} to Prometheus {@link MetricSnapshots}. */
final class Otel2PrometheusConverter {
private static final Logger LOGGER = Logger.getLogger(Otel2PrometheusConverter.class.getName());
private static final ThrottlingLogger THROTTLING_LOGGER = new ThrottlingLogger(LOGGER);
private static final String OTEL_SCOPE_NAME = "otel_scope_name";
private static final String OTEL_SCOPE_VERSION = "otel_scope_version";
private static final long NANOS_PER_MILLISECOND = TimeUnit.MILLISECONDS.toNanos(1);
static final int MAX_CACHE_SIZE = 10;
private final boolean otelScopeEnabled;
@Nullable private final Predicate allowedResourceAttributesFilter;
/**
* Used only if addResourceAttributesAsLabels is true. Once the cache reaches {@link
* #MAX_CACHE_SIZE}, it is cleared to protect against unbounded conversion over time.
*/
private final Map>> resourceAttributesToAllowedKeysCache;
/**
* Constructor with feature flag parameter.
*
* @param otelScopeEnabled enable generation of the OpenTelemetry instrumentation scope info
* metric and labels.
* @param allowedResourceAttributesFilter if not {@code null}, resource attributes with keys
* matching this predicate will be added as labels on each exported metric
*/
Otel2PrometheusConverter(
boolean otelScopeEnabled, @Nullable Predicate allowedResourceAttributesFilter) {
this.otelScopeEnabled = otelScopeEnabled;
this.allowedResourceAttributesFilter = allowedResourceAttributesFilter;
this.resourceAttributesToAllowedKeysCache =
allowedResourceAttributesFilter != null
? new ConcurrentHashMap<>()
: Collections.emptyMap();
}
MetricSnapshots convert(@Nullable Collection metricDataCollection) {
if (metricDataCollection == null || metricDataCollection.isEmpty()) {
return MetricSnapshots.of();
}
Map snapshotsByName = new HashMap<>(metricDataCollection.size());
Resource resource = null;
Set scopes = new LinkedHashSet<>();
for (MetricData metricData : metricDataCollection) {
MetricSnapshot snapshot = convert(metricData);
if (snapshot == null) {
continue;
}
putOrMerge(snapshotsByName, snapshot);
if (resource == null) {
resource = metricData.getResource();
}
if (otelScopeEnabled && !metricData.getInstrumentationScopeInfo().getAttributes().isEmpty()) {
scopes.add(metricData.getInstrumentationScopeInfo());
}
}
if (resource != null) {
putOrMerge(snapshotsByName, makeTargetInfo(resource));
}
if (otelScopeEnabled && !scopes.isEmpty()) {
putOrMerge(snapshotsByName, makeScopeInfo(scopes));
}
return new MetricSnapshots(snapshotsByName.values());
}
@Nullable
private MetricSnapshot convert(MetricData metricData) {
// Note that AggregationTemporality.DELTA should never happen
// because PrometheusMetricReader#getAggregationTemporality returns CUMULATIVE.
MetricMetadata metadata = convertMetadata(metricData);
InstrumentationScopeInfo scope = metricData.getInstrumentationScopeInfo();
switch (metricData.getType()) {
case LONG_GAUGE:
return convertLongGauge(
metadata, scope, metricData.getLongGaugeData().getPoints(), metricData.getResource());
case DOUBLE_GAUGE:
return convertDoubleGauge(
metadata, scope, metricData.getDoubleGaugeData().getPoints(), metricData.getResource());
case LONG_SUM:
SumData longSumData = metricData.getLongSumData();
if (longSumData.getAggregationTemporality() == AggregationTemporality.DELTA) {
return null;
} else if (longSumData.isMonotonic()) {
return convertLongCounter(
metadata, scope, longSumData.getPoints(), metricData.getResource());
} else {
return convertLongGauge(
metadata, scope, longSumData.getPoints(), metricData.getResource());
}
case DOUBLE_SUM:
SumData doubleSumData = metricData.getDoubleSumData();
if (doubleSumData.getAggregationTemporality() == AggregationTemporality.DELTA) {
return null;
} else if (doubleSumData.isMonotonic()) {
return convertDoubleCounter(
metadata, scope, doubleSumData.getPoints(), metricData.getResource());
} else {
return convertDoubleGauge(
metadata, scope, doubleSumData.getPoints(), metricData.getResource());
}
case HISTOGRAM:
HistogramData histogramData = metricData.getHistogramData();
if (histogramData.getAggregationTemporality() == AggregationTemporality.DELTA) {
return null;
} else {
return convertHistogram(
metadata, scope, histogramData.getPoints(), metricData.getResource());
}
case EXPONENTIAL_HISTOGRAM:
ExponentialHistogramData exponentialHistogramData =
metricData.getExponentialHistogramData();
if (exponentialHistogramData.getAggregationTemporality() == AggregationTemporality.DELTA) {
return null;
} else {
return convertExponentialHistogram(
metadata, scope, exponentialHistogramData.getPoints(), metricData.getResource());
}
case SUMMARY:
return convertSummary(
metadata, scope, metricData.getSummaryData().getPoints(), metricData.getResource());
}
return null;
}
private GaugeSnapshot convertLongGauge(
MetricMetadata metadata,
InstrumentationScopeInfo scope,
Collection dataPoints,
Resource resource) {
List data = new ArrayList<>(dataPoints.size());
for (LongPointData longData : dataPoints) {
data.add(
new GaugeDataPointSnapshot(
(double) longData.getValue(),
convertAttributes(resource, scope, longData.getAttributes()),
convertLongExemplar(longData.getExemplars())));
}
return new GaugeSnapshot(metadata, data);
}
private CounterSnapshot convertLongCounter(
MetricMetadata metadata,
InstrumentationScopeInfo scope,
Collection dataPoints,
Resource resource) {
List data = new ArrayList<>(dataPoints.size());
for (LongPointData longData : dataPoints) {
data.add(
new CounterDataPointSnapshot(
(double) longData.getValue(),
convertAttributes(resource, scope, longData.getAttributes()),
convertLongExemplar(longData.getExemplars()),
longData.getStartEpochNanos() / NANOS_PER_MILLISECOND));
}
return new CounterSnapshot(metadata, data);
}
private GaugeSnapshot convertDoubleGauge(
MetricMetadata metadata,
InstrumentationScopeInfo scope,
Collection dataPoints,
Resource resource) {
List data = new ArrayList<>(dataPoints.size());
for (DoublePointData doubleData : dataPoints) {
data.add(
new GaugeDataPointSnapshot(
doubleData.getValue(),
convertAttributes(resource, scope, doubleData.getAttributes()),
convertDoubleExemplar(doubleData.getExemplars())));
}
return new GaugeSnapshot(metadata, data);
}
private CounterSnapshot convertDoubleCounter(
MetricMetadata metadata,
InstrumentationScopeInfo scope,
Collection dataPoints,
Resource resource) {
List data = new ArrayList<>(dataPoints.size());
for (DoublePointData doubleData : dataPoints) {
data.add(
new CounterDataPointSnapshot(
doubleData.getValue(),
convertAttributes(resource, scope, doubleData.getAttributes()),
convertDoubleExemplar(doubleData.getExemplars()),
doubleData.getStartEpochNanos() / NANOS_PER_MILLISECOND));
}
return new CounterSnapshot(metadata, data);
}
private HistogramSnapshot convertHistogram(
MetricMetadata metadata,
InstrumentationScopeInfo scope,
Collection dataPoints,
Resource resource) {
List data = new ArrayList<>(dataPoints.size());
for (HistogramPointData histogramData : dataPoints) {
List boundaries = new ArrayList<>(histogramData.getBoundaries().size() + 1);
boundaries.addAll(histogramData.getBoundaries());
boundaries.add(Double.POSITIVE_INFINITY);
data.add(
new HistogramDataPointSnapshot(
ClassicHistogramBuckets.of(boundaries, histogramData.getCounts()),
histogramData.getSum(),
convertAttributes(resource, scope, histogramData.getAttributes()),
convertDoubleExemplars(histogramData.getExemplars()),
histogramData.getStartEpochNanos() / NANOS_PER_MILLISECOND));
}
return new HistogramSnapshot(metadata, data);
}
@Nullable
private HistogramSnapshot convertExponentialHistogram(
MetricMetadata metadata,
InstrumentationScopeInfo scope,
Collection dataPoints,
Resource resource) {
List data = new ArrayList<>(dataPoints.size());
for (ExponentialHistogramPointData histogramData : dataPoints) {
int scale = histogramData.getScale();
if (scale < -4) {
THROTTLING_LOGGER.log(
Level.WARNING,
"Dropping histogram "
+ metadata.getName()
+ " with attributes "
+ histogramData.getAttributes()
+ " because it has scale < -4 which is unsupported in Prometheus");
return null;
}
// Scale > 8 are not supported in Prometheus. Histograms with scale > 8 are scaled down to 8.
int scaleDown = scale > 8 ? scale - 8 : 0;
data.add(
new HistogramDataPointSnapshot(
scale - scaleDown,
histogramData.getZeroCount(),
0L,
convertExponentialHistogramBuckets(histogramData.getPositiveBuckets(), scaleDown),
convertExponentialHistogramBuckets(histogramData.getNegativeBuckets(), scaleDown),
histogramData.getSum(),
convertAttributes(resource, scope, histogramData.getAttributes()),
convertDoubleExemplars(histogramData.getExemplars()),
histogramData.getStartEpochNanos() / NANOS_PER_MILLISECOND));
}
return new HistogramSnapshot(metadata, data);
}
private static NativeHistogramBuckets convertExponentialHistogramBuckets(
ExponentialHistogramBuckets buckets, int scaleDown) {
if (buckets.getBucketCounts().isEmpty()) {
return NativeHistogramBuckets.EMPTY;
}
List otelCounts = buckets.getBucketCounts();
List indexes = new ArrayList<>(otelCounts.size());
List counts = new ArrayList<>(otelCounts.size());
int previousIndex = (buckets.getOffset() >> scaleDown) + 1;
long count = 0;
for (int i = 0; i < otelCounts.size(); i++) {
int index = ((buckets.getOffset() + i) >> scaleDown) + 1;
if (index > previousIndex) {
indexes.add(previousIndex);
counts.add(count);
previousIndex = index;
count = 0;
}
count += otelCounts.get(i);
}
indexes.add(previousIndex);
counts.add(count);
return NativeHistogramBuckets.of(indexes, counts);
}
private SummarySnapshot convertSummary(
MetricMetadata metadata,
InstrumentationScopeInfo scope,
Collection dataPoints,
Resource resource) {
List data = new ArrayList<>(dataPoints.size());
for (SummaryPointData summaryData : dataPoints) {
data.add(
new SummaryDataPointSnapshot(
summaryData.getCount(),
summaryData.getSum(),
convertQuantiles(summaryData.getValues()),
convertAttributes(resource, scope, summaryData.getAttributes()),
Exemplars.EMPTY, // Exemplars for Summaries not implemented yet.
summaryData.getStartEpochNanos() / NANOS_PER_MILLISECOND));
}
return new SummarySnapshot(metadata, data);
}
private static Quantiles convertQuantiles(List values) {
List result = new ArrayList<>(values.size());
for (ValueAtQuantile value : values) {
result.add(new Quantile(value.getQuantile(), value.getValue()));
}
return Quantiles.of(result);
}
@Nullable
private Exemplar convertLongExemplar(List exemplars) {
if (exemplars.isEmpty()) {
return null;
} else {
LongExemplarData exemplar = exemplars.get(0);
return convertExemplar((double) exemplar.getValue(), exemplar);
}
}
/** Converts the first exemplar in the list if available, else returns {#code null}. */
@Nullable
private Exemplar convertDoubleExemplar(List exemplars) {
if (exemplars.isEmpty()) {
return null;
} else {
DoubleExemplarData exemplar = exemplars.get(0);
return convertExemplar(exemplar.getValue(), exemplar);
}
}
/** Converts the first exemplar in the list if available, else returns {#code null}. */
private Exemplars convertDoubleExemplars(List exemplars) {
List result = new ArrayList<>(exemplars.size());
for (DoubleExemplarData exemplar : exemplars) {
result.add(convertExemplar(exemplar.getValue(), exemplar));
}
return Exemplars.of(result);
}
private Exemplar convertExemplar(double value, ExemplarData exemplar) {
SpanContext spanContext = exemplar.getSpanContext();
if (spanContext.isValid()) {
return new Exemplar(
value,
convertAttributes(
null, // resource attributes are only copied for point's attributes
null, // scope attributes are only needed for point's attributes
exemplar.getFilteredAttributes(),
"trace_id",
spanContext.getTraceId(),
"span_id",
spanContext.getSpanId()),
exemplar.getEpochNanos() / NANOS_PER_MILLISECOND);
} else {
return new Exemplar(
value,
convertAttributes(
null, // resource attributes are only copied for point's attributes
null, // scope attributes are only needed for point's attributes
exemplar.getFilteredAttributes()),
exemplar.getEpochNanos() / NANOS_PER_MILLISECOND);
}
}
private InfoSnapshot makeTargetInfo(Resource resource) {
return new InfoSnapshot(
new MetricMetadata("target"),
Collections.singletonList(
new InfoDataPointSnapshot(
convertAttributes(
null, // resource attributes are only copied for point's attributes
null, // scope attributes are only needed for point's attributes
resource.getAttributes()))));
}
private InfoSnapshot makeScopeInfo(Set scopes) {
List prometheusScopeInfos = new ArrayList<>(scopes.size());
for (InstrumentationScopeInfo scope : scopes) {
prometheusScopeInfos.add(
new InfoDataPointSnapshot(
convertAttributes(
null, // resource attributes are only copied for point's attributes
scope,
scope.getAttributes())));
}
return new InfoSnapshot(new MetricMetadata("otel_scope"), prometheusScopeInfos);
}
/**
* Convert OpenTelemetry attributes to Prometheus labels.
*
* @param resource optional resource (attributes) to be converted.
* @param scope will be converted to {@code otel_scope_*} labels if {@code otelScopeEnabled} is
* {@code true}.
* @param attributes the attributes to be converted.
* @param additionalAttributes optional list of key/value pairs, may be empty.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
private Labels convertAttributes(
@Nullable Resource resource,
@Nullable InstrumentationScopeInfo scope,
Attributes attributes,
String... additionalAttributes) {
List> allowedAttributeKeys =
allowedResourceAttributesFilter != null
? filterAllowedResourceAttributeKeys(resource)
: Collections.emptyList();
Map labelNameToValue = new HashMap<>();
attributes.forEach(
(key, value) -> labelNameToValue.put(sanitizeLabelName(key.getKey()), value.toString()));
for (int i = 0; i < additionalAttributes.length; i += 2) {
labelNameToValue.putIfAbsent(
requireNonNull(additionalAttributes[i]), additionalAttributes[i + 1]);
}
if (otelScopeEnabled && scope != null) {
labelNameToValue.putIfAbsent(OTEL_SCOPE_NAME, scope.getName());
if (scope.getVersion() != null) {
labelNameToValue.putIfAbsent(OTEL_SCOPE_VERSION, scope.getVersion());
}
}
if (resource != null) {
Attributes resourceAttributes = resource.getAttributes();
for (AttributeKey attributeKey : allowedAttributeKeys) {
Object attributeValue = resourceAttributes.get(attributeKey);
if (attributeValue != null) {
labelNameToValue.putIfAbsent(
sanitizeLabelName(attributeKey.getKey()), attributeValue.toString());
}
}
}
String[] names = new String[labelNameToValue.size()];
String[] values = new String[labelNameToValue.size()];
int[] pos = new int[] {0};
labelNameToValue.forEach(
(name, value) -> {
names[pos[0]] = name;
values[pos[0]] = value;
pos[0] += 1;
});
return Labels.of(names, values);
}
private List> filterAllowedResourceAttributeKeys(@Nullable Resource resource) {
requireNonNull(
allowedResourceAttributesFilter,
"This method should only be called when allowedResourceAttributesFilter is not null.");
if (resource == null) {
return Collections.emptyList();
}
List> allowedAttributeKeys =
resourceAttributesToAllowedKeysCache.computeIfAbsent(
resource.getAttributes(),
resourceAttributes ->
resourceAttributes.asMap().keySet().stream()
.filter(o -> allowedResourceAttributesFilter.test(o.getKey()))
.collect(Collectors.toList()));
if (resourceAttributesToAllowedKeysCache.size() > MAX_CACHE_SIZE) {
resourceAttributesToAllowedKeysCache.clear();
}
return allowedAttributeKeys;
}
private static MetricMetadata convertMetadata(MetricData metricData) {
String name = sanitizeMetricName(metricData.getName());
String help = metricData.getDescription();
Unit unit = PrometheusUnitsHelper.convertUnit(metricData.getUnit());
if (unit != null && !name.endsWith(unit.toString())) {
name = name + "_" + unit;
}
// Repeated __ are not allowed according to spec, although this is allowed in prometheus
while (name.contains("__")) {
name = name.replace("__", "_");
}
return new MetricMetadata(name, help, unit);
}
private static void putOrMerge(
Map snapshotsByName, MetricSnapshot snapshot) {
String name = snapshot.getMetadata().getPrometheusName();
if (snapshotsByName.containsKey(name)) {
MetricSnapshot merged = merge(snapshotsByName.get(name), snapshot);
if (merged != null) {
snapshotsByName.put(name, merged);
}
} else {
snapshotsByName.put(name, snapshot);
}
}
/**
* OpenTelemetry may use the same metric name multiple times but in different instrumentation
* scopes. In that case, we try to merge the metrics. They will have different {@code
* otel_scope_name} attributes. However, merging is only possible if the metrics have the same
* type. If the type differs, we log a message and drop one of them.
*/
@Nullable
private static MetricSnapshot merge(MetricSnapshot a, MetricSnapshot b) {
MetricMetadata metadata = mergeMetadata(a.getMetadata(), b.getMetadata());
if (metadata == null) {
return null;
}
int numberOfDataPoints = a.getDataPoints().size() + b.getDataPoints().size();
if (a instanceof GaugeSnapshot && b instanceof GaugeSnapshot) {
List dataPoints = new ArrayList<>(numberOfDataPoints);
dataPoints.addAll(((GaugeSnapshot) a).getDataPoints());
dataPoints.addAll(((GaugeSnapshot) b).getDataPoints());
return new GaugeSnapshot(metadata, dataPoints);
} else if (a instanceof CounterSnapshot && b instanceof CounterSnapshot) {
List dataPoints = new ArrayList<>(numberOfDataPoints);
dataPoints.addAll(((CounterSnapshot) a).getDataPoints());
dataPoints.addAll(((CounterSnapshot) b).getDataPoints());
return new CounterSnapshot(metadata, dataPoints);
} else if (a instanceof HistogramSnapshot && b instanceof HistogramSnapshot) {
List dataPoints = new ArrayList<>(numberOfDataPoints);
dataPoints.addAll(((HistogramSnapshot) a).getDataPoints());
dataPoints.addAll(((HistogramSnapshot) b).getDataPoints());
return new HistogramSnapshot(metadata, dataPoints);
} else if (a instanceof SummarySnapshot && b instanceof SummarySnapshot) {
List dataPoints = new ArrayList<>(numberOfDataPoints);
dataPoints.addAll(((SummarySnapshot) a).getDataPoints());
dataPoints.addAll(((SummarySnapshot) b).getDataPoints());
return new SummarySnapshot(metadata, dataPoints);
} else if (a instanceof InfoSnapshot && b instanceof InfoSnapshot) {
List dataPoints = new ArrayList<>(numberOfDataPoints);
dataPoints.addAll(((InfoSnapshot) a).getDataPoints());
dataPoints.addAll(((InfoSnapshot) b).getDataPoints());
return new InfoSnapshot(metadata, dataPoints);
} else {
THROTTLING_LOGGER.log(
Level.WARNING,
"Conflicting metric name "
+ a.getMetadata().getPrometheusName()
+ ": Found one metric with type "
+ typeString(a)
+ " and one of type "
+ typeString(b)
+ ". Dropping the one with type "
+ typeString(b)
+ ".");
return null;
}
}
@Nullable
private static MetricMetadata mergeMetadata(MetricMetadata a, MetricMetadata b) {
String name = a.getPrometheusName();
if (a.getName().equals(b.getName())) {
name = a.getName();
}
String help = null;
if (a.getHelp() != null && a.getHelp().equals(b.getHelp())) {
help = a.getHelp();
}
Unit unit = a.getUnit();
if (unit != null && !unit.equals(b.getUnit())) {
THROTTLING_LOGGER.log(
Level.WARNING,
"Conflicting metrics: Multiple metrics with name "
+ name
+ " but different units found. Dropping the one with unit "
+ b.getUnit()
+ ".");
return null;
}
return new MetricMetadata(name, help, unit);
}
private static String typeString(MetricSnapshot snapshot) {
// Simple helper for a log message.
return snapshot.getClass().getSimpleName().replace("Snapshot", "").toLowerCase(Locale.ENGLISH);
}
}