diff --git a/exporters/prometheus/build.gradle.kts b/exporters/prometheus/build.gradle.kts index d746c074bbf..f2d8497e6ec 100644 --- a/exporters/prometheus/build.gradle.kts +++ b/exporters/prometheus/build.gradle.kts @@ -12,12 +12,14 @@ dependencies { api(project(":sdk:metrics")) implementation(project(":sdk-extensions:autoconfigure-spi")) + implementation("io.prometheus:prometheus-metrics-exporter-httpserver:1.1.0") compileOnly("com.sun.net.httpserver:http") compileOnly("com.google.auto.value:auto-value-annotations") annotationProcessor("com.google.auto.value:auto-value") + testImplementation(project(":sdk:testing")) testImplementation("io.opentelemetry.proto:opentelemetry-proto") testImplementation("com.google.guava:guava") diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/NameSanitizer.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/NameSanitizer.java deleted file mode 100644 index 9e8c62c4ede..00000000000 --- a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/NameSanitizer.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.exporter.prometheus; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.regex.Pattern; - -/** Sanitizes a metric or label name. */ -class NameSanitizer implements Function { - - static final NameSanitizer INSTANCE = new NameSanitizer(); - - static final Pattern SANITIZE_CONSECUTIVE_UNDERSCORES = Pattern.compile("[_]{2,}"); - - private static final Pattern SANITIZE_PREFIX_PATTERN = Pattern.compile("^[^a-zA-Z_:]"); - private static final Pattern SANITIZE_BODY_PATTERN = Pattern.compile("[^a-zA-Z0-9_:]"); - - private final Function delegate; - private final Map cache = new ConcurrentHashMap<>(); - - NameSanitizer() { - this(NameSanitizer::sanitizeMetricName); - } - - // visible for testing - NameSanitizer(Function delegate) { - this.delegate = delegate; - } - - @Override - public String apply(String labelName) { - return cache.computeIfAbsent(labelName, delegate); - } - - private static String sanitizeMetricName(String metricName) { - return SANITIZE_CONSECUTIVE_UNDERSCORES - .matcher( - SANITIZE_BODY_PATTERN - .matcher(SANITIZE_PREFIX_PATTERN.matcher(metricName).replaceFirst("_")) - .replaceAll("_")) - .replaceAll("_"); - } -} diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverter.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverter.java new file mode 100644 index 00000000000..30247f6237f --- /dev/null +++ b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Otel2PrometheusConverter.java @@ -0,0 +1,547 @@ +/* + * 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 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.Arrays; +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.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** Convert OpenTelemetry {@link MetricData} to Prometheus {@link MetricSnapshots}. */ +public class Otel2PrometheusConverter { + + private static final Logger LOGGER = Logger.getLogger(Otel2PrometheusConverter.class.getName()); + private static final ThrottlingLogger THROTTLING_LOGGER = new ThrottlingLogger(LOGGER); + private final boolean otelScopeEnabled; + 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); + + public Otel2PrometheusConverter() { + this(true); + } + + /** + * Constructor with feature flag parameter. + * + * @param otelScopeEnabled enable generation of the OpenTelemetry instrumentation scope info + * metric and labels. + */ + public Otel2PrometheusConverter(boolean otelScopeEnabled) { + this.otelScopeEnabled = otelScopeEnabled; + } + + public 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) { + MetricMetadata metadata = convertMetadata(metricData); + InstrumentationScopeInfo scope = metricData.getInstrumentationScopeInfo(); + switch (metricData.getType()) { + case LONG_GAUGE: + return convertLongGauge(metadata, scope, metricData.getLongGaugeData().getPoints()); + case DOUBLE_GAUGE: + return convertDoubleGauge(metadata, scope, metricData.getDoubleGaugeData().getPoints()); + case LONG_SUM: + SumData longSumData = metricData.getLongSumData(); + if (longSumData.getAggregationTemporality() == AggregationTemporality.DELTA) { + return null; + } else if (longSumData.isMonotonic()) { + return convertLongCounter(metadata, scope, longSumData.getPoints()); + } else { + return convertLongGauge(metadata, scope, longSumData.getPoints()); + } + case DOUBLE_SUM: + SumData doubleSumData = metricData.getDoubleSumData(); + if (doubleSumData.getAggregationTemporality() == AggregationTemporality.DELTA) { + return null; + } else if (doubleSumData.isMonotonic()) { + return convertDoubleCounter(metadata, scope, doubleSumData.getPoints()); + } else { + return convertDoubleGauge(metadata, scope, doubleSumData.getPoints()); + } + case HISTOGRAM: + HistogramData histogramData = metricData.getHistogramData(); + if (histogramData.getAggregationTemporality() == AggregationTemporality.DELTA) { + return null; + } else { + return convertHistogram(metadata, scope, histogramData.getPoints()); + } + case EXPONENTIAL_HISTOGRAM: + ExponentialHistogramData exponentialHistogramData = + metricData.getExponentialHistogramData(); + if (exponentialHistogramData.getAggregationTemporality() == AggregationTemporality.DELTA) { + return null; + } else { + return convertExponentialHistogram(metadata, scope, exponentialHistogramData.getPoints()); + } + case SUMMARY: + return convertSummary(metadata, scope, metricData.getSummaryData().getPoints()); + } + return null; + } + + private GaugeSnapshot convertLongGauge( + MetricMetadata metadata, + InstrumentationScopeInfo scope, + Collection dataPoints) { + GaugeDataPointSnapshot[] data = new GaugeDataPointSnapshot[dataPoints.size()]; + int i = 0; + for (LongPointData longData : dataPoints) { + data[i++] = + new GaugeDataPointSnapshot( + (double) longData.getValue(), + convertAttributes(scope, longData.getAttributes()), + convertLongExemplar(longData.getExemplars())); + } + return new GaugeSnapshot(metadata, Arrays.asList(data)); + } + + private CounterSnapshot convertLongCounter( + MetricMetadata metadata, + InstrumentationScopeInfo scope, + Collection dataPoints) { + CounterDataPointSnapshot[] data = new CounterDataPointSnapshot[dataPoints.size()]; + int i = 0; + for (LongPointData longData : dataPoints) { + data[i++] = + new CounterDataPointSnapshot( + (double) longData.getValue(), + convertAttributes(scope, longData.getAttributes()), + convertLongExemplar(longData.getExemplars()), + longData.getStartEpochNanos() / NANOS_PER_MILLISECOND); + } + return new CounterSnapshot(metadata, Arrays.asList(data)); + } + + private GaugeSnapshot convertDoubleGauge( + MetricMetadata metadata, + InstrumentationScopeInfo scope, + Collection dataPoints) { + GaugeDataPointSnapshot[] data = new GaugeDataPointSnapshot[dataPoints.size()]; + int i = 0; + for (DoublePointData doubleData : dataPoints) { + data[i++] = + new GaugeDataPointSnapshot( + doubleData.getValue(), + convertAttributes(scope, doubleData.getAttributes()), + convertDoubleExemplar(doubleData.getExemplars())); + } + return new GaugeSnapshot(metadata, Arrays.asList(data)); + } + + private CounterSnapshot convertDoubleCounter( + MetricMetadata metadata, + InstrumentationScopeInfo scope, + Collection dataPoints) { + CounterDataPointSnapshot[] data = new CounterDataPointSnapshot[dataPoints.size()]; + int i = 0; + for (DoublePointData doubleData : dataPoints) { + data[i++] = + new CounterDataPointSnapshot( + doubleData.getValue(), + convertAttributes(scope, doubleData.getAttributes()), + convertDoubleExemplar(doubleData.getExemplars()), + doubleData.getStartEpochNanos() / NANOS_PER_MILLISECOND); + } + return new CounterSnapshot(metadata, Arrays.asList(data)); + } + + private HistogramSnapshot convertHistogram( + MetricMetadata metadata, + InstrumentationScopeInfo scope, + Collection dataPoints) { + HistogramDataPointSnapshot[] data = new HistogramDataPointSnapshot[dataPoints.size()]; + int i = 0; + for (HistogramPointData histogramData : dataPoints) { + List boundaries = new ArrayList<>(histogramData.getBoundaries().size() + 1); + boundaries.addAll(histogramData.getBoundaries()); + boundaries.add(Double.POSITIVE_INFINITY); + data[i++] = + new HistogramDataPointSnapshot( + ClassicHistogramBuckets.of(boundaries, histogramData.getCounts()), + histogramData.getSum(), + convertAttributes(scope, histogramData.getAttributes()), + convertDoubleExemplars(histogramData.getExemplars()), + histogramData.getStartEpochNanos() / NANOS_PER_MILLISECOND); + } + return new HistogramSnapshot(metadata, Arrays.asList(data)); + } + + @Nullable + private HistogramSnapshot convertExponentialHistogram( + MetricMetadata metadata, + InstrumentationScopeInfo scope, + Collection dataPoints) { + HistogramDataPointSnapshot[] data = new HistogramDataPointSnapshot[dataPoints.size()]; + int i = 0; + for (ExponentialHistogramPointData histogramData : dataPoints) { + int scale = histogramData.getScale(); + if (scale < -4) { + // Scale < -4 is not supported in Prometheus. Histograms with scale < -4 are dropped. + 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[i++] = + new HistogramDataPointSnapshot( + scale - scaleDown, + histogramData.getZeroCount(), + 0L, + convertExponentialHistogramBuckets(histogramData.getPositiveBuckets(), scaleDown), + convertExponentialHistogramBuckets(histogramData.getNegativeBuckets(), scaleDown), + histogramData.getSum(), + convertAttributes(scope, histogramData.getAttributes()), + convertDoubleExemplars(histogramData.getExemplars()), + histogramData.getStartEpochNanos() / NANOS_PER_MILLISECOND); + } + return new HistogramSnapshot(metadata, Arrays.asList(data)); + } + + @SuppressWarnings("MethodCanBeStatic") + private 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) { + SummaryDataPointSnapshot[] data = new SummaryDataPointSnapshot[dataPoints.size()]; + int i = 0; + for (SummaryPointData summaryData : dataPoints) { + data[i++] = + new SummaryDataPointSnapshot( + summaryData.getCount(), + summaryData.getSum(), + convertQuantiles(summaryData.getValues()), + convertAttributes(scope, summaryData.getAttributes()), + Exemplars.EMPTY, // Exemplars for Summaries not implemented yet. + summaryData.getStartEpochNanos() / NANOS_PER_MILLISECOND); + } + return new SummarySnapshot(metadata, Arrays.asList(data)); + } + + @SuppressWarnings("MethodCanBeStatic") + private Quantiles convertQuantiles(List values) { + Quantile[] result = new Quantile[values.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = new Quantile(values.get(i).getQuantile(), values.get(i).getValue()); + } + return Quantiles.of(result); + } + + @Nullable + private Exemplar convertLongExemplar(List exemplars) { + if (exemplars.size() == 0) { + return null; + } else { + LongExemplarData exemplar = exemplars.get(0); + return convertExemplar((double) exemplar.getValue(), exemplar); + } + } + + @Nullable + private Exemplar convertDoubleExemplar(List exemplars) { + if (exemplars.size() == 0) { + return null; + } else { + DoubleExemplarData exemplar = exemplars.get(0); + return convertExemplar(exemplar.getValue(), exemplar); + } + } + + private Exemplars convertDoubleExemplars(List exemplars) { + Exemplar[] result = new Exemplar[exemplars.size()]; + for (int i = 0; i < result.length; i++) { + DoubleExemplarData exemplar = exemplars.get(i); + result[i] = 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, + exemplar.getFilteredAttributes(), + "trace_id", + spanContext.getTraceId(), + "span_id", + spanContext.getSpanId()), + exemplar.getEpochNanos() / NANOS_PER_MILLISECOND); + } else { + return new Exemplar( + value, + convertAttributes(null, 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.getAttributes())))); + } + + private InfoSnapshot makeScopeInfo(Set scopes) { + List prometheusScopeInfos = new ArrayList<>(scopes.size()); + for (InstrumentationScopeInfo scope : scopes) { + prometheusScopeInfos.add( + new InfoDataPointSnapshot(convertAttributes(scope, scope.getAttributes()))); + } + return new InfoSnapshot(new MetricMetadata("otel_scope"), prometheusScopeInfos); + } + + private Labels convertAttributes( + @Nullable InstrumentationScopeInfo scope, + Attributes attributes, + String... additionalAttributes) { + int numberOfScopeAttributes = 0; + if (otelScopeEnabled && scope != null) { + numberOfScopeAttributes = scope.getVersion() == null ? 1 : 2; + } + String[] names = + new String[attributes.size() + numberOfScopeAttributes + additionalAttributes.length / 2]; + String[] values = new String[names.length]; + int[] pos = new int[] {0}; // using an array because we want to increment in a forEach() lambda. + attributes.forEach( + (key, value) -> { + names[pos[0]] = sanitizeLabelName(key.getKey()); + values[pos[0]] = value.toString(); + pos[0]++; + }); + for (int i = 0; i < additionalAttributes.length; i += 2) { + names[pos[0]] = additionalAttributes[i]; + values[pos[0]] = additionalAttributes[i + 1]; + pos[0]++; + } + if (otelScopeEnabled && scope != null) { + names[pos[0]] = OTEL_SCOPE_NAME; + values[pos[0]] = scope.getName(); + pos[0]++; + if (scope.getVersion() != null) { + names[pos[0]] = OTEL_SCOPE_VERSION; + values[pos[0]] = scope.getVersion(); + pos[0]++; + } + } + return Labels.of(names, values); + } + + @SuppressWarnings("MethodCanBeStatic") + private 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 += "_" + unit; + } + return new MetricMetadata(name, help, unit); + } + + private void putOrMerge(Map snapshotsByName, MetricSnapshot snapshot) { + String name = snapshot.getMetadata().getName(); + 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 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 + @SuppressWarnings("MethodCanBeStatic") + private 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); + } + + @SuppressWarnings("MethodCanBeStatic") + private String typeString(MetricSnapshot snapshot) { + // Simple helper for a log message. + return snapshot.getClass().getSimpleName().replace("Snapshot", "").toLowerCase(Locale.ENGLISH); + } +} diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServer.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServer.java index cf1515a645e..38213dffb27 100644 --- a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServer.java +++ b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServer.java @@ -10,56 +10,31 @@ package io.opentelemetry.exporter.prometheus; -import static java.util.stream.Collectors.joining; - -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.internal.DaemonThreadFactory; import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; -import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.export.CollectionRegistration; import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.prometheus.metrics.exporter.httpserver.HTTPServer; +import io.prometheus.metrics.exporter.httpserver.MetricsHandler; +import io.prometheus.metrics.model.registry.PrometheusRegistry; import java.io.IOException; -import java.io.OutputStream; import java.io.UncheckedIOException; -import java.net.HttpURLConnection; -import java.net.InetAddress; import java.net.InetSocketAddress; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.zip.GZIPOutputStream; import javax.annotation.Nullable; /** * A {@link MetricReader} that starts an HTTP server that will collect metrics and serialize to * Prometheus text format on request. */ -// Very similar to -// https://github.com/prometheus/client_java/blob/master/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/HTTPServer.java public final class PrometheusHttpServer implements MetricReader { - private static final DaemonThreadFactory THREAD_FACTORY = - new DaemonThreadFactory("prometheus-http"); - private static final Logger LOGGER = Logger.getLogger(PrometheusHttpServer.class.getName()); - - private final HttpServer server; - private final ExecutorService executor; - private volatile CollectionRegistration collectionRegistration = CollectionRegistration.noop(); + private final HTTPServer httpServer; + private final PrometheusMetricReader prometheusMetricReader; + private final PrometheusRegistry prometheusRegistry; + private final String host; /** * Returns a new {@link PrometheusHttpServer} which can be registered to an {@link @@ -75,87 +50,59 @@ public static PrometheusHttpServerBuilder builder() { return new PrometheusHttpServerBuilder(); } - PrometheusHttpServer(String host, int port, ExecutorService executor) { + PrometheusHttpServer( + String host, + int port, + @Nullable ExecutorService executor, + PrometheusRegistry prometheusRegistry) { + this.prometheusMetricReader = new PrometheusMetricReader(/* otelScopeEnabled= */ true); + this.host = host; + this.prometheusRegistry = prometheusRegistry; + prometheusRegistry.register(prometheusMetricReader); try { - server = createServer(host, port); + this.httpServer = + HTTPServer.builder() + .hostname(host) + .port(port) + .executorService(executor) + .defaultHandler(new MetricsHandler()) + .buildAndStart(); } catch (IOException e) { throw new UncheckedIOException("Could not create Prometheus HTTP server", e); } - MetricsHandler metricsHandler = - new MetricsHandler(() -> collectionRegistration.collectAllMetrics()); - server.createContext("/", metricsHandler); - server.createContext("/metrics", metricsHandler); - server.createContext("/-/healthy", HealthHandler.INSTANCE); - this.executor = executor; - server.setExecutor(executor); - - start(); - } - - private static HttpServer createServer(String host, int port) throws IOException { - IOException exception = null; - for (InetAddress address : InetAddress.getAllByName(host)) { - try { - return HttpServer.create(new InetSocketAddress(address, port), 3); - } catch (IOException e) { - if (exception == null) { - exception = e; - } else { - exception.addSuppressed(e); - } - } - } - assert exception != null; - throw exception; - } - - private void start() { - // server.start must be called from a daemon thread for it to be a daemon. - if (Thread.currentThread().isDaemon()) { - server.start(); - return; - } - - Thread thread = THREAD_FACTORY.newThread(server::start); - thread.start(); - try { - thread.join(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } } @Override public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { - return AggregationTemporality.CUMULATIVE; + return prometheusMetricReader.getAggregationTemporality(instrumentType); } @Override public void register(CollectionRegistration registration) { - this.collectionRegistration = registration; + prometheusMetricReader.register(registration); } @Override public CompletableResultCode forceFlush() { - return CompletableResultCode.ofSuccess(); + return prometheusMetricReader.forceFlush(); } @Override public CompletableResultCode shutdown() { CompletableResultCode result = new CompletableResultCode(); - Thread thread = - THREAD_FACTORY.newThread( - () -> { - try { - server.stop(10); - executor.shutdownNow(); - } catch (Throwable t) { - result.fail(); - return; - } - result.succeed(); - }); - thread.start(); + Runnable shutdownFunction = + () -> { + try { + prometheusRegistry.unregister(prometheusMetricReader); + httpServer.stop(); + prometheusMetricReader.shutdown().whenComplete(result::succeed); + } catch (Throwable t) { + result.fail(); + } + }; + Thread shutdownThread = new Thread(shutdownFunction, "prometheus-httpserver-shutdown"); + shutdownThread.setDaemon(true); + shutdownThread.start(); return result; } @@ -166,112 +113,11 @@ public void close() { @Override public String toString() { - return "PrometheusHttpServer{address=" + server.getAddress() + "}"; + return "PrometheusHttpServer{address=" + getAddress() + "}"; } // Visible for testing. InetSocketAddress getAddress() { - return server.getAddress(); - } - - private static class MetricsHandler implements HttpHandler { - - private final Set allConflictHeaderNames = - Collections.newSetFromMap(new ConcurrentHashMap<>()); - - private final Supplier> metricsSupplier; - - private MetricsHandler(Supplier> metricsSupplier) { - this.metricsSupplier = metricsSupplier; - } - - @Override - public void handle(HttpExchange exchange) throws IOException { - Collection metrics = metricsSupplier.get(); - Set requestedNames = parseQuery(exchange.getRequestURI().getRawQuery()); - Predicate filter = - requestedNames.isEmpty() ? unused -> true : requestedNames::contains; - Serializer serializer = - Serializer.create(exchange.getRequestHeaders().getFirst("Accept"), filter); - exchange.getResponseHeaders().set("Content-Type", serializer.contentType()); - - boolean compress = shouldUseCompression(exchange); - if (compress) { - exchange.getResponseHeaders().set("Content-Encoding", "gzip"); - } - - if (exchange.getRequestMethod().equals("HEAD")) { - exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, -1); - } else { - exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0); - OutputStream out; - if (compress) { - out = new GZIPOutputStream(exchange.getResponseBody()); - } else { - out = exchange.getResponseBody(); - } - Set conflictHeaderNames = serializer.write(metrics, out); - conflictHeaderNames.removeAll(allConflictHeaderNames); - if (conflictHeaderNames.size() > 0 && LOGGER.isLoggable(Level.WARNING)) { - LOGGER.log( - Level.WARNING, - "Metric conflict(s) detected. Multiple metrics with same name but different type: " - + conflictHeaderNames.stream().collect(joining(",", "[", "]"))); - allConflictHeaderNames.addAll(conflictHeaderNames); - } - } - exchange.close(); - } - } - - private static boolean shouldUseCompression(HttpExchange exchange) { - List encodingHeaders = exchange.getRequestHeaders().get("Accept-Encoding"); - if (encodingHeaders == null) { - return false; - } - - for (String encodingHeader : encodingHeaders) { - String[] encodings = encodingHeader.split(","); - for (String encoding : encodings) { - if (encoding.trim().equalsIgnoreCase("gzip")) { - return true; - } - } - } - return false; - } - - private static Set parseQuery(@Nullable String query) throws IOException { - if (query == null) { - return Collections.emptySet(); - } - Set names = new HashSet<>(); - String[] pairs = query.split("&"); - for (String pair : pairs) { - int idx = pair.indexOf("="); - if (idx != -1 && URLDecoder.decode(pair.substring(0, idx), "UTF-8").equals("name[]")) { - names.add(URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); - } - } - return names; - } - - private enum HealthHandler implements HttpHandler { - INSTANCE; - - private static final byte[] RESPONSE = "Exporter is Healthy.".getBytes(StandardCharsets.UTF_8); - private static final String CONTENT_LENGTH_VALUE = String.valueOf(RESPONSE.length); - - @Override - public void handle(HttpExchange exchange) throws IOException { - exchange.getResponseHeaders().set("Content-Length", CONTENT_LENGTH_VALUE); - if (exchange.getRequestMethod().equals("HEAD")) { - exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, -1); - } else { - exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, RESPONSE.length); - exchange.getResponseBody().write(RESPONSE); - } - exchange.close(); - } + return new InetSocketAddress(host, httpServer.getPort()); } } diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServerBuilder.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServerBuilder.java index 539f46811ed..e9fc5a1a0e2 100644 --- a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServerBuilder.java +++ b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServerBuilder.java @@ -8,9 +8,8 @@ import static io.opentelemetry.api.internal.Utils.checkArgument; import static java.util.Objects.requireNonNull; -import io.opentelemetry.sdk.internal.DaemonThreadFactory; +import io.prometheus.metrics.model.registry.PrometheusRegistry; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import javax.annotation.Nullable; /** A builder for {@link PrometheusHttpServer}. */ @@ -21,6 +20,7 @@ public final class PrometheusHttpServerBuilder { private String host = DEFAULT_HOST; private int port = DEFAULT_PORT; + private PrometheusRegistry prometheusRegistry = PrometheusRegistry.defaultRegistry; @Nullable private ExecutorService executor; @@ -46,21 +46,20 @@ public PrometheusHttpServerBuilder setExecutor(ExecutorService executor) { return this; } + /** Sets the {@link PrometheusRegistry} to be used for {@link PrometheusHttpServer}. */ + public PrometheusHttpServerBuilder setPrometheusRegistry(PrometheusRegistry prometheusRegistry) { + requireNonNull(prometheusRegistry, "prometheusRegistry"); + this.prometheusRegistry = prometheusRegistry; + return this; + } + /** * Returns a new {@link PrometheusHttpServer} with the configuration of this builder which can be * registered with a {@link io.opentelemetry.sdk.metrics.SdkMeterProvider}. */ public PrometheusHttpServer build() { - ExecutorService executorService = this.executor; - if (executorService == null) { - executorService = getDefaultExecutor(); - } - return new PrometheusHttpServer(host, port, executorService); + return new PrometheusHttpServer(host, port, executor, prometheusRegistry); } PrometheusHttpServerBuilder() {} - - private static ExecutorService getDefaultExecutor() { - return Executors.newFixedThreadPool(5, new DaemonThreadFactory("prometheus-http")); - } } diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusMetricNameMapper.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusMetricNameMapper.java deleted file mode 100644 index 0cdc35ea33d..00000000000 --- a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusMetricNameMapper.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.exporter.prometheus; - -import com.google.auto.value.AutoValue; -import io.opentelemetry.api.internal.StringUtils; -import io.opentelemetry.sdk.metrics.data.MetricData; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiFunction; -import javax.annotation.concurrent.Immutable; - -/** A class that maps a raw metric name to Prometheus equivalent name. */ -class PrometheusMetricNameMapper implements BiFunction { - private static final String TOTAL_SUFFIX = "_total"; - static final PrometheusMetricNameMapper INSTANCE = new PrometheusMetricNameMapper(); - - private final Map cache = new ConcurrentHashMap<>(); - private final BiFunction delegate; - - // private constructor - prevent external object initialization - private PrometheusMetricNameMapper() { - this(PrometheusMetricNameMapper::mapToPrometheusName); - } - - // Visible for testing - PrometheusMetricNameMapper(BiFunction delegate) { - this.delegate = delegate; - } - - @Override - public String apply(MetricData rawMetric, PrometheusType prometheusType) { - return cache.computeIfAbsent( - createKeyForCacheMapping(rawMetric, prometheusType), - metricData -> delegate.apply(rawMetric, prometheusType)); - } - - private static String mapToPrometheusName(MetricData rawMetric, PrometheusType prometheusType) { - String name = NameSanitizer.INSTANCE.apply(rawMetric.getName()); - String prometheusEquivalentUnit = - PrometheusUnitsHelper.getEquivalentPrometheusUnit(rawMetric.getUnit()); - boolean shouldAppendUnit = - !StringUtils.isNullOrEmpty(prometheusEquivalentUnit) - && !name.contains(prometheusEquivalentUnit); - // trim counter's _total suffix so the unit is placed before it. - if (prometheusType == PrometheusType.COUNTER && name.endsWith(TOTAL_SUFFIX)) { - name = name.substring(0, name.length() - TOTAL_SUFFIX.length()); - } - // append prometheus unit if not null or empty. - if (shouldAppendUnit) { - name = name + "_" + prometheusEquivalentUnit; - } - - // replace _total suffix, or add if it wasn't already present. - if (prometheusType == PrometheusType.COUNTER) { - name = name + TOTAL_SUFFIX; - } - // special case - gauge - if (rawMetric.getUnit().equals("1") - && prometheusType == PrometheusType.GAUGE - && !name.contains("ratio")) { - name = name + "_ratio"; - } - return name; - } - - /** - * Creates a suitable mapping key to be used for maintaining mapping between raw metric and its - * equivalent Prometheus name. - * - * @param metricData the metric data for which the mapping is to be created. - * @param prometheusType the prometheus type to which the metric is to be mapped. - * @return an {@link ImmutableMappingKey} that can be used as a key for mapping between metric - * data and its prometheus equivalent name. - */ - private static ImmutableMappingKey createKeyForCacheMapping( - MetricData metricData, PrometheusType prometheusType) { - return ImmutableMappingKey.create( - metricData.getName(), metricData.getUnit(), prometheusType.name()); - } - - /** - * Objects of this class acts as mapping keys for Prometheus metric mapping cache used in {@link - * PrometheusMetricNameMapper}. - */ - @Immutable - @AutoValue - abstract static class ImmutableMappingKey { - static ImmutableMappingKey create( - String rawMetricName, String rawMetricUnit, String prometheusType) { - return new AutoValue_PrometheusMetricNameMapper_ImmutableMappingKey( - rawMetricName, rawMetricUnit, prometheusType); - } - - abstract String rawMetricName(); - - abstract String rawMetricUnit(); - - abstract String prometheusType(); - } -} diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusMetricReader.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusMetricReader.java new file mode 100644 index 00000000000..02ac63e4d81 --- /dev/null +++ b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusMetricReader.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.prometheus; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.export.CollectionRegistration; +import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.prometheus.metrics.model.registry.MultiCollector; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; + +/** + * This is the bridge between Prometheus and OpenTelemetry. + * + *

The {@link PrometheusMetricReader} is a Prometheus {@link MultiCollector} and can be + * registered with the {@link io.prometheus.metrics.model.registry.PrometheusRegistry + * PrometheusRegistry}. It's also an OpenTelemetry {@link MetricReader} and can be registered with a + * {@link io.opentelemetry.sdk.metrics.SdkMeterProvider SdkMeterProvider}. + */ +public class PrometheusMetricReader implements MetricReader, MultiCollector { + + private volatile CollectionRegistration collectionRegistration = CollectionRegistration.noop(); + private final Otel2PrometheusConverter converter; + + /** See {@link Otel2PrometheusConverter#Otel2PrometheusConverter(boolean)}. */ + public PrometheusMetricReader(boolean otelScopeEnabled) { + this.converter = new Otel2PrometheusConverter(otelScopeEnabled); + } + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return AggregationTemporality.CUMULATIVE; + } + + @Override + public void register(CollectionRegistration registration) { + this.collectionRegistration = registration; + } + + @Override + public CompletableResultCode forceFlush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public MetricSnapshots collect() { + return converter.convert(collectionRegistration.collectAllMetrics()); + } +} diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusType.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusType.java deleted file mode 100644 index 8f55022d3b8..00000000000 --- a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusType.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.exporter.prometheus; - -import io.opentelemetry.sdk.metrics.data.DoublePointData; -import io.opentelemetry.sdk.metrics.data.LongPointData; -import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.metrics.data.SumData; - -// Four types we use are same in prometheus and openmetrics format -enum PrometheusType { - GAUGE("gauge"), - COUNTER("counter"), - SUMMARY("summary"), - HISTOGRAM("histogram"); - - private final String typeString; - - PrometheusType(String typeString) { - this.typeString = typeString; - } - - static PrometheusType forMetric(MetricData metric) { - switch (metric.getType()) { - case LONG_GAUGE: - case DOUBLE_GAUGE: - return GAUGE; - case LONG_SUM: - SumData longSumData = metric.getLongSumData(); - if (longSumData.isMonotonic()) { - return COUNTER; - } - return GAUGE; - case DOUBLE_SUM: - SumData doubleSumData = metric.getDoubleSumData(); - if (doubleSumData.isMonotonic()) { - return COUNTER; - } - return GAUGE; - case SUMMARY: - return SUMMARY; - case HISTOGRAM: - case EXPONENTIAL_HISTOGRAM: - return HISTOGRAM; - } - throw new IllegalArgumentException( - "Unsupported metric type, this generally indicates version misalignment " - + "among opentelemetry dependencies. Please make sure to use opentelemetry-bom."); - } - - String getTypeString() { - return typeString; - } -} diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusUnitsHelper.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusUnitsHelper.java index a0ca81669d9..94c8bf15698 100644 --- a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusUnitsHelper.java +++ b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/PrometheusUnitsHelper.java @@ -5,208 +5,84 @@ package io.opentelemetry.exporter.prometheus; -import static io.opentelemetry.exporter.prometheus.NameSanitizer.SANITIZE_CONSECUTIVE_UNDERSCORES; +import io.prometheus.metrics.model.snapshots.Unit; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; -import io.opentelemetry.api.internal.StringUtils; -import java.util.regex.Pattern; +/** Convert OpenTelemetry unit names to Prometheus units. */ +class PrometheusUnitsHelper { -/** - * A utility class that contains helper function(s) to aid conversion from OTLP to Prometheus units. - * - * @see OpenMetrics - * specification for units - * @see Prometheus best practices - * for units - */ -final class PrometheusUnitsHelper { - - private static final Pattern INVALID_CHARACTERS_PATTERN = Pattern.compile("[^a-zA-Z0-9]"); - private static final Pattern CHARACTERS_BETWEEN_BRACES_PATTERN = Pattern.compile("\\{(.*?)}"); - private static final Pattern SANITIZE_LEADING_UNDERSCORES = Pattern.compile("^_+"); - private static final Pattern SANITIZE_TRAILING_UNDERSCORES = Pattern.compile("_+$"); - - private PrometheusUnitsHelper() { - // Prevent object creation for utility classes - } + private static final Map pluralNames = new ConcurrentHashMap<>(); + private static final Map singularNames = new ConcurrentHashMap<>(); + private static final Map predefinedUnits = new ConcurrentHashMap<>(); - /** - * A utility function that returns the equivalent Prometheus name for the provided OTLP metric - * unit. - * - * @param rawMetricUnitName The raw metric unit for which Prometheus metric unit needs to be - * computed. - * @return the computed Prometheus metric unit equivalent of the OTLP metric un - */ - static String getEquivalentPrometheusUnit(String rawMetricUnitName) { - if (StringUtils.isNullOrEmpty(rawMetricUnitName)) { - return rawMetricUnitName; - } - // Drop units specified between curly braces - String convertedMetricUnitName = removeUnitPortionInBraces(rawMetricUnitName); - // Handling for the "per" unit(s), e.g. foo/bar -> foo_per_bar - convertedMetricUnitName = convertRateExpressedToPrometheusUnit(convertedMetricUnitName); - // Converting abbreviated unit names to full names - return cleanUpString(getPrometheusUnit(convertedMetricUnitName)); + // See + // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/c3b2997563106e11d39f66eec629fde25dce2bdd/pkg/translator/prometheus/normalize_name.go#L19-L19 + static { + // Time + initUnit("d", "days", "day"); + initUnit("h", "hours", "hour"); + initUnit("min", "minutes", "minute"); + initUnit("s", "seconds", "second"); + initUnit("ms", "milliseconds", "millisecond"); + initUnit("us", "microseconds", "microsecond"); + initUnit("ns", "nanoseconds", "nanosecond"); + // Bytes + initUnit("By", "bytes", "byte"); + initUnit("KiBy", "kibibytes", "kibibyte"); + initUnit("MiBy", "mebibytes", "mebibyte"); + initUnit("GiBy", "gibibytes", "gibibyte"); + initUnit("TiBy", "tibibytes", "tibibyte"); + initUnit("KBy", "kilobytes", "kilobyte"); + initUnit("MBy", "megabytes", "megabyte"); + initUnit("GBy", "gigabytes", "gigabyte"); + initUnit("TBy", "terabytes", "terabyte"); + // SI + initUnit("m", "meters", "meter"); + initUnit("V", "volts", "volt"); + initUnit("A", "amperes", "ampere"); + initUnit("J", "joules", "joule"); + initUnit("W", "watts", "watt"); + initUnit("g", "grams", "gram"); + // Misc + initUnit("Cel", "celsius"); + initUnit("Hz", "hertz"); + initUnit("%", "percent"); + initUnit("1", "ratio"); } - /** - * This method is used to convert the units expressed as a rate via '/' symbol in their name to - * their expanded text equivalent. For instance, km/h => km_per_hour. The method operates on the - * input by splitting it in 2 parts - before and after '/' symbol and will attempt to expand any - * known unit abbreviation in both parts. Unknown abbreviations & unsupported characters will - * remain unchanged in the final output of this function. - * - * @param rateExpressedUnit The rate unit input that needs to be converted to its text equivalent. - * @return The text equivalent of unit expressed as rate. If the input does not contain '/', the - * function returns it as-is. - */ - private static String convertRateExpressedToPrometheusUnit(String rateExpressedUnit) { - if (!rateExpressedUnit.contains("/")) { - return rateExpressedUnit; - } - String[] rateEntities = rateExpressedUnit.split("/", 2); - // Only convert rate expressed units if it's a valid expression - if (rateEntities[1].equals("")) { - return rateExpressedUnit; - } - return getPrometheusUnit(rateEntities[0]) + "_per_" + getPrometheusPerUnit(rateEntities[1]); - } + private PrometheusUnitsHelper() {} - /** - * This method drops all characters enclosed within '{}' (including the curly braces) by replacing - * them with an empty string. Note that this method will not produce the intended effect if there - * are nested curly braces within the outer enclosure of '{}'. - * - *

For instance, {packet{s}s} => s}. - * - * @param unit The input unit from which text within curly braces needs to be removed. - * @return The resulting unit after removing the text within '{}'. - */ - private static String removeUnitPortionInBraces(String unit) { - return CHARACTERS_BETWEEN_BRACES_PATTERN.matcher(unit).replaceAll(""); + private static void initUnit(String otelName, String pluralName) { + pluralNames.put(otelName, pluralName); + predefinedUnits.put(otelName, new Unit(pluralName)); } - /** - * Replaces all characters that are not a letter or a digit with '_' to make the resulting string - * Prometheus compliant. This method also removes leading and trailing underscores - this is done - * to keep the resulting unit similar to what is produced from the collector's implementation. - * - * @param string The string input that needs to be made Prometheus compliant. - * @return the cleaned-up Prometheus compliant string. - */ - private static String cleanUpString(String string) { - return SANITIZE_LEADING_UNDERSCORES - .matcher( - SANITIZE_TRAILING_UNDERSCORES - .matcher( - SANITIZE_CONSECUTIVE_UNDERSCORES - .matcher(INVALID_CHARACTERS_PATTERN.matcher(string).replaceAll("_")) - .replaceAll("_")) - .replaceAll("")) - .replaceAll(""); + private static void initUnit(String otelName, String pluralName, String singularName) { + initUnit(otelName, pluralName); + singularNames.put(otelName, singularName); } - /** - * This method retrieves the expanded Prometheus unit name for known abbreviations. OTLP metrics - * use the c/s notation as specified at UCUM. The list of - * mappings is adopted from OpenTelemetry - * Collector Contrib. - * - * @param unitAbbreviation The unit that name that needs to be expanded/converted to Prometheus - * units. - * @return The expanded/converted unit name if known, otherwise returns the input unit name as-is. - */ - private static String getPrometheusUnit(String unitAbbreviation) { - switch (unitAbbreviation) { - // Time - case "d": - return "days"; - case "h": - return "hours"; - case "min": - return "minutes"; - case "s": - return "seconds"; - case "ms": - return "milliseconds"; - case "us": - return "microseconds"; - case "ns": - return "nanoseconds"; - // Bytes - case "By": - return "bytes"; - case "KiBy": - return "kibibytes"; - case "MiBy": - return "mebibytes"; - case "GiBy": - return "gibibytes"; - case "TiBy": - return "tibibytes"; - case "KBy": - return "kilobytes"; - case "MBy": - return "megabytes"; - case "GBy": - return "gigabytes"; - case "TBy": - return "terabytes"; - // SI - case "m": - return "meters"; - case "V": - return "volts"; - case "A": - return "amperes"; - case "J": - return "joules"; - case "W": - return "watts"; - case "g": - return "grams"; - // Misc - case "Cel": - return "celsius"; - case "Hz": - return "hertz"; - case "1": - return ""; - case "%": - return "percent"; - default: - return unitAbbreviation; + @Nullable + static Unit convertUnit(String otelUnit) { + if (otelUnit.isEmpty() || otelUnit.equals("1")) { + // The spec says "1" should be translated to "ratio", but this is not implemented in the Java + // SDK. + return null; } - } - - /** - * This method retrieves the expanded Prometheus unit name to be used with "per" units for known - * units. For example: s => per second (singular) - * - * @param perUnitAbbreviation The unit abbreviation used in a 'per' unit. - * @return The expanded unit equivalent to be used in 'per' unit if the input is a known unit, - * otherwise returns the input as-is. - */ - private static String getPrometheusPerUnit(String perUnitAbbreviation) { - switch (perUnitAbbreviation) { - case "s": - return "second"; - case "m": - return "minute"; - case "h": - return "hour"; - case "d": - return "day"; - case "w": - return "week"; - case "mo": - return "month"; - case "y": - return "year"; - default: - return perUnitAbbreviation; + if (predefinedUnits.containsKey(otelUnit)) { + return predefinedUnits.get(otelUnit); + } + if (otelUnit.contains("{")) { + return null; + } + if (otelUnit.contains("/")) { + String[] parts = otelUnit.split("/", 2); + String part1 = pluralNames.getOrDefault(parts[0], parts[0]); + String part2 = singularNames.getOrDefault(parts[1], parts[1]); + return new Unit(part1 + "_per_" + part2); } + return new Unit(otelUnit); } } diff --git a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Serializer.java b/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Serializer.java deleted file mode 100644 index a7089d03abb..00000000000 --- a/exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Serializer.java +++ /dev/null @@ -1,707 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -// Includes work from: - -/* - * Prometheus instrumentation library for JVM applications - * Copyright 2012-2015 The Prometheus Authors - * - * This product includes software developed at - * Boxever Ltd. (http://www.boxever.com/). - * - * This product includes software developed at - * SoundCloud Ltd. (http://soundcloud.com/). - * - * This product includes software developed as part of the - * Ocelli project by Netflix Inc. (https://github.com/Netflix/ocelli/). - */ - -package io.opentelemetry.exporter.prometheus; - -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.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.MetricDataType; -import io.opentelemetry.sdk.metrics.data.PointData; -import io.opentelemetry.sdk.metrics.data.SummaryPointData; -import io.opentelemetry.sdk.metrics.data.ValueAtQuantile; -import io.opentelemetry.sdk.resources.Resource; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.UncheckedIOException; -import java.io.Writer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; -import java.util.function.Predicate; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.Nullable; - -/** Serializes metrics into Prometheus exposition formats. */ -// Adapted from -// https://github.com/prometheus/client_java/blob/master/simpleclient_common/src/main/java/io/prometheus/client/exporter/common/TextFormat.java -abstract class Serializer { - - private static final Logger LOGGER = Logger.getLogger(Serializer.class.getName()); - private static final ThrottlingLogger THROTTLING_LOGGER = new ThrottlingLogger(LOGGER); - - static Serializer create(@Nullable String acceptHeader, Predicate filter) { - if (acceptHeader == null) { - return new Prometheus004Serializer(filter); - } - - for (String accepts : acceptHeader.split(",")) { - if ("application/openmetrics-text".equals(accepts.split(";")[0].trim())) { - return new OpenMetrics100Serializer(filter); - } - } - - return new Prometheus004Serializer(filter); - } - - private final Predicate metricNameFilter; - - Serializer(Predicate metricNameFilter) { - this.metricNameFilter = metricNameFilter; - } - - abstract String contentType(); - - abstract String headerName(String name, MetricData rawMetric, PrometheusType type); - - abstract void writeHelp(Writer writer, String description) throws IOException; - - abstract void writeTimestamp(Writer writer, long timestampNanos) throws IOException; - - abstract void writeExemplar( - Writer writer, - Collection exemplars, - double minExemplar, - double maxExemplar) - throws IOException; - - abstract void writeEof(Writer writer) throws IOException; - - final Set write(Collection metrics, OutputStream output) throws IOException { - Set conflictMetricNames = new HashSet<>(); - Map> metricsByName = new LinkedHashMap<>(); - Set scopes = new LinkedHashSet<>(); - // Iterate through metrics, filtering and grouping by headerName - for (MetricData metric : metrics) { - // Not supported in specification yet. - if (metric.getType() == MetricDataType.EXPONENTIAL_HISTOGRAM) { - continue; - } - // PrometheusHttpServer#getAggregationTemporality specifies cumulative temporality for - // all instruments, but non-SDK MetricProducers may not conform. We drop delta - // temporality metrics to avoid the complexity of stateful transformation to cumulative. - if (isDeltaTemporality(metric)) { - continue; - } - PrometheusType prometheusType = PrometheusType.forMetric(metric); - String metricName = PrometheusMetricNameMapper.INSTANCE.apply(metric, prometheusType); - // Skip metrics which do not pass metricNameFilter - if (!metricNameFilter.test(metricName)) { - continue; - } - List metricsWithHeaderName = - metricsByName.computeIfAbsent(metricName, unused -> new ArrayList<>()); - // Skip metrics with the same name but different type - if (metricsWithHeaderName.size() > 0 - && prometheusType != PrometheusType.forMetric(metricsWithHeaderName.get(0))) { - conflictMetricNames.add(metricName); - continue; - } - - metricsWithHeaderName.add(metric); - scopes.add(metric.getInstrumentationScopeInfo()); - } - - Optional optResource = metrics.stream().findFirst().map(MetricData::getResource); - try (Writer writer = - new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) { - if (optResource.isPresent()) { - writeResource(optResource.get(), writer); - } - for (InstrumentationScopeInfo scope : scopes) { - writeScopeInfo(scope, writer); - } - for (Map.Entry> entry : metricsByName.entrySet()) { - write(entry.getValue(), entry.getKey(), writer); - } - writeEof(writer); - } - return conflictMetricNames; - } - - private void write(List metrics, String metricName, Writer writer) - throws IOException { - // Write header based on first metric - MetricData first = metrics.get(0); - PrometheusType type = PrometheusType.forMetric(first); - String headerName = headerName(metricName, first, type); - String description = metrics.get(0).getDescription(); - - writer.write("# TYPE "); - writer.write(headerName); - writer.write(' '); - writer.write(type.getTypeString()); - writer.write('\n'); - - writer.write("# HELP "); - writer.write(headerName); - writer.write(' '); - writeHelp(writer, description); - writer.write('\n'); - - // Then write the metrics. - for (MetricData metric : metrics) { - write(metric, metricName, writer); - } - } - - private void write(MetricData metric, String metricName, Writer writer) throws IOException { - for (PointData point : getPoints(metric)) { - switch (metric.getType()) { - case DOUBLE_SUM: - case DOUBLE_GAUGE: - writePoint( - writer, - metric.getInstrumentationScopeInfo(), - metricName, - ((DoublePointData) point).getValue(), - point.getAttributes(), - point.getEpochNanos()); - break; - case LONG_SUM: - case LONG_GAUGE: - writePoint( - writer, - metric.getInstrumentationScopeInfo(), - metricName, - (double) ((LongPointData) point).getValue(), - point.getAttributes(), - point.getEpochNanos()); - break; - case HISTOGRAM: - writeHistogram( - writer, metric.getInstrumentationScopeInfo(), metricName, (HistogramPointData) point); - break; - case SUMMARY: - writeSummary( - writer, metric.getInstrumentationScopeInfo(), metricName, (SummaryPointData) point); - break; - case EXPONENTIAL_HISTOGRAM: - throw new IllegalArgumentException("Can't happen"); - } - } - } - - private static boolean isDeltaTemporality(MetricData metricData) { - switch (metricData.getType()) { - case LONG_GAUGE: - case DOUBLE_GAUGE: - case SUMMARY: - return false; - case LONG_SUM: - return metricData.getLongSumData().getAggregationTemporality() - == AggregationTemporality.DELTA; - case DOUBLE_SUM: - return metricData.getDoubleSumData().getAggregationTemporality() - == AggregationTemporality.DELTA; - case HISTOGRAM: - return metricData.getHistogramData().getAggregationTemporality() - == AggregationTemporality.DELTA; - default: - } - throw new IllegalArgumentException("Can't happen"); - } - - private static void writeResource(Resource resource, Writer writer) throws IOException { - if (resource.getAttributes().isEmpty()) { - return; - } - - writer.write("# TYPE target info\n"); - writer.write("# HELP target Target metadata\n"); - writer.write("target_info{"); - writeAttributePairs(writer, /* initialComma= */ false, resource.getAttributes()); - writer.write("} 1\n"); - } - - private static void writeScopeInfo( - InstrumentationScopeInfo instrumentationScopeInfo, Writer writer) throws IOException { - if (instrumentationScopeInfo.getAttributes().isEmpty()) { - return; - } - - writer.write("# TYPE otel_scope_info info\n"); - writer.write("# HELP otel_scope_info Scope metadata\n"); - writer.write("otel_scope_info{"); - writeScopeNameAndVersion(writer, instrumentationScopeInfo); - writeAttributePairs(writer, /* initialComma= */ true, instrumentationScopeInfo.getAttributes()); - writer.write("} 1\n"); - } - - private void writeHistogram( - Writer writer, - InstrumentationScopeInfo instrumentationScopeInfo, - String name, - HistogramPointData point) - throws IOException { - writePoint( - writer, - instrumentationScopeInfo, - name + "_count", - (double) point.getCount(), - point.getAttributes(), - point.getEpochNanos()); - writePoint( - writer, - instrumentationScopeInfo, - name + "_sum", - point.getSum(), - point.getAttributes(), - point.getEpochNanos()); - - long cumulativeCount = 0; - List counts = point.getCounts(); - for (int i = 0; i < counts.size(); i++) { - // This is the upper boundary (inclusive). I.e. all values should be < this value (LE - - // Less-then-or-Equal). - double boundary = getBucketUpperBound(point, i); - - cumulativeCount += counts.get(i); - writePoint( - writer, - instrumentationScopeInfo, - name + "_bucket", - (double) cumulativeCount, - point.getAttributes(), - point.getEpochNanos(), - "le", - boundary, - point.getExemplars(), - getBucketLowerBound(point, i), - boundary); - } - } - - /** - * Returns the lower bound of a bucket (all values would have been greater than). - * - * @param bucketIndex The bucket index, should match {@link HistogramPointData#getCounts()} index. - */ - static double getBucketLowerBound(HistogramPointData point, int bucketIndex) { - return bucketIndex > 0 ? point.getBoundaries().get(bucketIndex - 1) : Double.NEGATIVE_INFINITY; - } - - /** - * Returns the upper inclusive bound of a bucket (all values would have been less then or equal). - * - * @param bucketIndex The bucket index, should match {@link HistogramPointData#getCounts()} index. - */ - static double getBucketUpperBound(HistogramPointData point, int bucketIndex) { - List boundaries = point.getBoundaries(); - return (bucketIndex < boundaries.size()) - ? boundaries.get(bucketIndex) - : Double.POSITIVE_INFINITY; - } - - private void writeSummary( - Writer writer, - InstrumentationScopeInfo instrumentationScopeInfo, - String name, - SummaryPointData point) - throws IOException { - writePoint( - writer, - instrumentationScopeInfo, - name + "_count", - (double) point.getCount(), - point.getAttributes(), - point.getEpochNanos()); - writePoint( - writer, - instrumentationScopeInfo, - name + "_sum", - point.getSum(), - point.getAttributes(), - point.getEpochNanos()); - - List valueAtQuantiles = point.getValues(); - for (ValueAtQuantile valueAtQuantile : valueAtQuantiles) { - writePoint( - writer, - instrumentationScopeInfo, - name, - valueAtQuantile.getValue(), - point.getAttributes(), - point.getEpochNanos(), - "quantile", - valueAtQuantile.getQuantile(), - Collections.emptyList(), - 0, - 0); - } - } - - private void writePoint( - Writer writer, - InstrumentationScopeInfo instrumentationScopeInfo, - String name, - double value, - Attributes attributes, - long epochNanos) - throws IOException { - writer.write(name); - writeAttributes(writer, instrumentationScopeInfo, attributes); - writer.write(' '); - writeDouble(writer, value); - writer.write(' '); - writeTimestamp(writer, epochNanos); - writer.write('\n'); - } - - private void writePoint( - Writer writer, - InstrumentationScopeInfo instrumentationScopeInfo, - String name, - double value, - Attributes attributes, - long epochNanos, - String additionalAttrKey, - double additionalAttrValue, - Collection exemplars, - double minExemplar, - double maxExemplar) - throws IOException { - writer.write(name); - writeAttributes( - writer, instrumentationScopeInfo, attributes, additionalAttrKey, additionalAttrValue); - writer.write(' '); - writeDouble(writer, value); - writer.write(' '); - writeTimestamp(writer, epochNanos); - writeExemplar(writer, exemplars, minExemplar, maxExemplar); - writer.write('\n'); - } - - private static void writeAttributes( - Writer writer, InstrumentationScopeInfo instrumentationScopeInfo, Attributes attributes) - throws IOException { - writer.write('{'); - writeScopeNameAndVersion(writer, instrumentationScopeInfo); - if (!attributes.isEmpty()) { - writeAttributePairs(writer, /* initialComma= */ true, attributes); - } - writer.write('}'); - } - - private static void writeAttributes( - Writer writer, - InstrumentationScopeInfo instrumentationScopeInfo, - Attributes attributes, - String additionalAttrKey, - double additionalAttrValue) - throws IOException { - writer.write('{'); - writeScopeNameAndVersion(writer, instrumentationScopeInfo); - writer.write(','); - if (!attributes.isEmpty()) { - writeAttributePairs(writer, /* initialComma= */ false, attributes); - writer.write(','); - } - writer.write(additionalAttrKey); - writer.write("=\""); - writeDouble(writer, additionalAttrValue); - writer.write('"'); - writer.write('}'); - } - - private static void writeScopeNameAndVersion( - Writer writer, InstrumentationScopeInfo instrumentationScopeInfo) throws IOException { - writer.write("otel_scope_name=\""); - writer.write(instrumentationScopeInfo.getName()); - writer.write("\""); - if (instrumentationScopeInfo.getVersion() != null) { - writer.write(",otel_scope_version=\""); - writer.write(instrumentationScopeInfo.getVersion()); - writer.write("\""); - } - } - - private static void writeAttributePairs( - Writer writer, boolean initialComma, Attributes attributes) throws IOException { - try { - // This logic handles colliding attribute keys by joining the values, - // separated by a semicolon. It relies on the attributes being sorted, so that - // colliding attribute keys are in subsequent iterations of the for loop. - attributes.forEach( - new BiConsumer, Object>() { - boolean initialAttribute = true; - String previousKey = ""; - String previousValue = ""; - - @Override - public void accept(AttributeKey key, Object value) { - try { - String sanitizedKey = NameSanitizer.INSTANCE.apply(key.getKey()); - int compare = sanitizedKey.compareTo(previousKey); - if (compare == 0) { - // This key collides with the previous one. Append the value - // to the previous value instead of writing the key again. - writer.write(';'); - } else { - if (compare < 0) { - THROTTLING_LOGGER.log( - Level.WARNING, - "Dropping out-of-order attribute " - + sanitizedKey - + "=" - + value - + ", which occurred after " - + previousKey - + ". This can occur when an alternative Attribute implementation is used."); - } - if (!initialAttribute) { - writer.write('"'); - } - if (initialComma || !initialAttribute) { - writer.write(','); - } - writer.write(sanitizedKey); - writer.write("=\""); - } - String stringValue = value.toString(); - writeEscapedLabelValue(writer, stringValue); - previousKey = sanitizedKey; - previousValue = stringValue; - initialAttribute = false; - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - }); - if (!attributes.isEmpty()) { - writer.write('"'); - } - } catch (UncheckedIOException e) { - throw e.getCause(); - } - } - - private static void writeDouble(Writer writer, double d) throws IOException { - if (d == Double.POSITIVE_INFINITY) { - writer.write("+Inf"); - } else if (d == Double.NEGATIVE_INFINITY) { - writer.write("-Inf"); - } else { - writer.write(Double.toString(d)); - } - } - - static void writeEscapedLabelValue(Writer writer, String s) throws IOException { - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - switch (c) { - case '\\': - writer.write("\\\\"); - break; - case '\"': - writer.write("\\\""); - break; - case '\n': - writer.write("\\n"); - break; - default: - writer.write(c); - } - } - } - - static class Prometheus004Serializer extends Serializer { - - Prometheus004Serializer(Predicate metricNameFilter) { - super(metricNameFilter); - } - - @Override - String contentType() { - return "text/plain; version=0.0.4; charset=utf-8"; - } - - @Override - String headerName(String name, MetricData rawMetric, PrometheusType type) { - return name; - } - - @Override - void writeHelp(Writer writer, String help) throws IOException { - for (int i = 0; i < help.length(); i++) { - char c = help.charAt(i); - switch (c) { - case '\\': - writer.write("\\\\"); - break; - case '\n': - writer.write("\\n"); - break; - default: - writer.write(c); - } - } - } - - @Override - void writeTimestamp(Writer writer, long timestampNanos) throws IOException { - writer.write(Long.toString(TimeUnit.NANOSECONDS.toMillis(timestampNanos))); - } - - @Override - void writeExemplar( - Writer writer, - Collection exemplars, - double minExemplar, - double maxExemplar) { - // Don't write exemplars - } - - @Override - void writeEof(Writer writer) { - // Don't write EOF - } - } - - static class OpenMetrics100Serializer extends Serializer { - - OpenMetrics100Serializer(Predicate metricNameFilter) { - super(metricNameFilter); - } - - @Override - String contentType() { - return "application/openmetrics-text; version=1.0.0; charset=utf-8"; - } - - @Override - String headerName(String name, MetricData rawMetric, PrometheusType type) { - // If the name didn't originally have a _total suffix, and we added it later, omit it from the - // header. - String sanitizedOriginalName = NameSanitizer.INSTANCE.apply(rawMetric.getName()); - if (!sanitizedOriginalName.endsWith("_total") && (type == PrometheusType.COUNTER)) { - return name.substring(0, name.length() - "_total".length()); - } - return name; - } - - @Override - void writeHelp(Writer writer, String description) throws IOException { - writeEscapedLabelValue(writer, description); - } - - @Override - void writeTimestamp(Writer writer, long timestampNanos) throws IOException { - long timestampMillis = TimeUnit.NANOSECONDS.toMillis(timestampNanos); - writer.write(Long.toString(timestampMillis / 1000)); - writer.write("."); - long millis = timestampMillis % 1000; - if (millis < 100) { - writer.write('0'); - } - if (millis < 10) { - writer.write('0'); - } - writer.write(Long.toString(millis)); - } - - @Override - void writeExemplar( - Writer writer, - Collection exemplars, - double minExemplar, - double maxExemplar) - throws IOException { - for (ExemplarData exemplar : exemplars) { - double value = getExemplarValue(exemplar); - if (value > minExemplar && value <= maxExemplar) { - writer.write(" # {"); - SpanContext spanContext = exemplar.getSpanContext(); - if (spanContext.isValid()) { - // NB: Output sorted to match prometheus client library even though it shouldn't matter. - // OTel generally outputs in trace_id span_id order though so we can consider breaking - // from reference implementation if it makes sense. - writer.write("span_id=\""); - writer.write(spanContext.getSpanId()); - writer.write("\",trace_id=\""); - writer.write(spanContext.getTraceId()); - writer.write('"'); - } - writer.write("} "); - writeDouble(writer, value); - writer.write(' '); - writeTimestamp(writer, exemplar.getEpochNanos()); - // Only write one exemplar. - return; - } - } - } - - @Override - void writeEof(Writer writer) throws IOException { - writer.write("# EOF\n"); - } - } - - static Collection getPoints(MetricData metricData) { - switch (metricData.getType()) { - case DOUBLE_GAUGE: - return metricData.getDoubleGaugeData().getPoints(); - case DOUBLE_SUM: - return metricData.getDoubleSumData().getPoints(); - case LONG_GAUGE: - return metricData.getLongGaugeData().getPoints(); - case LONG_SUM: - return metricData.getLongSumData().getPoints(); - case SUMMARY: - return metricData.getSummaryData().getPoints(); - case HISTOGRAM: - return metricData.getHistogramData().getPoints(); - case EXPONENTIAL_HISTOGRAM: - return metricData.getExponentialHistogramData().getPoints(); - } - return Collections.emptyList(); - } - - private static double getExemplarValue(ExemplarData exemplar) { - return exemplar instanceof DoubleExemplarData - ? ((DoubleExemplarData) exemplar).getValue() - : (double) ((LongExemplarData) exemplar).getValue(); - } -} diff --git a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/NameSanitizerTest.java b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/NameSanitizerTest.java deleted file mode 100644 index 56eb36f085b..00000000000 --- a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/NameSanitizerTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.exporter.prometheus; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -class NameSanitizerTest { - - @Test - void testSanitizerCaching() { - AtomicInteger count = new AtomicInteger(); - Function delegate = labelName -> labelName + count.incrementAndGet(); - NameSanitizer sanitizer = new NameSanitizer(delegate); - String labelName = "http.name"; - - assertThat(sanitizer.apply(labelName)).isEqualTo("http.name1"); - assertThat(sanitizer.apply(labelName)).isEqualTo("http.name1"); - assertThat(sanitizer.apply(labelName)).isEqualTo("http.name1"); - assertThat(sanitizer.apply(labelName)).isEqualTo("http.name1"); - assertThat(sanitizer.apply(labelName)).isEqualTo("http.name1"); - assertThat(count).hasValue(1); - } - - @ParameterizedTest - @MethodSource("provideMetricNamesForTest") - void testSanitizerCleansing(String unsanitizedName, String sanitizedName) { - Assertions.assertEquals(sanitizedName, NameSanitizer.INSTANCE.apply(unsanitizedName)); - } - - private static Stream provideMetricNamesForTest() { - return Stream.of( - // valid name - already sanitized - Arguments.of( - "active_directory_ds_replication_network_io", - "active_directory_ds_replication_network_io"), - // consecutive underscores - Arguments.of("cpu_sp__d_hertz", "cpu_sp_d_hertz"), - // leading and trailing underscores - should be fine - Arguments.of("_cpu_speed_hertz_", "_cpu_speed_hertz_"), - // unsupported characters replaced - Arguments.of("metric_unit_$1000", "metric_unit_1000"), - // multiple unsupported characters - whitespace - Arguments.of("sample_me%%$$$_count_ !!@unit include", "sample_me_count_unit_include"), - // metric names cannot start with a number - Arguments.of("1_some_metric_name", "_some_metric_name"), - // metric names can have : - Arguments.of("sample_metric_name__:_per_meter", "sample_metric_name_:_per_meter"), - // Illegal characters - Arguments.of("cpu_sp$$d_hertz", "cpu_sp_d_hertz")); - } -} diff --git a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServerTest.java b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServerTest.java index 7f65cf1e0c2..41537635991 100644 --- a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServerTest.java +++ b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusHttpServerTest.java @@ -9,6 +9,7 @@ import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; @@ -33,6 +34,7 @@ import io.opentelemetry.sdk.metrics.internal.data.ImmutableMetricData; import io.opentelemetry.sdk.metrics.internal.data.ImmutableSumData; import io.opentelemetry.sdk.resources.Resource; +import io.prometheus.metrics.exporter.httpserver.HTTPServer; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.ServerSocket; @@ -52,8 +54,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; class PrometheusHttpServerTest { private static final AtomicReference> metricData = new AtomicReference<>(); @@ -65,7 +65,7 @@ class PrometheusHttpServerTest { static WebClient client; @RegisterExtension - LogCapturer logs = LogCapturer.create().captureForType(PrometheusHttpServer.class); + LogCapturer logs = LogCapturer.create().captureForType(Otel2PrometheusConverter.class); @BeforeAll static void beforeAll() { @@ -108,71 +108,71 @@ void invalidConfig() { .hasMessage("host must not be empty"); } - @ParameterizedTest - @ValueSource(strings = {"/metrics", "/"}) - void fetchPrometheus(String endpoint) { - AggregatedHttpResponse response = client.get(endpoint).aggregate().join(); + @Test + void fetchPrometheus() { + AggregatedHttpResponse response = client.get("/metrics").aggregate().join(); assertThat(response.status()).isEqualTo(HttpStatus.OK); assertThat(response.headers().get(HttpHeaderNames.CONTENT_TYPE)) .isEqualTo("text/plain; version=0.0.4; charset=utf-8"); - assertThat(response.contentUtf8()) - .isEqualTo( - "# TYPE target info\n" - + "# HELP target Target metadata\n" - + "target_info{kr=\"vr\"} 1\n" - + "# TYPE grpc_name_total counter\n" - + "# HELP grpc_name_total long_description\n" - + "grpc_name_total{otel_scope_name=\"grpc\",otel_scope_version=\"version\",kp=\"vp\"} 5.0 0\n" - + "# TYPE http_name_total counter\n" - + "# HELP http_name_total double_description\n" - + "http_name_total{otel_scope_name=\"http\",otel_scope_version=\"version\",kp=\"vp\"} 3.5 0\n"); + assertEquals( + "# HELP grpc_name_total long_description\n" + + "# TYPE grpc_name_total counter\n" + + "grpc_name_total{kp=\"vp\",otel_scope_name=\"grpc\",otel_scope_version=\"version\"} 5.0\n" + + "# HELP http_name_total double_description\n" + + "# TYPE http_name_total counter\n" + + "http_name_total{kp=\"vp\",otel_scope_name=\"http\",otel_scope_version=\"version\"} 3.5\n" + + "# TYPE target_info gauge\n" + + "target_info{kr=\"vr\"} 1\n", + response.contentUtf8()); } - @ParameterizedTest - @ValueSource(strings = {"/metrics", "/"}) - void fetchOpenMetrics(String endpoint) { + @Test + void fetchOpenMetrics() { AggregatedHttpResponse response = client .execute( RequestHeaders.of( HttpMethod.GET, - endpoint, + "/metrics", HttpHeaderNames.ACCEPT, "application/openmetrics-text")) .aggregate() .join(); - assertThat(response.status()).isEqualTo(HttpStatus.OK); - assertThat(response.headers().get(HttpHeaderNames.CONTENT_TYPE)) - .isEqualTo("application/openmetrics-text; version=1.0.0; charset=utf-8"); - assertThat(response.contentUtf8()) - .isEqualTo( - "# TYPE target info\n" - + "# HELP target Target metadata\n" - + "target_info{kr=\"vr\"} 1\n" - + "# TYPE grpc_name counter\n" - + "# HELP grpc_name long_description\n" - + "grpc_name_total{otel_scope_name=\"grpc\",otel_scope_version=\"version\",kp=\"vp\"} 5.0 0.000\n" - + "# TYPE http_name counter\n" - + "# HELP http_name double_description\n" - + "http_name_total{otel_scope_name=\"http\",otel_scope_version=\"version\",kp=\"vp\"} 3.5 0.000\n" - + "# EOF\n"); + assertEquals(HttpStatus.OK, response.status()); + assertEquals( + "application/openmetrics-text; version=1.0.0; charset=utf-8", + response.headers().get(HttpHeaderNames.CONTENT_TYPE)); + assertEquals( + "# TYPE grpc_name counter\n" + + "# HELP grpc_name long_description\n" + + "grpc_name_total{kp=\"vp\",otel_scope_name=\"grpc\",otel_scope_version=\"version\"} 5.0\n" + + "# TYPE http_name counter\n" + + "# HELP http_name double_description\n" + + "http_name_total{kp=\"vp\",otel_scope_name=\"http\",otel_scope_version=\"version\"} 3.5\n" + + "# TYPE target info\n" + + "target_info{kr=\"vr\"} 1\n" + + "# EOF\n", + response.contentUtf8()); } @Test void fetchFiltered() { AggregatedHttpResponse response = - client.get("/?name[]=grpc_name_total&name[]=bears_total").aggregate().join(); + client + .get("/?name[]=grpc_name_total&name[]=bears_total&name[]=target_info") + .aggregate() + .join(); assertThat(response.status()).isEqualTo(HttpStatus.OK); assertThat(response.headers().get(HttpHeaderNames.CONTENT_TYPE)) .isEqualTo("text/plain; version=0.0.4; charset=utf-8"); - assertThat(response.contentUtf8()) - .isEqualTo( - "# TYPE target info\n" - + "# HELP target Target metadata\n" - + "target_info{kr=\"vr\"} 1\n" - + "# TYPE grpc_name_total counter\n" - + "# HELP grpc_name_total long_description\n" - + "grpc_name_total{otel_scope_name=\"grpc\",otel_scope_version=\"version\",kp=\"vp\"} 5.0 0\n"); + assertEquals( + "" + + "# HELP grpc_name_total long_description\n" + + "# TYPE grpc_name_total counter\n" + + "grpc_name_total{kp=\"vp\",otel_scope_name=\"grpc\",otel_scope_version=\"version\"} 5.0\n" + + "# TYPE target_info gauge\n" + + "target_info{kr=\"vr\"} 1\n", + response.contentUtf8()); } @Test @@ -182,24 +182,23 @@ void fetchPrometheusCompressed() throws IOException { .decorator(RetryingClient.newDecorator(RetryRule.failsafe())) .addHeader(HttpHeaderNames.ACCEPT_ENCODING, "gzip") .build(); - AggregatedHttpResponse response = client.get("/").aggregate().join(); + AggregatedHttpResponse response = client.get("/metrics").aggregate().join(); assertThat(response.status()).isEqualTo(HttpStatus.OK); assertThat(response.headers().get(HttpHeaderNames.CONTENT_TYPE)) .isEqualTo("text/plain; version=0.0.4; charset=utf-8"); assertThat(response.headers().get(HttpHeaderNames.CONTENT_ENCODING)).isEqualTo("gzip"); GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(response.content().array())); String content = new String(ByteStreams.toByteArray(gis), StandardCharsets.UTF_8); - assertThat(content) - .isEqualTo( - "# TYPE target info\n" - + "# HELP target Target metadata\n" - + "target_info{kr=\"vr\"} 1\n" - + "# TYPE grpc_name_total counter\n" - + "# HELP grpc_name_total long_description\n" - + "grpc_name_total{otel_scope_name=\"grpc\",otel_scope_version=\"version\",kp=\"vp\"} 5.0 0\n" - + "# TYPE http_name_total counter\n" - + "# HELP http_name_total double_description\n" - + "http_name_total{otel_scope_name=\"http\",otel_scope_version=\"version\",kp=\"vp\"} 3.5 0\n"); + assertEquals( + "# HELP grpc_name_total long_description\n" + + "# TYPE grpc_name_total counter\n" + + "grpc_name_total{kp=\"vp\",otel_scope_name=\"grpc\",otel_scope_version=\"version\"} 5.0\n" + + "# HELP http_name_total double_description\n" + + "# TYPE http_name_total counter\n" + + "http_name_total{kp=\"vp\",otel_scope_name=\"http\",otel_scope_version=\"version\"} 3.5\n" + + "# TYPE target_info gauge\n" + + "target_info{kr=\"vr\"} 1\n", + content); } @Test @@ -216,7 +215,7 @@ void fetchHealth() { AggregatedHttpResponse response = client.get("/-/healthy").aggregate().join(); assertThat(response.status()).isEqualTo(HttpStatus.OK); - assertThat(response.contentUtf8()).isEqualTo("Exporter is Healthy."); + assertThat(response.contentUtf8()).isEqualTo("Exporter is healthy.\n"); } @Test @@ -260,28 +259,23 @@ void fetch_DuplicateMetrics() { Collections.singletonList( ImmutableLongPointData.create(123, 456, Attributes.empty(), 3)))))); - AggregatedHttpResponse response = client.get("/").aggregate().join(); + AggregatedHttpResponse response = client.get("/metrics").aggregate().join(); assertThat(response.status()).isEqualTo(HttpStatus.OK); assertThat(response.headers().get(HttpHeaderNames.CONTENT_TYPE)) .isEqualTo("text/plain; version=0.0.4; charset=utf-8"); - assertThat(response.contentUtf8()) - .isEqualTo( - "# TYPE target info\n" - + "# HELP target Target metadata\n" - + "target_info{kr=\"vr\"} 1\n" - + "# TYPE foo_unit_total counter\n" - + "# HELP foo_unit_total description1\n" - + "foo_unit_total{otel_scope_name=\"scope1\"} 1.0 0\n" - + "foo_unit_total{otel_scope_name=\"scope2\"} 2.0 0\n"); + assertEquals( + "" + + "# TYPE foo_unit_total counter\n" + + "foo_unit_total{otel_scope_name=\"scope1\"} 1.0\n" + + "foo_unit_total{otel_scope_name=\"scope2\"} 2.0\n" + + "# TYPE target_info gauge\n" + + "target_info{kr=\"vr\"} 1\n", + response.contentUtf8()); // Validate conflict warning message assertThat(logs.getEvents()).hasSize(1); logs.assertContains( - "Metric conflict(s) detected. Multiple metrics with same name but different type: [foo_unit_total]"); - - // Make another request and confirm warning is only logged once - client.get("/").aggregate().join(); - assertThat(logs.getEvents()).hasSize(1); + "Conflicting metric name foo_unit: Found one metric with type counter and one of type gauge. Dropping the one with type gauge."); } @Test @@ -293,8 +287,9 @@ void stringRepresentation() { @Test void defaultExecutor() { assertThat(prometheusServer) - .extracting("executor", as(InstanceOfAssertFactories.type(ThreadPoolExecutor.class))) - .satisfies(executor -> assertThat(executor.getCorePoolSize()).isEqualTo(5)); + .extracting("httpServer", as(InstanceOfAssertFactories.type(HTTPServer.class))) + .extracting("executorService", as(InstanceOfAssertFactories.type(ThreadPoolExecutor.class))) + .satisfies(executor -> assertThat(executor.getCorePoolSize()).isEqualTo(1)); } @Test @@ -311,8 +306,10 @@ void customExecutor() throws IOException { .setExecutor(scheduledExecutor) .build()) { assertThat(server) + .extracting("httpServer", as(InstanceOfAssertFactories.type(HTTPServer.class))) .extracting( - "executor", as(InstanceOfAssertFactories.type(ScheduledThreadPoolExecutor.class))) + "executorService", + as(InstanceOfAssertFactories.type(ScheduledThreadPoolExecutor.class))) .satisfies(executor -> assertThat(executor).isSameAs(scheduledExecutor)); } } diff --git a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusMetricNameMapperTest.java b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusMetricNameMapperTest.java deleted file mode 100644 index 6a677c361ef..00000000000 --- a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusMetricNameMapperTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.exporter.prometheus; - -import static io.opentelemetry.exporter.prometheus.TestConstants.DELTA_HISTOGRAM; -import static io.opentelemetry.exporter.prometheus.TestConstants.DOUBLE_GAUGE; -import static io.opentelemetry.exporter.prometheus.TestConstants.MONOTONIC_CUMULATIVE_LONG_SUM; -import static io.opentelemetry.exporter.prometheus.TestConstants.SUMMARY; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableMetricData; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiFunction; -import java.util.stream.Stream; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -class PrometheusMetricNameMapperTest { - - @Test - void prometheusMetricNameMapperCaching() { - AtomicInteger count = new AtomicInteger(); - BiFunction delegate = - (metricData, prometheusType) -> - String.join( - "_", - metricData.getName(), - prometheusType.name(), - Integer.toString(count.incrementAndGet())); - PrometheusMetricNameMapper mapper = new PrometheusMetricNameMapper(delegate); - - assertThat(mapper.apply(MONOTONIC_CUMULATIVE_LONG_SUM, PrometheusType.GAUGE)) - .isEqualTo("monotonic.cumulative.long.sum_GAUGE_1"); - assertThat(mapper.apply(MONOTONIC_CUMULATIVE_LONG_SUM, PrometheusType.GAUGE)) - .isEqualTo("monotonic.cumulative.long.sum_GAUGE_1"); - assertThat(mapper.apply(MONOTONIC_CUMULATIVE_LONG_SUM, PrometheusType.GAUGE)) - .isEqualTo("monotonic.cumulative.long.sum_GAUGE_1"); - assertThat(mapper.apply(MONOTONIC_CUMULATIVE_LONG_SUM, PrometheusType.GAUGE)) - .isEqualTo("monotonic.cumulative.long.sum_GAUGE_1"); - assertThat(mapper.apply(MONOTONIC_CUMULATIVE_LONG_SUM, PrometheusType.GAUGE)) - .isEqualTo("monotonic.cumulative.long.sum_GAUGE_1"); - assertThat(count).hasValue(1); - } - - @ParameterizedTest - @MethodSource("provideRawMetricDataForTest") - void metricNameSerializationTest(MetricData metricData, String expectedSerializedName) { - assertEquals( - expectedSerializedName, - PrometheusMetricNameMapper.INSTANCE.apply( - metricData, PrometheusType.forMetric(metricData))); - } - - private static Stream provideRawMetricDataForTest() { - return Stream.of( - // special case for gauge - Arguments.of(createSampleMetricData("sample", "1", PrometheusType.GAUGE), "sample_ratio"), - // special case for gauge with drop - metric unit should match "1" to be converted to - // "ratio" - Arguments.of( - createSampleMetricData("sample", "1{dropped}", PrometheusType.GAUGE), "sample"), - // Gauge without "1" as unit - Arguments.of(createSampleMetricData("sample", "unit", PrometheusType.GAUGE), "sample_unit"), - // special case with counter - Arguments.of( - createSampleMetricData("sample", "unit", PrometheusType.COUNTER), "sample_unit_total"), - // special case unit "1", but no gauge - "1" is dropped - Arguments.of(createSampleMetricData("sample", "1", PrometheusType.COUNTER), "sample_total"), - // units expressed as numbers other than 1 are retained - Arguments.of( - createSampleMetricData("sample", "2", PrometheusType.COUNTER), "sample_2_total"), - // metric name with unsupported characters - Arguments.of( - createSampleMetricData("s%%ple", "%/m", PrometheusType.SUMMARY), - "s_ple_percent_per_minute"), - // metric name with dropped portions - Arguments.of( - createSampleMetricData("s%%ple", "%/m", PrometheusType.SUMMARY), - "s_ple_percent_per_minute"), - // metric unit as a number other than 1 is not treated specially - Arguments.of( - createSampleMetricData("metric_name", "2", PrometheusType.SUMMARY), "metric_name_2"), - // metric unit is not appended if the name already contains the unit - Arguments.of( - createSampleMetricData("metric_name_total", "total", PrometheusType.COUNTER), - "metric_name_total"), - // metric unit is not appended if the name already contains the unit - special case for - // total with non-counter type - Arguments.of( - createSampleMetricData("metric_name_total", "total", PrometheusType.SUMMARY), - "metric_name_total"), - // metric unit not appended if present in metric name - special case for ratio - Arguments.of( - createSampleMetricData("metric_name_ratio", "1", PrometheusType.GAUGE), - "metric_name_ratio"), - // metric unit not appended if present in metric name - special case for ratio - unit not - // gauge - Arguments.of( - createSampleMetricData("metric_name_ratio", "1", PrometheusType.SUMMARY), - "metric_name_ratio"), - // metric unit is not appended if the name already contains the unit - unit can be anywhere - Arguments.of( - createSampleMetricData("metric_hertz", "hertz", PrometheusType.GAUGE), "metric_hertz"), - // metric unit is not appended if the name already contains the unit - applies to every unit - Arguments.of( - createSampleMetricData("metric_hertz_total", "hertz_total", PrometheusType.COUNTER), - "metric_hertz_total"), - // metric unit is not appended if the name already contains the unit - order matters - Arguments.of( - createSampleMetricData("metric_total_hertz", "hertz_total", PrometheusType.COUNTER), - "metric_total_hertz_hertz_total_total"), - // metric name cannot start with a number - Arguments.of( - createSampleMetricData("2_metric_name", "By", PrometheusType.SUMMARY), - "_metric_name_bytes")); - } - - static MetricData createSampleMetricData( - String metricName, String metricUnit, PrometheusType prometheusType) { - switch (prometheusType) { - case SUMMARY: - return ImmutableMetricData.createDoubleSummary( - SUMMARY.getResource(), - SUMMARY.getInstrumentationScopeInfo(), - metricName, - SUMMARY.getDescription(), - metricUnit, - SUMMARY.getSummaryData()); - case COUNTER: - return ImmutableMetricData.createLongSum( - MONOTONIC_CUMULATIVE_LONG_SUM.getResource(), - MONOTONIC_CUMULATIVE_LONG_SUM.getInstrumentationScopeInfo(), - metricName, - MONOTONIC_CUMULATIVE_LONG_SUM.getDescription(), - metricUnit, - MONOTONIC_CUMULATIVE_LONG_SUM.getLongSumData()); - case GAUGE: - return ImmutableMetricData.createDoubleGauge( - DOUBLE_GAUGE.getResource(), - DOUBLE_GAUGE.getInstrumentationScopeInfo(), - metricName, - DOUBLE_GAUGE.getDescription(), - metricUnit, - DOUBLE_GAUGE.getDoubleGaugeData()); - case HISTOGRAM: - return ImmutableMetricData.createDoubleHistogram( - DELTA_HISTOGRAM.getResource(), - DELTA_HISTOGRAM.getInstrumentationScopeInfo(), - metricName, - DELTA_HISTOGRAM.getDescription(), - metricUnit, - DELTA_HISTOGRAM.getHistogramData()); - } - throw new IllegalArgumentException(); - } -} diff --git a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusMetricReaderTest.java b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusMetricReaderTest.java new file mode 100644 index 00000000000..efb8f9cdbee --- /dev/null +++ b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusMetricReaderTest.java @@ -0,0 +1,1037 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.prometheus; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleUpDownCounter; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.time.TestClock; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter; +import io.prometheus.metrics.model.snapshots.HistogramSnapshot; +import io.prometheus.metrics.model.snapshots.HistogramSnapshot.HistogramDataPointSnapshot; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; +import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class PrometheusMetricReaderTest { + + private final TestClock testClock = TestClock.create(); + private String createdTimestamp; + private PrometheusMetricReader reader; + private Meter meter; + private Tracer tracer; + + @BeforeEach + public void setUp() { + this.testClock.setTime(Instant.ofEpochMilli((System.currentTimeMillis() / 100) * 100)); + this.createdTimestamp = convertTimestamp(testClock.now()); + this.reader = new PrometheusMetricReader(true); + this.meter = + SdkMeterProvider.builder() + .setClock(testClock) + .registerMetricReader(this.reader) + .setResource( + Resource.getDefault().toBuilder().put("telemetry.sdk.version", "1.x.x").build()) + .registerView( + InstrumentSelector.builder().setName("my.exponential.histogram").build(), + View.builder() + .setAggregation(Aggregation.base2ExponentialBucketHistogram()) + .build()) + .build() + .meterBuilder("test") + .build(); + this.tracer = + SdkTracerProvider.builder().setClock(testClock).build().tracerBuilder("test").build(); + } + + @Test + public void testLongCounterComplete() throws IOException { + LongCounter counter = + meter + .counterBuilder("requests.size") + .setDescription("some help text") + .setUnit("By") + .build(); + Span span1 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span1.makeCurrent()) { + counter.add(3, Attributes.builder().put("animal", "bear").build()); + } finally { + span1.end(); + } + Span span2 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span2.makeCurrent()) { + counter.add(2, Attributes.builder().put("animal", "mouse").build()); + } finally { + span2.end(); + } + assertCounterComplete(reader.collect(), span1, span2); + } + + @Test + public void testDoubleCounterComplete() throws IOException { + DoubleCounter counter = + meter + .counterBuilder("requests.size") + .setDescription("some help text") + .setUnit("By") + .ofDoubles() + .build(); + Span span1 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span1.makeCurrent()) { + counter.add(3.0, Attributes.builder().put("animal", "bear").build()); + } finally { + span1.end(); + } + Span span2 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span2.makeCurrent()) { + counter.add(2.0, Attributes.builder().put("animal", "mouse").build()); + } finally { + span2.end(); + } + assertCounterComplete(reader.collect(), span1, span2); + } + + private void assertCounterComplete(MetricSnapshots snapshots, Span span1, Span span2) + throws IOException { + String expected = + "" + + "# TYPE requests_size_bytes counter\n" + + "# UNIT requests_size_bytes bytes\n" + + "# HELP requests_size_bytes some help text\n" + + "requests_size_bytes_total{animal=\"bear\",otel_scope_name=\"test\"} 3.0 # {span_id=\"" + + span1.getSpanContext().getSpanId() + + "\",trace_id=\"" + + span1.getSpanContext().getTraceId() + + "\"} 3.0 \n" + + "requests_size_bytes_created{animal=\"bear\",otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "requests_size_bytes_total{animal=\"mouse\",otel_scope_name=\"test\"} 2.0 # {span_id=\"" + + span2.getSpanContext().getSpanId() + + "\",trace_id=\"" + + span2.getSpanContext().getTraceId() + + "\"} 2.0 \n" + + "requests_size_bytes_created{animal=\"mouse\",otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# EOF\n"; + assertMatches(expected, toOpenMetrics(snapshots)); + } + + @Test + public void testLongCounterMinimal() throws IOException { + LongCounter counter = meter.counterBuilder("requests").build(); + counter.add(2); + assertCounterMinimal(reader.collect()); + } + + @Test + public void testDoubleCounterMinimal() throws IOException { + DoubleCounter counter = meter.counterBuilder("requests").ofDoubles().build(); + counter.add(2.0); + assertCounterMinimal(reader.collect()); + } + + private void assertCounterMinimal(MetricSnapshots snapshots) throws IOException { + String expected = + "" + + "# TYPE requests counter\n" + + "requests_total{otel_scope_name=\"test\"} 2.0\n" + + "requests_created{otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# EOF\n"; + assertEquals(expected, toOpenMetrics(snapshots)); + } + + @Test + public void testLongUpDownCounterComplete() throws IOException { + LongUpDownCounter counter = + meter + .upDownCounterBuilder("queue.size") + .setDescription("some help text") + .setUnit("By") + .build(); + Span span1 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span1.makeCurrent()) { + counter.add(3, Attributes.builder().put("animal", "bear").build()); + } finally { + span1.end(); + } + Span span2 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span2.makeCurrent()) { + counter.add(2, Attributes.builder().put("animal", "mouse").build()); + } finally { + span2.end(); + } + assertUpDownCounterComplete(reader.collect(), span1, span2); + } + + @Test + public void testDoubleUpDownCounterComplete() throws IOException { + DoubleUpDownCounter counter = + meter + .upDownCounterBuilder("queue.size") + .setDescription("some help text") + .setUnit("By") + .ofDoubles() + .build(); + Span span1 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span1.makeCurrent()) { + counter.add(3.0, Attributes.builder().put("animal", "bear").build()); + } finally { + span1.end(); + } + Span span2 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span2.makeCurrent()) { + counter.add(2.0, Attributes.builder().put("animal", "mouse").build()); + } finally { + span2.end(); + } + assertUpDownCounterComplete(reader.collect(), span1, span2); + } + + private void assertUpDownCounterComplete(MetricSnapshots snapshots, Span span1, Span span2) + throws IOException { + String expected = + "" + + "# TYPE queue_size_bytes gauge\n" + + "# UNIT queue_size_bytes bytes\n" + + "# HELP queue_size_bytes some help text\n" + + "queue_size_bytes{animal=\"bear\",otel_scope_name=\"test\"} 3.0 # {span_id=\"" + + span1.getSpanContext().getSpanId() + + "\",trace_id=\"" + + span1.getSpanContext().getTraceId() + + "\"} 3.0 \n" + + "queue_size_bytes{animal=\"mouse\",otel_scope_name=\"test\"} 2.0 # {span_id=\"" + + span2.getSpanContext().getSpanId() + + "\",trace_id=\"" + + span2.getSpanContext().getTraceId() + + "\"} 2.0 \n" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# EOF\n"; + assertMatches(expected, toOpenMetrics(snapshots)); + } + + @Test + public void testLongUpDownCounterMinimal() throws IOException { + LongUpDownCounter counter = meter.upDownCounterBuilder("users.active").build(); + counter.add(27); + assertUpDownCounterMinimal(reader.collect()); + } + + @Test + public void testDoubleUpDownCounterMinimal() throws IOException { + DoubleUpDownCounter counter = meter.upDownCounterBuilder("users.active").ofDoubles().build(); + counter.add(27.0); + assertUpDownCounterMinimal(reader.collect()); + } + + private void assertUpDownCounterMinimal(MetricSnapshots snapshots) throws IOException { + String expected = + "" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# TYPE users_active gauge\n" + + "users_active{otel_scope_name=\"test\"} 27.0\n" + + "# EOF\n"; + assertEquals(expected, toOpenMetrics(snapshots)); + } + + @Test + public void testLongGaugeComplete() throws IOException { + meter + .gaugeBuilder("temperature") + .setUnit("Cel") + .setDescription("help text") + .ofLongs() + .buildWithCallback( + m -> { + m.record(23, Attributes.builder().put("location", "inside").build()); + m.record(17, Attributes.builder().put("location", "outside").build()); + }); + assertGaugeComplete(reader.collect()); + } + + @Test + public void testDoubleGaugeComplete() throws IOException { + meter + .gaugeBuilder("temperature") + .setUnit("Cel") + .setDescription("help text") + .buildWithCallback( + m -> { + m.record(23.0, Attributes.builder().put("location", "inside").build()); + m.record(17.0, Attributes.builder().put("location", "outside").build()); + }); + assertGaugeComplete(reader.collect()); + } + + private void assertGaugeComplete(MetricSnapshots snapshots) throws IOException { + String expected = + "" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# TYPE temperature_celsius gauge\n" + + "# UNIT temperature_celsius celsius\n" + + "# HELP temperature_celsius help text\n" + + "temperature_celsius{location=\"inside\",otel_scope_name=\"test\"} 23.0\n" + + "temperature_celsius{location=\"outside\",otel_scope_name=\"test\"} 17.0\n" + + "# EOF\n"; + assertEquals(expected, toOpenMetrics(snapshots)); + } + + @Test + public void testLongGaugeMinimal() throws IOException { + meter.gaugeBuilder("my_gauge").ofLongs().buildWithCallback(m -> m.record(2)); + assertGaugeMinimal(reader.collect()); + } + + @Test + public void testDoubleGaugeMinimal() throws IOException { + meter.gaugeBuilder("my_gauge").buildWithCallback(m -> m.record(2.0)); + assertGaugeMinimal(reader.collect()); + } + + private void assertGaugeMinimal(MetricSnapshots snapshots) throws IOException { + String expected = + "" + + "# TYPE my_gauge gauge\n" + + "my_gauge{otel_scope_name=\"test\"} 2.0\n" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# EOF\n"; + assertEquals(expected, toOpenMetrics(snapshots)); + } + + @Test + public void testLongHistogramComplete() throws IOException { + LongHistogram histogram = + meter + .histogramBuilder("request.size") + .setDescription("some help text") + .setUnit("By") + .ofLongs() + .build(); + Span span1 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span1.makeCurrent()) { + histogram.record(173, Attributes.builder().put("animal", "bear").build()); + } finally { + span1.end(); + } + Span span2 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span2.makeCurrent()) { + histogram.record(400, Attributes.builder().put("animal", "bear").build()); + } finally { + span1.end(); + } + Span span3 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span3.makeCurrent()) { + histogram.record(204, Attributes.builder().put("animal", "mouse").build()); + } finally { + span3.end(); + } + assertHistogramComplete(reader.collect(), span1, span2, span3); + } + + @Test + public void testDoubleHistogramComplete() throws IOException { + DoubleHistogram histogram = + meter + .histogramBuilder("request.size") + .setDescription("some help text") + .setUnit("By") + .build(); + Span span1 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span1.makeCurrent()) { + histogram.record(173.0, Attributes.builder().put("animal", "bear").build()); + } finally { + span1.end(); + } + Span span2 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span2.makeCurrent()) { + histogram.record(400.0, Attributes.builder().put("animal", "bear").build()); + } finally { + span1.end(); + } + Span span3 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span3.makeCurrent()) { + histogram.record(204.0, Attributes.builder().put("animal", "mouse").build()); + } finally { + span3.end(); + } + assertHistogramComplete(reader.collect(), span1, span2, span3); + } + + private void assertHistogramComplete( + MetricSnapshots snapshots, Span span1, Span span2, Span span3) throws IOException { + String expected = + "" + + "# TYPE request_size_bytes histogram\n" + + "# UNIT request_size_bytes bytes\n" + + "# HELP request_size_bytes some help text\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"0.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"5.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"10.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"25.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"50.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"75.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"100.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"250.0\"} 1 # {span_id=\"" + + span1.getSpanContext().getSpanId() + + "\",trace_id=\"" + + span1.getSpanContext().getTraceId() + + "\"} 173.0 \n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"500.0\"} 2 # {span_id=\"" + + span2.getSpanContext().getSpanId() + + "\",trace_id=\"" + + span2.getSpanContext().getTraceId() + + "\"} 400.0 \n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"750.0\"} 2\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"1000.0\"} 2\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"2500.0\"} 2\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"5000.0\"} 2\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"7500.0\"} 2\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"10000.0\"} 2\n" + + "request_size_bytes_bucket{animal=\"bear\",otel_scope_name=\"test\",le=\"+Inf\"} 2\n" + + "request_size_bytes_count{animal=\"bear\",otel_scope_name=\"test\"} 2\n" + + "request_size_bytes_sum{animal=\"bear\",otel_scope_name=\"test\"} 573.0\n" + + "request_size_bytes_created{animal=\"bear\",otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"0.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"5.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"10.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"25.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"50.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"75.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"100.0\"} 0\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"250.0\"} 1 # {span_id=\"" + + span3.getSpanContext().getSpanId() + + "\",trace_id=\"" + + span3.getSpanContext().getTraceId() + + "\"} 204.0 \n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"500.0\"} 1\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"750.0\"} 1\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"1000.0\"} 1\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"2500.0\"} 1\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"5000.0\"} 1\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"7500.0\"} 1\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"10000.0\"} 1\n" + + "request_size_bytes_bucket{animal=\"mouse\",otel_scope_name=\"test\",le=\"+Inf\"} 1\n" + + "request_size_bytes_count{animal=\"mouse\",otel_scope_name=\"test\"} 1\n" + + "request_size_bytes_sum{animal=\"mouse\",otel_scope_name=\"test\"} 204.0\n" + + "request_size_bytes_created{animal=\"mouse\",otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# EOF\n"; + assertMatches(expected, toOpenMetrics(snapshots)); + } + + @Test + public void testLongHistogramMinimal() throws IOException { + LongHistogram histogram = meter.histogramBuilder("request.size").ofLongs().build(); + histogram.record(173); + histogram.record(173); + histogram.record(100_000); + assertHistogramMinimal(reader.collect()); + } + + @Test + public void testDoubleHistogramMinimal() throws IOException { + DoubleHistogram histogram = meter.histogramBuilder("request.size").build(); + histogram.record(173.0); + histogram.record(173.0); + histogram.record(100_000.0); + assertHistogramMinimal(reader.collect()); + } + + private void assertHistogramMinimal(MetricSnapshots snapshots) throws IOException { + String expected = + "" + + "# TYPE request_size histogram\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"0.0\"} 0\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"5.0\"} 0\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"10.0\"} 0\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"25.0\"} 0\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"50.0\"} 0\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"75.0\"} 0\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"100.0\"} 0\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"250.0\"} 2\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"500.0\"} 2\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"750.0\"} 2\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"1000.0\"} 2\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"2500.0\"} 2\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"5000.0\"} 2\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"7500.0\"} 2\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"10000.0\"} 2\n" + + "request_size_bucket{otel_scope_name=\"test\",le=\"+Inf\"} 3\n" + + "request_size_count{otel_scope_name=\"test\"} 3\n" + + "request_size_sum{otel_scope_name=\"test\"} 100346.0\n" + + "request_size_created{otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# EOF\n"; + assertEquals(expected, toOpenMetrics(snapshots)); + } + + @Test + @Disabled("disabled until #6010 is fixed") + public void testExponentialLongHistogramComplete() throws IOException { + LongHistogram histogram = + meter + .histogramBuilder("my.exponential.histogram") + .setDescription("some help text") + .setUnit("By") + .ofLongs() + .build(); + Span span1 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span1.makeCurrent()) { + histogram.record(7, Attributes.builder().put("animal", "bear").build()); + } finally { + span1.end(); + } + histogram.record(0, Attributes.builder().put("animal", "bear").build()); + Span span2 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span2.makeCurrent()) { + histogram.record(3, Attributes.builder().put("animal", "mouse").build()); + } finally { + span2.end(); + } + assertExponentialHistogramComplete(reader.collect(), span1, span2); + } + + @Test + public void testExponentialDoubleHistogramComplete() throws IOException { + DoubleHistogram histogram = + meter + .histogramBuilder("my.exponential.histogram") + .setDescription("some help text") + .setUnit("By") + .build(); + Span span1 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span1.makeCurrent()) { + histogram.record(7.0, Attributes.builder().put("animal", "bear").build()); + } finally { + span1.end(); + } + histogram.record(0.0, Attributes.builder().put("animal", "bear").build()); + Span span2 = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span2.makeCurrent()) { + histogram.record(3.0, Attributes.builder().put("animal", "mouse").build()); + } finally { + span2.end(); + } + assertExponentialHistogramComplete(reader.collect(), span1, span2); + } + + private void assertExponentialHistogramComplete(MetricSnapshots snapshots, Span span1, Span span2) + throws IOException { + String expected = + "" + + "name: \"my_exponential_histogram_bytes\"\n" + + "help: \"some help text\"\n" + + "type: HISTOGRAM\n" + + "metric {\n" + + " label {\n" + + " name: \"animal\"\n" + + " value: \"bear\"\n" + + " }\n" + + " label {\n" + + " name: \"otel_scope_name\"\n" + + " value: \"test\"\n" + + " }\n" + + " histogram {\n" + + " sample_count: 2\n" + + " sample_sum: 7.0\n" + + " bucket {\n" + + " cumulative_count: 2\n" + + " upper_bound: Infinity\n" + + " exemplar {\n" + + " label {\n" + + " name: \"span_id\"\n" + + " value: \"" + + span1.getSpanContext().getSpanId() + + "\"\n" + + " }\n" + + " label {\n" + + " name: \"trace_id\"\n" + + " value: \"" + + span1.getSpanContext().getTraceId() + + "\"\n" + + " }\n" + + " value: 7.0\n" + + " timestamp {\n" + + " seconds: \n" + + " nanos: \n" + + " }\n" + + " }\n" + + " }\n" + + " schema: 8\n" + + " zero_threshold: 0.0\n" + + " zero_count: 1\n" + + " positive_span {\n" + + " offset: 719\n" + + " length: 1\n" + + " }\n" + + " positive_delta: 1\n" + + " }\n" + + "}\n" + + "metric {\n" + + " label {\n" + + " name: \"animal\"\n" + + " value: \"mouse\"\n" + + " }\n" + + " label {\n" + + " name: \"otel_scope_name\"\n" + + " value: \"test\"\n" + + " }\n" + + " histogram {\n" + + " sample_count: 1\n" + + " sample_sum: 3.0\n" + + " bucket {\n" + + " cumulative_count: 1\n" + + " upper_bound: Infinity\n" + + " exemplar {\n" + + " label {\n" + + " name: \"span_id\"\n" + + " value: \"" + + span2.getSpanContext().getSpanId() + + "\"\n" + + " }\n" + + " label {\n" + + " name: \"trace_id\"\n" + + " value: \"" + + span2.getSpanContext().getTraceId() + + "\"\n" + + " }\n" + + " value: 3.0\n" + + " timestamp {\n" + + " seconds: \n" + + " nanos: \n" + + " }\n" + + " }\n" + + " }\n" + + " schema: 8\n" + + " zero_threshold: 0.0\n" + + " zero_count: 0\n" + + " positive_span {\n" + + " offset: 406\n" + + " length: 1\n" + + " }\n" + + " positive_delta: 1\n" + + " }\n" + + "}\n" + + "name: \"target_info\"\n" + + "type: GAUGE\n" + + "metric {\n" + + " label {\n" + + " name: \"service_name\"\n" + + " value: \"unknown_service:java\"\n" + + " }\n" + + " label {\n" + + " name: \"telemetry_sdk_language\"\n" + + " value: \"java\"\n" + + " }\n" + + " label {\n" + + " name: \"telemetry_sdk_name\"\n" + + " value: \"opentelemetry\"\n" + + " }\n" + + " label {\n" + + " name: \"telemetry_sdk_version\"\n" + + " value: \"1.x.x\"\n" + + " }\n" + + " gauge {\n" + + " value: 1.0\n" + + " }\n" + + "}\n"; + assertMatches(expected, toPrometheusProtobuf(snapshots)); + } + + @Test + public void testExponentialLongHistogramMinimal() throws IOException { + LongHistogram histogram = meter.histogramBuilder("my.exponential.histogram").ofLongs().build(); + histogram.record(1, Attributes.builder().put("animal", "bear").build()); + assertExponentialHistogramMinimal(reader.collect()); + } + + @Test + public void testExponentialDoubleHistogramMinimal() throws IOException { + DoubleHistogram histogram = meter.histogramBuilder("my.exponential.histogram").build(); + histogram.record(1.0, Attributes.builder().put("animal", "bear").build()); + assertExponentialHistogramMinimal(reader.collect()); + } + + private void assertExponentialHistogramMinimal(MetricSnapshots snapshots) throws IOException { + String expected = + "" + + "name: \"my_exponential_histogram\"\n" + + "help: \"\"\n" + + "type: HISTOGRAM\n" + + "metric {\n" + + " label {\n" + + " name: \"animal\"\n" + + " value: \"bear\"\n" + + " }\n" + + " label {\n" + + " name: \"otel_scope_name\"\n" + + " value: \"test\"\n" + + " }\n" + + " histogram {\n" + + " sample_count: 1\n" + + " sample_sum: 1.0\n" + + " schema: 8\n" + + " zero_threshold: 0.0\n" + + " zero_count: 0\n" + + " positive_span {\n" + + " offset: 0\n" + + " length: 1\n" + + " }\n" + + " positive_delta: 1\n" + + " }\n" + + "}\n" + + "name: \"target_info\"\n" + + "type: GAUGE\n" + + "metric {\n" + + " label {\n" + + " name: \"service_name\"\n" + + " value: \"unknown_service:java\"\n" + + " }\n" + + " label {\n" + + " name: \"telemetry_sdk_language\"\n" + + " value: \"java\"\n" + + " }\n" + + " label {\n" + + " name: \"telemetry_sdk_name\"\n" + + " value: \"opentelemetry\"\n" + + " }\n" + + " label {\n" + + " name: \"telemetry_sdk_version\"\n" + + " value: \"1.x.x\"\n" + + " }\n" + + " gauge {\n" + + " value: 1.0\n" + + " }\n" + + "}\n"; + assertMatches(expected, toPrometheusProtobuf(snapshots)); + } + + @Test + public void testExponentialHistogramBucketConversion() { + Random random = new Random(); + for (int i = 0; i < 100_000; i++) { + int otelScale = random.nextInt(24) - 4; + int prometheusScale = Math.min(otelScale, 8); + PrometheusMetricReader reader = new PrometheusMetricReader(true); + Meter meter = + SdkMeterProvider.builder() + .registerMetricReader(reader) + .registerView( + InstrumentSelector.builder().setName("my.exponential.histogram").build(), + View.builder() + .setAggregation(Aggregation.base2ExponentialBucketHistogram(160, otelScale)) + .build()) + .build() + .meterBuilder("test") + .build(); + int orderOfMagnitude = random.nextInt(18) - 9; + double observation = random.nextDouble() * Math.pow(10, orderOfMagnitude); + if (observation == 0) { + continue; + } + DoubleHistogram histogram = meter.histogramBuilder("my.exponential.histogram").build(); + histogram.record(observation); + MetricSnapshots snapshots = reader.collect(); + HistogramSnapshot snapshot = (HistogramSnapshot) snapshots.get(0); + HistogramDataPointSnapshot dataPoint = snapshot.getDataPoints().get(0); + Assertions.assertEquals(prometheusScale, dataPoint.getNativeSchema()); + NativeHistogramBuckets buckets = dataPoint.getNativeBucketsForPositiveValues(); + Assertions.assertEquals(1, buckets.size()); + int index = buckets.getBucketIndex(0); + double base = Math.pow(2, Math.pow(2, -prometheusScale)); + double lowerBound = Math.pow(base, index - 1); + double upperBound = Math.pow(base, index); + Assertions.assertTrue(lowerBound < observation); + Assertions.assertTrue(upperBound >= observation); + } + } + + @Test + @SuppressWarnings("SystemOut") + public void testExponentialLongHistogramScaleDown() throws IOException { + // The following histogram will have the default scale, which is 20. + DoubleHistogram histogram = meter.histogramBuilder("my.exponential.histogram").build(); + double base = Math.pow(2, Math.pow(2, -20)); + int i; + for (i = 0; i < Math.pow(2, 12); i++) { + histogram.record(Math.pow(base, i)); // one observation per bucket + } + for (int j = 0; j < 10; j++) { + histogram.record(Math.pow(base, i + 2 * j)); // few empty buckets between the observations + } + MetricSnapshots snapshots = reader.collect(); + HistogramSnapshot snapshot = (HistogramSnapshot) snapshots.get(0); + HistogramDataPointSnapshot dataPoint = snapshot.getDataPoints().get(0); + Assertions.assertEquals(8, dataPoint.getNativeSchema()); // scaled down from 20 to 8. + NativeHistogramBuckets buckets = dataPoint.getNativeBucketsForPositiveValues(); + Assertions.assertEquals(3, buckets.size()); + // In bucket 0 we have exactly one observation: the value 1.0 + Assertions.assertEquals(0, buckets.getBucketIndex(0)); + Assertions.assertEquals(1, buckets.getCount(0)); + // In bucket 1 we have 4095 observations + Assertions.assertEquals(1, buckets.getBucketIndex(1)); + Assertions.assertEquals(4095, buckets.getCount(1)); + // In bucket 2 we have 10 observations (despite the empty buckets all observations fall into the + // same bucket at scale 8) + Assertions.assertEquals(2, buckets.getBucketIndex(2)); + Assertions.assertEquals(10, buckets.getCount(2)); + } + + @Test + public void testNameSuffix() throws IOException { + LongCounter unitAndTotal = + meter.counterBuilder("request.duration.seconds.total").setUnit("s").build(); + unitAndTotal.add(1); + LongCounter unitOnly = meter.counterBuilder("response.duration.seconds").setUnit("s").build(); + unitOnly.add(2); + LongCounter totalOnly = meter.counterBuilder("processing.duration.total").setUnit("s").build(); + totalOnly.add(3); + LongCounter noSuffix = meter.counterBuilder("queue.time").setUnit("s").build(); + noSuffix.add(4); + String expected = + "" + + "# TYPE processing_duration_seconds counter\n" + + "# UNIT processing_duration_seconds seconds\n" + + "processing_duration_seconds_total{otel_scope_name=\"test\"} 3.0\n" + + "processing_duration_seconds_created{otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "# TYPE queue_time_seconds counter\n" + + "# UNIT queue_time_seconds seconds\n" + + "queue_time_seconds_total{otel_scope_name=\"test\"} 4.0\n" + + "queue_time_seconds_created{otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "# TYPE request_duration_seconds counter\n" + + "# UNIT request_duration_seconds seconds\n" + + "request_duration_seconds_total{otel_scope_name=\"test\"} 1.0\n" + + "request_duration_seconds_created{otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "# TYPE response_duration_seconds counter\n" + + "# UNIT response_duration_seconds seconds\n" + + "response_duration_seconds_total{otel_scope_name=\"test\"} 2.0\n" + + "response_duration_seconds_created{otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# EOF\n"; + assertEquals(expected, toOpenMetrics(reader.collect())); + } + + @Test + public void testNameSuffixUnit() throws IOException { + LongCounter counter = meter.counterBuilder("request.duration.seconds").setUnit("s").build(); + counter.add(1); + String expected = + "" + + "# TYPE request_duration_seconds counter\n" + + "# UNIT request_duration_seconds seconds\n" + + "request_duration_seconds_total{otel_scope_name=\"test\"} 1.0\n" + + "request_duration_seconds_created{otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# EOF\n"; + assertEquals(expected, toOpenMetrics(reader.collect())); + } + + @Test + public void testIllegalCharacters() throws IOException { + LongCounter counter = meter.counterBuilder("prod/request.count").build(); + counter.add(1, Attributes.builder().put("user-count", 30).build()); + String expected = + "" + + "# TYPE prod_request_count counter\n" + + "prod_request_count_total{otel_scope_name=\"test\",user_count=\"30\"} 1.0\n" + + "prod_request_count_created{otel_scope_name=\"test\",user_count=\"30\"} " + + createdTimestamp + + "\n" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# EOF\n"; + assertEquals(expected, toOpenMetrics(reader.collect())); + } + + @Test + public void testCreatedTimestamp() throws IOException { + + LongCounter counter = meter.counterBuilder("requests").build(); + testClock.advance(Duration.ofMillis(1)); + counter.add(3, Attributes.builder().put("animal", "bear").build()); + testClock.advance(Duration.ofMillis(1)); + counter.add(2, Attributes.builder().put("animal", "mouse").build()); + testClock.advance(Duration.ofMillis(1)); + + // There is a curious difference between Prometheus and OpenTelemetry: + // In Prometheus metrics the _created timestamp is per data point, + // i.e. the _created timestamp says when this specific set of label values + // was first observed. + // In the OTel Java SDK the _created timestamp is the initialization time + // of the SdkMeterProvider, i.e. all data points will have the same _created timestamp. + // So we expect the _created timestamp to be the start time of the application, + // not the timestamp when the counter or an individual data point was created. + String expected = + "" + + "# TYPE requests counter\n" + + "requests_total{animal=\"bear\",otel_scope_name=\"test\"} 3.0\n" + + "requests_created{animal=\"bear\",otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "requests_total{animal=\"mouse\",otel_scope_name=\"test\"} 2.0\n" + + "requests_created{animal=\"mouse\",otel_scope_name=\"test\"} " + + createdTimestamp + + "\n" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# EOF\n"; + + assertEquals(expected, toOpenMetrics(reader.collect())); + } + + @Test + public void testOtelScopeComplete() throws IOException { + // There is currently no API for adding scope attributes. + // However, we can at least test the otel_scope_version attribute. + Meter meter = + SdkMeterProvider.builder() + .setClock(testClock) + .registerMetricReader(this.reader) + .setResource( + Resource.getDefault().toBuilder().put("telemetry.sdk.version", "1.x.x").build()) + .build() + .meterBuilder("test-scope") + .setInstrumentationVersion("a.b.c") + .build(); + LongCounter counter = meter.counterBuilder("test.count").build(); + counter.add(1); + String expected = + "" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# TYPE test_count counter\n" + + "test_count_total{otel_scope_name=\"test-scope\",otel_scope_version=\"a.b.c\"} 1.0\n" + + "test_count_created{otel_scope_name=\"test-scope\",otel_scope_version=\"a.b.c\"} " + + createdTimestamp + + "\n" + + "# EOF\n"; + assertEquals(expected, toOpenMetrics(reader.collect())); + } + + @Test + public void testOtelScopeDisabled() throws IOException { + PrometheusMetricReader reader = new PrometheusMetricReader(false); + Meter meter = + SdkMeterProvider.builder() + .setClock(testClock) + .registerMetricReader(reader) + .setResource( + Resource.getDefault().toBuilder().put("telemetry.sdk.version", "1.x.x").build()) + .build() + .meterBuilder("test-scope") + .setInstrumentationVersion("a.b.c") + .build(); + LongCounter counter = meter.counterBuilder("test.count").build(); + counter.add(1); + String expected = + "" + + "# TYPE target info\n" + + "target_info{service_name=\"unknown_service:java\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"opentelemetry\",telemetry_sdk_version=\"1.x.x\"} 1\n" + + "# TYPE test_count counter\n" + + "test_count_total 1.0\n" + + "test_count_created " + + createdTimestamp + + "\n" + + "# EOF\n"; + assertEquals(expected, toOpenMetrics(reader.collect())); + } + + /** + * Unfortunately there is no easy way to use {@link TestClock} for Exemplar timestamps. The + * following is like {@code assertEquals()}, but {@code } matches arbitrary timestamps. + */ + @SuppressWarnings("MethodCanBeStatic") + private void assertMatches(String expected, String actual) { + String[] parts = expected.split(Pattern.quote("")); + StringBuilder regex = new StringBuilder(); + for (int i = 0; i < parts.length; i++) { + regex.append(Pattern.quote(parts[i])); + if (i <= parts.length - 2) { + regex.append("[0-9]+(\\.[0-9]+)?"); + } + } + Assertions.assertTrue( + Pattern.matches(regex.toString(), actual), "Expected: " + expected + "\nActual: " + actual); + } + + @SuppressWarnings({"MethodCanBeStatic", "DefaultCharset"}) + private String toOpenMetrics(MetricSnapshots snapshots) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + OpenMetricsTextFormatWriter writer = new OpenMetricsTextFormatWriter(true, true); + writer.write(out, snapshots); + return out.toString(); + } + + @SuppressWarnings({"MethodCanBeStatic", "DefaultCharset"}) + private String toPrometheusProtobuf(MetricSnapshots snapshots) throws IOException { + PrometheusProtobufWriter writer = new PrometheusProtobufWriter(); + return writer.toDebugString(snapshots); + } + + @SuppressWarnings("MethodCanBeStatic") + private String convertTimestamp(long nanoTime) { + String millis = Long.toString(TimeUnit.NANOSECONDS.toMillis(nanoTime)); + return millis.substring(0, millis.length() - 3) + "." + millis.substring(millis.length() - 3); + } +} diff --git a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusUnitsHelperTest.java b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusUnitsHelperTest.java deleted file mode 100644 index 2a8d01f0ce3..00000000000 --- a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/PrometheusUnitsHelperTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.exporter.prometheus; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.stream.Stream; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -class PrometheusUnitsHelperTest { - - @ParameterizedTest - @MethodSource("providePrometheusOTelUnitEquivalentPairs") - public void testPrometheusUnitEquivalency(String otlpUnit, String prometheusUnit) { - assertEquals(prometheusUnit, PrometheusUnitsHelper.getEquivalentPrometheusUnit(otlpUnit)); - } - - private static Stream providePrometheusOTelUnitEquivalentPairs() { - return Stream.of( - // Simple expansion - storage Bytes - Arguments.of("By", "bytes"), - // Simple expansion - storage KBy - Arguments.of("KBy", "kilobytes"), - // Simple expansion - storage MBy - Arguments.of("MBy", "megabytes"), - // Simple expansion - storage GBy - Arguments.of("GBy", "gigabytes"), - // Simple expansion - storage TBy - Arguments.of("TBy", "terabytes"), - // Simple expansion - storage KiBy - Arguments.of("KiBy", "kibibytes"), - // Simple expansion - storage MiBy - Arguments.of("MiBy", "mebibytes"), - // Simple expansion - storage GiBy - Arguments.of("GiBy", "gibibytes"), - // Simple expansion - storage TiBy - Arguments.of("TiBy", "tibibytes"), - // Simple expansion - Time unit d - Arguments.of("d", "days"), - // Simple expansion - Time unit h - Arguments.of("h", "hours"), - // Simple expansion - Time unit s - Arguments.of("s", "seconds"), - // Simple expansion - Time unit ms - Arguments.of("ms", "milliseconds"), - // Simple expansion - Time unit us - Arguments.of("us", "microseconds"), - // Simple expansion - Time unit ns - Arguments.of("ns", "nanoseconds"), - // Simple expansion - Time unit min - Arguments.of("min", "minutes"), - // Simple expansion - special symbol - % - Arguments.of("%", "percent"), - // Simple expansion - frequency - Arguments.of("Hz", "hertz"), - // Simple expansion - temperature - Arguments.of("Cel", "celsius"), - // Unit not found - Case sensitive - Arguments.of("S", "S"), - // Special case - 1 - Arguments.of("1", ""), - // Special Case - Drop metric units in {} - Arguments.of("{packets}", ""), - // Special Case - Dropped metric units only in {} - Arguments.of("{packets}V", "volts"), - // Special Case - Dropped metric units with 'per' unit handling applicable - Arguments.of("{scanned}/{returned}", ""), - // Special Case - Dropped metric units with 'per' unit handling applicable - Arguments.of("{objects}/s", "per_second"), - // Units expressing rate - 'per' units, both units expanded - Arguments.of("m/s", "meters_per_second"), - // Units expressing rate - per minute - Arguments.of("m/m", "meters_per_minute"), - // Units expressing rate - per day - Arguments.of("A/d", "amperes_per_day"), - // Units expressing rate - per week - Arguments.of("W/w", "watts_per_week"), - // Units expressing rate - per month - Arguments.of("J/mo", "joules_per_month"), - // Units expressing rate - per year - Arguments.of("TBy/y", "terabytes_per_year"), - // Units expressing rate - 'per' units, both units unknown - Arguments.of("v/v", "v_per_v"), - // Units expressing rate - 'per' units, first unit unknown - Arguments.of("km/h", "km_per_hour"), - // Units expressing rate - 'per' units, 'per' unit unknown - Arguments.of("g/g", "grams_per_g"), - // Misc - unit containing known abbreviations improperly formatted - Arguments.of("watts_W", "watts_W"), - // Unsupported symbols - Arguments.of("°F", "F"), - // Unsupported symbols - multiple - Arguments.of("unit+=.:,!* & #unused", "unit_unused"), - // Unsupported symbols - 'per' units - Arguments.of("__test $/°C", "test_per_C"), - // Unsupported symbols - whitespace - Arguments.of("\t", ""), - // Null unit - Arguments.of(null, null), - // Misc - unit cleanup - no case match special char - Arguments.of("$1000", "1000")); - } -} diff --git a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/SerializerTest.java b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/SerializerTest.java deleted file mode 100644 index b76c390eab7..00000000000 --- a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/SerializerTest.java +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.exporter.prometheus; - -import static io.opentelemetry.exporter.prometheus.TestConstants.CUMULATIVE_HISTOGRAM_NO_ATTRIBUTES; -import static io.opentelemetry.exporter.prometheus.TestConstants.CUMULATIVE_HISTOGRAM_SINGLE_ATTRIBUTE; -import static io.opentelemetry.exporter.prometheus.TestConstants.DELTA_DOUBLE_SUM; -import static io.opentelemetry.exporter.prometheus.TestConstants.DELTA_HISTOGRAM; -import static io.opentelemetry.exporter.prometheus.TestConstants.DELTA_LONG_SUM; -import static io.opentelemetry.exporter.prometheus.TestConstants.DOUBLE_GAUGE; -import static io.opentelemetry.exporter.prometheus.TestConstants.DOUBLE_GAUGE_COLLIDING_ATTRIBUTES; -import static io.opentelemetry.exporter.prometheus.TestConstants.DOUBLE_GAUGE_MULTIPLE_ATTRIBUTES; -import static io.opentelemetry.exporter.prometheus.TestConstants.DOUBLE_GAUGE_NO_ATTRIBUTES; -import static io.opentelemetry.exporter.prometheus.TestConstants.LONG_GAUGE; -import static io.opentelemetry.exporter.prometheus.TestConstants.MONOTONIC_CUMULATIVE_DOUBLE_SUM; -import static io.opentelemetry.exporter.prometheus.TestConstants.MONOTONIC_CUMULATIVE_DOUBLE_SUM_WITH_SUFFIX_TOTAL; -import static io.opentelemetry.exporter.prometheus.TestConstants.MONOTONIC_CUMULATIVE_LONG_SUM; -import static io.opentelemetry.exporter.prometheus.TestConstants.NON_MONOTONIC_CUMULATIVE_DOUBLE_SUM; -import static io.opentelemetry.exporter.prometheus.TestConstants.NON_MONOTONIC_CUMULATIVE_LONG_SUM; -import static io.opentelemetry.exporter.prometheus.TestConstants.SUMMARY; -import static org.assertj.core.api.Assertions.assertThat; - -import io.github.netmikey.logunit.api.LogCapturer; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.internal.testing.slf4j.SuppressLogger; -import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.metrics.data.AggregationTemporality; -import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableDoublePointData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableMetricData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableSumData; -import io.opentelemetry.sdk.resources.Resource; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.function.BiConsumer; -import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -class SerializerTest { - - @RegisterExtension - private final LogCapturer logCapturer = - LogCapturer.create().captureForLogger(Serializer.class.getName()); - - @Test - void prometheus004() { - // Same output as prometheus client library except for these changes which are compatible with - // Prometheus - // TYPE / HELP line order reversed - // Attributes do not end in trailing comma - assertThat( - serialize004( - MONOTONIC_CUMULATIVE_DOUBLE_SUM, - MONOTONIC_CUMULATIVE_DOUBLE_SUM_WITH_SUFFIX_TOTAL, - NON_MONOTONIC_CUMULATIVE_DOUBLE_SUM, - DELTA_DOUBLE_SUM, // Deltas are dropped - MONOTONIC_CUMULATIVE_LONG_SUM, - NON_MONOTONIC_CUMULATIVE_LONG_SUM, - DELTA_LONG_SUM, // Deltas are dropped - DOUBLE_GAUGE, - LONG_GAUGE, - SUMMARY, - DELTA_HISTOGRAM, // Deltas are dropped - CUMULATIVE_HISTOGRAM_NO_ATTRIBUTES, - CUMULATIVE_HISTOGRAM_SINGLE_ATTRIBUTE, - DOUBLE_GAUGE_NO_ATTRIBUTES, - DOUBLE_GAUGE_MULTIPLE_ATTRIBUTES, - DOUBLE_GAUGE_COLLIDING_ATTRIBUTES)) - .isEqualTo( - "# TYPE target info\n" - + "# HELP target Target metadata\n" - + "target_info{kr=\"vr\"} 1\n" - + "# TYPE otel_scope_info info\n" - + "# HELP otel_scope_info Scope metadata\n" - + "otel_scope_info{otel_scope_name=\"full\",otel_scope_version=\"version\",ks=\"vs\"} 1\n" - + "# TYPE monotonic_cumulative_double_sum_seconds_total counter\n" - + "# HELP monotonic_cumulative_double_sum_seconds_total description\n" - + "monotonic_cumulative_double_sum_seconds_total{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"mcds\"} 5.0 1633950672000\n" - + "# TYPE monotonic_cumulative_double_sum_suffix_seconds_total counter\n" - + "# HELP monotonic_cumulative_double_sum_suffix_seconds_total description\n" - + "monotonic_cumulative_double_sum_suffix_seconds_total{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"mcds\"} 5.0 1633950672000\n" - + "# TYPE non_monotonic_cumulative_double_sum_seconds gauge\n" - + "# HELP non_monotonic_cumulative_double_sum_seconds description\n" - + "non_monotonic_cumulative_double_sum_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"nmcds\"} 5.0 1633950672000\n" - + "# TYPE monotonic_cumulative_long_sum_seconds_total counter\n" - + "# HELP monotonic_cumulative_long_sum_seconds_total unused\n" - + "monotonic_cumulative_long_sum_seconds_total{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"mcls\"} 5.0 1633950672000\n" - + "# TYPE non_monotonic_cumulative_long_sum_seconds gauge\n" - + "# HELP non_monotonic_cumulative_long_sum_seconds unused\n" - + "non_monotonic_cumulative_long_sum_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"nmcls\"} 5.0 1633950672000\n" - + "# TYPE double_gauge_seconds gauge\n" - + "# HELP double_gauge_seconds unused\n" - + "double_gauge_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"dg\"} 5.0 1633950672000\n" - + "# TYPE long_gauge_seconds gauge\n" - + "# HELP long_gauge_seconds unused\n" - + "long_gauge_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"lg\"} 5.0 1633950672000\n" - + "# TYPE summary_seconds summary\n" - + "# HELP summary_seconds unused\n" - + "summary_seconds_count{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"s\"} 5.0 1633950672000\n" - + "summary_seconds_sum{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"s\"} 7.0 1633950672000\n" - + "summary_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"s\",quantile=\"0.9\"} 0.1 1633950672000\n" - + "summary_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"s\",quantile=\"0.99\"} 0.3 1633950672000\n" - + "# TYPE cumulative_histogram_no_attributes_seconds histogram\n" - + "# HELP cumulative_histogram_no_attributes_seconds unused\n" - + "cumulative_histogram_no_attributes_seconds_count{otel_scope_name=\"full\",otel_scope_version=\"version\"} 2.0 1633950672000\n" - + "cumulative_histogram_no_attributes_seconds_sum{otel_scope_name=\"full\",otel_scope_version=\"version\"} 1.0 1633950672000\n" - + "cumulative_histogram_no_attributes_seconds_bucket{otel_scope_name=\"full\",otel_scope_version=\"version\",le=\"+Inf\"} 2.0 1633950672000\n" - + "# TYPE cumulative_histogram_single_attribute_seconds histogram\n" - + "# HELP cumulative_histogram_single_attribute_seconds unused\n" - + "cumulative_histogram_single_attribute_seconds_count{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"hs\"} 2.0 1633950672000\n" - + "cumulative_histogram_single_attribute_seconds_sum{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"hs\"} 1.0 1633950672000\n" - + "cumulative_histogram_single_attribute_seconds_bucket{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"hs\",le=\"+Inf\"} 2.0 1633950672000\n" - + "# TYPE double_gauge_no_attributes_seconds gauge\n" - + "# HELP double_gauge_no_attributes_seconds unused\n" - + "double_gauge_no_attributes_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\"} 7.0 1633950672000\n" - + "# TYPE double_gauge_multiple_attributes_seconds gauge\n" - + "# HELP double_gauge_multiple_attributes_seconds unused\n" - + "double_gauge_multiple_attributes_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",animal=\"bear\",type=\"dgma\"} 8.0 1633950672000\n" - + "# TYPE double_gauge_colliding_attributes_seconds gauge\n" - + "# HELP double_gauge_colliding_attributes_seconds unused\n" - + "double_gauge_colliding_attributes_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",foo_bar=\"a;b\",type=\"dgma\"} 8.0 1633950672000\n"); - assertThat(logCapturer.size()).isZero(); - } - - @Test - void openMetrics() { - assertThat( - serializeOpenMetrics( - MONOTONIC_CUMULATIVE_DOUBLE_SUM, - MONOTONIC_CUMULATIVE_DOUBLE_SUM_WITH_SUFFIX_TOTAL, - NON_MONOTONIC_CUMULATIVE_DOUBLE_SUM, - DELTA_DOUBLE_SUM, // Deltas are dropped - MONOTONIC_CUMULATIVE_LONG_SUM, - NON_MONOTONIC_CUMULATIVE_LONG_SUM, - DELTA_LONG_SUM, // Deltas are dropped - DOUBLE_GAUGE, - LONG_GAUGE, - SUMMARY, - DELTA_HISTOGRAM, // Deltas are dropped - CUMULATIVE_HISTOGRAM_NO_ATTRIBUTES, - CUMULATIVE_HISTOGRAM_SINGLE_ATTRIBUTE, - DOUBLE_GAUGE_NO_ATTRIBUTES, - DOUBLE_GAUGE_MULTIPLE_ATTRIBUTES, - DOUBLE_GAUGE_COLLIDING_ATTRIBUTES)) - .isEqualTo( - "# TYPE target info\n" - + "# HELP target Target metadata\n" - + "target_info{kr=\"vr\"} 1\n" - + "# TYPE otel_scope_info info\n" - + "# HELP otel_scope_info Scope metadata\n" - + "otel_scope_info{otel_scope_name=\"full\",otel_scope_version=\"version\",ks=\"vs\"} 1\n" - + "# TYPE monotonic_cumulative_double_sum_seconds counter\n" - + "# HELP monotonic_cumulative_double_sum_seconds description\n" - + "monotonic_cumulative_double_sum_seconds_total{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"mcds\"} 5.0 1633950672.000\n" - + "# TYPE monotonic_cumulative_double_sum_suffix_seconds_total counter\n" - + "# HELP monotonic_cumulative_double_sum_suffix_seconds_total description\n" - + "monotonic_cumulative_double_sum_suffix_seconds_total{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"mcds\"} 5.0 1633950672.000\n" - + "# TYPE non_monotonic_cumulative_double_sum_seconds gauge\n" - + "# HELP non_monotonic_cumulative_double_sum_seconds description\n" - + "non_monotonic_cumulative_double_sum_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"nmcds\"} 5.0 1633950672.000\n" - + "# TYPE monotonic_cumulative_long_sum_seconds counter\n" - + "# HELP monotonic_cumulative_long_sum_seconds unused\n" - + "monotonic_cumulative_long_sum_seconds_total{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"mcls\"} 5.0 1633950672.000\n" - + "# TYPE non_monotonic_cumulative_long_sum_seconds gauge\n" - + "# HELP non_monotonic_cumulative_long_sum_seconds unused\n" - + "non_monotonic_cumulative_long_sum_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"nmcls\"} 5.0 1633950672.000\n" - + "# TYPE double_gauge_seconds gauge\n" - + "# HELP double_gauge_seconds unused\n" - + "double_gauge_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"dg\"} 5.0 1633950672.000\n" - + "# TYPE long_gauge_seconds gauge\n" - + "# HELP long_gauge_seconds unused\n" - + "long_gauge_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"lg\"} 5.0 1633950672.000\n" - + "# TYPE summary_seconds summary\n" - + "# HELP summary_seconds unused\n" - + "summary_seconds_count{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"s\"} 5.0 1633950672.000\n" - + "summary_seconds_sum{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"s\"} 7.0 1633950672.000\n" - + "summary_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"s\",quantile=\"0.9\"} 0.1 1633950672.000\n" - + "summary_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"s\",quantile=\"0.99\"} 0.3 1633950672.000\n" - + "# TYPE cumulative_histogram_no_attributes_seconds histogram\n" - + "# HELP cumulative_histogram_no_attributes_seconds unused\n" - + "cumulative_histogram_no_attributes_seconds_count{otel_scope_name=\"full\",otel_scope_version=\"version\"} 2.0 1633950672.000\n" - + "cumulative_histogram_no_attributes_seconds_sum{otel_scope_name=\"full\",otel_scope_version=\"version\"} 1.0 1633950672.000\n" - + "cumulative_histogram_no_attributes_seconds_bucket{otel_scope_name=\"full\",otel_scope_version=\"version\",le=\"+Inf\"} 2.0 1633950672.000 # {span_id=\"0000000000000002\",trace_id=\"00000000000000000000000000000001\"} 4.0 0.001\n" - + "# TYPE cumulative_histogram_single_attribute_seconds histogram\n" - + "# HELP cumulative_histogram_single_attribute_seconds unused\n" - + "cumulative_histogram_single_attribute_seconds_count{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"hs\"} 2.0 1633950672.000\n" - + "cumulative_histogram_single_attribute_seconds_sum{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"hs\"} 1.0 1633950672.000\n" - + "cumulative_histogram_single_attribute_seconds_bucket{otel_scope_name=\"full\",otel_scope_version=\"version\",type=\"hs\",le=\"+Inf\"} 2.0 1633950672.000 # {span_id=\"0000000000000002\",trace_id=\"00000000000000000000000000000001\"} 4.0 0.001\n" - + "# TYPE double_gauge_no_attributes_seconds gauge\n" - + "# HELP double_gauge_no_attributes_seconds unused\n" - + "double_gauge_no_attributes_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\"} 7.0 1633950672.000\n" - + "# TYPE double_gauge_multiple_attributes_seconds gauge\n" - + "# HELP double_gauge_multiple_attributes_seconds unused\n" - + "double_gauge_multiple_attributes_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",animal=\"bear\",type=\"dgma\"} 8.0 1633950672.000\n" - + "# TYPE double_gauge_colliding_attributes_seconds gauge\n" - + "# HELP double_gauge_colliding_attributes_seconds unused\n" - + "double_gauge_colliding_attributes_seconds{otel_scope_name=\"full\",otel_scope_version=\"version\",foo_bar=\"a;b\",type=\"dgma\"} 8.0 1633950672.000\n" - + "# EOF\n"); - assertThat(logCapturer.size()).isZero(); - } - - @Test - @SuppressLogger(Serializer.class) - void outOfOrderedAttributes() { - // Alternative attributes implementation which sorts entries by the order they were added rather - // than lexicographically - // all attributes are retained, we log a warning, and b_key and b.key are not be merged - LinkedHashMap, Object> attributesMap = new LinkedHashMap<>(); - attributesMap.put(AttributeKey.stringKey("b_key"), "val1"); - attributesMap.put(AttributeKey.stringKey("a_key"), "val2"); - attributesMap.put(AttributeKey.stringKey("b.key"), "val3"); - Attributes attributes = new MapAttributes(attributesMap); - - MetricData metricData = - ImmutableMetricData.createDoubleSum( - Resource.builder().put("kr", "vr").build(), - InstrumentationScopeInfo.builder("scope").setVersion("1.0.0").build(), - "sum", - "description", - "s", - ImmutableSumData.create( - /* isMonotonic= */ true, - AggregationTemporality.CUMULATIVE, - Collections.singletonList( - ImmutableDoublePointData.create( - 1633947011000000000L, 1633950672000000000L, attributes, 5)))); - - assertThat(serialize004(metricData)) - .isEqualTo( - "# TYPE target info\n" - + "# HELP target Target metadata\n" - + "target_info{kr=\"vr\"} 1\n" - + "# TYPE sum_seconds_total counter\n" - + "# HELP sum_seconds_total description\n" - + "sum_seconds_total{otel_scope_name=\"scope\",otel_scope_version=\"1.0.0\",b_key=\"val1\",a_key=\"val2\",b_key=\"val3\"} 5.0 1633950672000\n"); - logCapturer.assertContains( - "Dropping out-of-order attribute a_key=val2, which occurred after b_key. This can occur when an alternative Attribute implementation is used."); - } - - @Test - void emptyResource() { - MetricData metricData = - ImmutableMetricData.createDoubleSum( - Resource.empty(), - InstrumentationScopeInfo.builder("scope").setVersion("1.0.0").build(), - "monotonic.cumulative.double.sum", - "description", - "s", - ImmutableSumData.create( - /* isMonotonic= */ true, - AggregationTemporality.CUMULATIVE, - Collections.singletonList( - ImmutableDoublePointData.create( - 1633947011000000000L, 1633950672000000000L, Attributes.empty(), 5)))); - - assertThat(serialize004(metricData)) - .isEqualTo( - "# TYPE monotonic_cumulative_double_sum_seconds_total counter\n" - + "# HELP monotonic_cumulative_double_sum_seconds_total description\n" - + "monotonic_cumulative_double_sum_seconds_total{otel_scope_name=\"scope\",otel_scope_version=\"1.0.0\"} 5.0 1633950672000\n"); - } - - private static String serialize004(MetricData... metrics) { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try { - new Serializer.Prometheus004Serializer(unused -> true).write(Arrays.asList(metrics), bos); - return bos.toString("UTF-8"); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - private static String serializeOpenMetrics(MetricData... metrics) { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try { - new Serializer.OpenMetrics100Serializer(unused -> true).write(Arrays.asList(metrics), bos); - return bos.toString("UTF-8"); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - @SuppressWarnings("unchecked") - private static class MapAttributes implements Attributes { - - private final LinkedHashMap, Object> map; - - @SuppressWarnings("NonApiType") - private MapAttributes(LinkedHashMap, Object> map) { - this.map = map; - } - - @Nullable - @Override - public T get(AttributeKey key) { - return (T) map.get(key); - } - - @Override - public void forEach(BiConsumer, ? super Object> consumer) { - map.forEach(consumer); - } - - @Override - public int size() { - return map.size(); - } - - @Override - public boolean isEmpty() { - return map.isEmpty(); - } - - @Override - public Map, Object> asMap() { - return map; - } - - @Override - public AttributesBuilder toBuilder() { - throw new UnsupportedOperationException("not supported"); - } - } -} diff --git a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/TestConstants.java b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/TestConstants.java deleted file mode 100644 index 3edc286c9ee..00000000000 --- a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/TestConstants.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.exporter.prometheus; - -import static io.opentelemetry.api.common.AttributeKey.stringKey; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.api.trace.TraceFlags; -import io.opentelemetry.api.trace.TraceState; -import io.opentelemetry.sdk.common.InstrumentationScopeInfo; -import io.opentelemetry.sdk.metrics.data.AggregationTemporality; -import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableDoubleExemplarData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableDoublePointData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableGaugeData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableHistogramData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableHistogramPointData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableLongPointData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableMetricData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableSumData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableSummaryData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableSummaryPointData; -import io.opentelemetry.sdk.metrics.internal.data.ImmutableValueAtQuantile; -import io.opentelemetry.sdk.resources.Resource; -import java.util.Arrays; -import java.util.Collections; -import java.util.concurrent.TimeUnit; - -/** A helper class encapsulating immutable static data that can be shared across all the tests. */ -class TestConstants { - - private TestConstants() { - // Private constructor to prevent instantiation - } - - private static final AttributeKey TYPE = stringKey("type"); - - static final MetricData MONOTONIC_CUMULATIVE_DOUBLE_SUM = - ImmutableMetricData.createDoubleSum( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "monotonic.cumulative.double.sum", - "description", - "s", - ImmutableSumData.create( - /* isMonotonic= */ true, - AggregationTemporality.CUMULATIVE, - Collections.singletonList( - ImmutableDoublePointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.of(TYPE, "mcds"), - 5)))); - - static final MetricData MONOTONIC_CUMULATIVE_DOUBLE_SUM_WITH_SUFFIX_TOTAL = - ImmutableMetricData.createDoubleSum( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "monotonic.cumulative.double.sum.suffix.total", - "description", - "s", - ImmutableSumData.create( - /* isMonotonic= */ true, - AggregationTemporality.CUMULATIVE, - Collections.singletonList( - ImmutableDoublePointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.of(TYPE, "mcds"), - 5)))); - - static final MetricData NON_MONOTONIC_CUMULATIVE_DOUBLE_SUM = - ImmutableMetricData.createDoubleSum( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "non.monotonic.cumulative.double.sum", - "description", - "s", - ImmutableSumData.create( - /* isMonotonic= */ false, - AggregationTemporality.CUMULATIVE, - Collections.singletonList( - ImmutableDoublePointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.of(TYPE, "nmcds"), - 5)))); - - static final MetricData DELTA_DOUBLE_SUM = - ImmutableMetricData.createDoubleSum( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "delta.double.sum", - "unused", - "s", - ImmutableSumData.create( - /* isMonotonic= */ true, - AggregationTemporality.DELTA, - Collections.singletonList( - ImmutableDoublePointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.of(TYPE, "mdds"), - 5)))); - - static final MetricData MONOTONIC_CUMULATIVE_LONG_SUM = - ImmutableMetricData.createLongSum( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "monotonic.cumulative.long.sum", - "unused", - "s", - ImmutableSumData.create( - /* isMonotonic= */ true, - AggregationTemporality.CUMULATIVE, - Collections.singletonList( - ImmutableLongPointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.of(TYPE, "mcls"), - 5)))); - - static final MetricData NON_MONOTONIC_CUMULATIVE_LONG_SUM = - ImmutableMetricData.createLongSum( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "non.monotonic.cumulative.long_sum", - "unused", - "s", - ImmutableSumData.create( - /* isMonotonic= */ false, - AggregationTemporality.CUMULATIVE, - Collections.singletonList( - ImmutableLongPointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.of(TYPE, "nmcls"), - 5)))); - static final MetricData DELTA_LONG_SUM = - ImmutableMetricData.createLongSum( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "delta.long.sum", - "unused", - "s", - ImmutableSumData.create( - /* isMonotonic= */ true, - AggregationTemporality.DELTA, - Collections.singletonList( - ImmutableLongPointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.of(TYPE, "mdls"), - 5)))); - - static final MetricData DOUBLE_GAUGE = - ImmutableMetricData.createDoubleGauge( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "double.gauge", - "unused", - "s", - ImmutableGaugeData.create( - Collections.singletonList( - ImmutableDoublePointData.create( - 1633947011000000000L, 1633950672000000000L, Attributes.of(TYPE, "dg"), 5)))); - static final MetricData LONG_GAUGE = - ImmutableMetricData.createLongGauge( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "long.gauge", - "unused", - "s", - ImmutableGaugeData.create( - Collections.singletonList( - ImmutableLongPointData.create( - 1633947011000000000L, 1633950672000000000L, Attributes.of(TYPE, "lg"), 5)))); - static final MetricData SUMMARY = - ImmutableMetricData.createDoubleSummary( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "summary", - "unused", - "s", - ImmutableSummaryData.create( - Collections.singletonList( - ImmutableSummaryPointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.of(TYPE, "s"), - 5, - 7, - Arrays.asList( - ImmutableValueAtQuantile.create(0.9, 0.1), - ImmutableValueAtQuantile.create(0.99, 0.3)))))); - - static final MetricData DELTA_HISTOGRAM = - ImmutableMetricData.createDoubleHistogram( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "delta.histogram", - "unused", - "s", - ImmutableHistogramData.create( - AggregationTemporality.DELTA, - Collections.singletonList( - ImmutableHistogramPointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.empty(), - 1.0, - /* hasMin= */ false, - 0, - /* hasMax= */ false, - 0, - Collections.emptyList(), - Collections.singletonList(2L), - Collections.emptyList())))); - - static final MetricData CUMULATIVE_HISTOGRAM_NO_ATTRIBUTES = - ImmutableMetricData.createDoubleHistogram( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "cumulative.histogram.no.attributes", - "unused", - "s", - ImmutableHistogramData.create( - AggregationTemporality.CUMULATIVE, - Collections.singletonList( - ImmutableHistogramPointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.empty(), - 1.0, - /* hasMin= */ false, - 0, - /* hasMax= */ false, - 0, - Collections.emptyList(), - Collections.singletonList(2L), - Collections.singletonList( - ImmutableDoubleExemplarData.create( - Attributes.empty(), - TimeUnit.MILLISECONDS.toNanos(1L), - SpanContext.create( - "00000000000000000000000000000001", - "0000000000000002", - TraceFlags.getDefault(), - TraceState.getDefault()), - /* value= */ 4)))))); - - static final MetricData CUMULATIVE_HISTOGRAM_SINGLE_ATTRIBUTE = - ImmutableMetricData.createDoubleHistogram( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "cumulative.histogram.single.attribute", - "unused", - "s", - ImmutableHistogramData.create( - AggregationTemporality.CUMULATIVE, - Collections.singletonList( - ImmutableHistogramPointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.of(TYPE, "hs"), - 1.0, - /* hasMin= */ false, - 0, - /* hasMax= */ false, - 0, - Collections.emptyList(), - Collections.singletonList(2L), - Collections.singletonList( - ImmutableDoubleExemplarData.create( - Attributes.empty(), - TimeUnit.MILLISECONDS.toNanos(1L), - SpanContext.create( - "00000000000000000000000000000001", - "0000000000000002", - TraceFlags.getDefault(), - TraceState.getDefault()), - /* value= */ 4)))))); - - static final MetricData DOUBLE_GAUGE_NO_ATTRIBUTES = - ImmutableMetricData.createDoubleGauge( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "double.gauge.no.attributes", - "unused", - "s", - ImmutableGaugeData.create( - Collections.singletonList( - ImmutableDoublePointData.create( - 1633947011000000000L, 1633950672000000000L, Attributes.empty(), 7)))); - - static final MetricData DOUBLE_GAUGE_MULTIPLE_ATTRIBUTES = - ImmutableMetricData.createDoubleGauge( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "double.gauge.multiple.attributes", - "unused", - "s", - ImmutableGaugeData.create( - Collections.singletonList( - ImmutableDoublePointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.of(TYPE, "dgma", stringKey("animal"), "bear"), - 8)))); - static final MetricData DOUBLE_GAUGE_COLLIDING_ATTRIBUTES = - ImmutableMetricData.createDoubleGauge( - Resource.create(Attributes.of(stringKey("kr"), "vr")), - InstrumentationScopeInfo.builder("full") - .setVersion("version") - .setAttributes(Attributes.of(stringKey("ks"), "vs")) - .build(), - "double.gauge.colliding.attributes", - "unused", - "s", - ImmutableGaugeData.create( - Collections.singletonList( - ImmutableDoublePointData.create( - 1633947011000000000L, - 1633950672000000000L, - Attributes.of( - TYPE, "dgma", stringKey("foo.bar"), "a", stringKey("foo_bar"), "b"), - 8)))); -} diff --git a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/internal/PrometheusMetricReaderProviderTest.java b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/internal/PrometheusMetricReaderProviderTest.java index 9cc1acd2614..58504e2e7e9 100644 --- a/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/internal/PrometheusMetricReaderProviderTest.java +++ b/exporters/prometheus/src/test/java/io/opentelemetry/exporter/prometheus/internal/PrometheusMetricReaderProviderTest.java @@ -15,6 +15,7 @@ import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.prometheus.metrics.exporter.httpserver.HTTPServer; import java.io.IOException; import java.net.ServerSocket; import java.util.HashMap; @@ -49,6 +50,7 @@ void createMetricReader_Default() throws IOException { try (MetricReader metricReader = provider.createMetricReader(configProperties)) { assertThat(metricReader) .isInstanceOf(PrometheusHttpServer.class) + .extracting("httpServer", as(InstanceOfAssertFactories.type(HTTPServer.class))) .extracting("server", as(InstanceOfAssertFactories.type(HttpServer.class))) .satisfies( server -> { @@ -78,6 +80,7 @@ void createMetricReader_WithConfiguration() throws IOException { try (MetricReader metricReader = provider.createMetricReader(DefaultConfigProperties.createFromMap(config))) { assertThat(metricReader) + .extracting("httpServer", as(InstanceOfAssertFactories.type(HTTPServer.class))) .extracting("server", as(InstanceOfAssertFactories.type(HttpServer.class))) .satisfies( server -> {