Skip to content

Commit 4d034b0

Browse files
authored
Append unit to prometheus metric names (#5400)
1 parent 951221e commit 4d034b0

File tree

11 files changed

+1038
-363
lines changed

11 files changed

+1038
-363
lines changed

exporters/prometheus/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ dependencies {
1414
implementation(project(":sdk-extensions:autoconfigure-spi"))
1515

1616
compileOnly("com.sun.net.httpserver:http")
17+
compileOnly("com.google.auto.value:auto-value-annotations")
18+
19+
annotationProcessor("com.google.auto.value:auto-value")
1720

1821
testImplementation(project(":semconv"))
1922

exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/NameSanitizer.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class NameSanitizer implements Function<String, String> {
1515

1616
static final NameSanitizer INSTANCE = new NameSanitizer();
1717

18+
static final Pattern SANITIZE_CONSECUTIVE_UNDERSCORES = Pattern.compile("[_]{2,}");
19+
1820
private static final Pattern SANITIZE_PREFIX_PATTERN = Pattern.compile("^[^a-zA-Z_:]");
1921
private static final Pattern SANITIZE_BODY_PATTERN = Pattern.compile("[^a-zA-Z0-9_:]");
2022

@@ -36,8 +38,11 @@ public String apply(String labelName) {
3638
}
3739

3840
private static String sanitizeMetricName(String metricName) {
39-
return SANITIZE_BODY_PATTERN
40-
.matcher(SANITIZE_PREFIX_PATTERN.matcher(metricName).replaceFirst("_"))
41+
return SANITIZE_CONSECUTIVE_UNDERSCORES
42+
.matcher(
43+
SANITIZE_BODY_PATTERN
44+
.matcher(SANITIZE_PREFIX_PATTERN.matcher(metricName).replaceFirst("_"))
45+
.replaceAll("_"))
4146
.replaceAll("_");
4247
}
4348
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.exporter.prometheus;
7+
8+
import com.google.auto.value.AutoValue;
9+
import io.opentelemetry.api.internal.StringUtils;
10+
import io.opentelemetry.sdk.metrics.data.MetricData;
11+
import java.util.Map;
12+
import java.util.concurrent.ConcurrentHashMap;
13+
import java.util.function.BiFunction;
14+
import javax.annotation.concurrent.Immutable;
15+
16+
/** A class that maps a raw metric name to Prometheus equivalent name. */
17+
class PrometheusMetricNameMapper implements BiFunction<MetricData, PrometheusType, String> {
18+
19+
static final PrometheusMetricNameMapper INSTANCE = new PrometheusMetricNameMapper();
20+
21+
private final Map<ImmutableMappingKey, String> cache = new ConcurrentHashMap<>();
22+
private final BiFunction<MetricData, PrometheusType, String> delegate;
23+
24+
// private constructor - prevent external object initialization
25+
private PrometheusMetricNameMapper() {
26+
this(PrometheusMetricNameMapper::mapToPrometheusName);
27+
}
28+
29+
// Visible for testing
30+
PrometheusMetricNameMapper(BiFunction<MetricData, PrometheusType, String> delegate) {
31+
this.delegate = delegate;
32+
}
33+
34+
@Override
35+
public String apply(MetricData rawMetric, PrometheusType prometheusType) {
36+
return cache.computeIfAbsent(
37+
createKeyForCacheMapping(rawMetric, prometheusType),
38+
metricData -> delegate.apply(rawMetric, prometheusType));
39+
}
40+
41+
private static String mapToPrometheusName(MetricData rawMetric, PrometheusType prometheusType) {
42+
String name = NameSanitizer.INSTANCE.apply(rawMetric.getName());
43+
String prometheusEquivalentUnit =
44+
PrometheusUnitsHelper.getEquivalentPrometheusUnit(rawMetric.getUnit());
45+
// append prometheus unit if not null or empty.
46+
if (!StringUtils.isNullOrEmpty(prometheusEquivalentUnit)
47+
&& !name.contains(prometheusEquivalentUnit)) {
48+
name = name + "_" + prometheusEquivalentUnit;
49+
}
50+
51+
// special case - counter
52+
if (prometheusType == PrometheusType.COUNTER && !name.contains("total")) {
53+
name = name + "_total";
54+
}
55+
// special case - gauge
56+
if (rawMetric.getUnit().equals("1")
57+
&& prometheusType == PrometheusType.GAUGE
58+
&& !name.contains("ratio")) {
59+
name = name + "_ratio";
60+
}
61+
return name;
62+
}
63+
64+
/**
65+
* Creates a suitable mapping key to be used for maintaining mapping between raw metric and its
66+
* equivalent Prometheus name.
67+
*
68+
* @param metricData the metric data for which the mapping is to be created.
69+
* @param prometheusType the prometheus type to which the metric is to be mapped.
70+
* @return an {@link ImmutableMappingKey} that can be used as a key for mapping between metric
71+
* data and its prometheus equivalent name.
72+
*/
73+
private static ImmutableMappingKey createKeyForCacheMapping(
74+
MetricData metricData, PrometheusType prometheusType) {
75+
return ImmutableMappingKey.create(
76+
metricData.getName(), metricData.getUnit(), prometheusType.name());
77+
}
78+
79+
/**
80+
* Objects of this class acts as mapping keys for Prometheus metric mapping cache used in {@link
81+
* PrometheusMetricNameMapper}.
82+
*/
83+
@Immutable
84+
@AutoValue
85+
abstract static class ImmutableMappingKey {
86+
static ImmutableMappingKey create(
87+
String rawMetricName, String rawMetricUnit, String prometheusType) {
88+
return new AutoValue_PrometheusMetricNameMapper_ImmutableMappingKey(
89+
rawMetricName, rawMetricUnit, prometheusType);
90+
}
91+
92+
abstract String rawMetricName();
93+
94+
abstract String rawMetricUnit();
95+
96+
abstract String prometheusType();
97+
}
98+
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.exporter.prometheus;
7+
8+
import static io.opentelemetry.exporter.prometheus.NameSanitizer.SANITIZE_CONSECUTIVE_UNDERSCORES;
9+
10+
import io.opentelemetry.api.internal.StringUtils;
11+
import java.util.regex.Pattern;
12+
13+
/**
14+
* A utility class that contains helper function(s) to aid conversion from OTLP to Prometheus units.
15+
*
16+
* @see <a
17+
* href="https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#units-and-base-units">OpenMetrics
18+
* specification for units</a>
19+
* @see <a href="https://prometheus.io/docs/practices/naming/#base-units">Prometheus best practices
20+
* for units</a>
21+
*/
22+
final class PrometheusUnitsHelper {
23+
24+
private static final Pattern INVALID_CHARACTERS_PATTERN = Pattern.compile("[^a-zA-Z0-9]");
25+
private static final Pattern CHARACTERS_BETWEEN_BRACES_PATTERN = Pattern.compile("\\{(.*?)}");
26+
private static final Pattern SANITIZE_LEADING_UNDERSCORES = Pattern.compile("^_+");
27+
private static final Pattern SANITIZE_TRAILING_UNDERSCORES = Pattern.compile("_+$");
28+
29+
private PrometheusUnitsHelper() {
30+
// Prevent object creation for utility classes
31+
}
32+
33+
/**
34+
* A utility function that returns the equivalent Prometheus name for the provided OTLP metric
35+
* unit.
36+
*
37+
* @param rawMetricUnitName The raw metric unit for which Prometheus metric unit needs to be
38+
* computed.
39+
* @return the computed Prometheus metric unit equivalent of the OTLP metric un
40+
*/
41+
static String getEquivalentPrometheusUnit(String rawMetricUnitName) {
42+
if (StringUtils.isNullOrEmpty(rawMetricUnitName)) {
43+
return rawMetricUnitName;
44+
}
45+
// Drop units specified between curly braces
46+
String convertedMetricUnitName = removeUnitPortionInBraces(rawMetricUnitName);
47+
// Handling for the "per" unit(s), e.g. foo/bar -> foo_per_bar
48+
convertedMetricUnitName = convertRateExpressedToPrometheusUnit(convertedMetricUnitName);
49+
// Converting abbreviated unit names to full names
50+
return cleanUpString(getPrometheusUnit(convertedMetricUnitName));
51+
}
52+
53+
/**
54+
* This method is used to convert the units expressed as a rate via '/' symbol in their name to
55+
* their expanded text equivalent. For instance, km/h => km_per_hour. The method operates on the
56+
* input by splitting it in 2 parts - before and after '/' symbol and will attempt to expand any
57+
* known unit abbreviation in both parts. Unknown abbreviations & unsupported characters will
58+
* remain unchanged in the final output of this function.
59+
*
60+
* @param rateExpressedUnit The rate unit input that needs to be converted to its text equivalent.
61+
* @return The text equivalent of unit expressed as rate. If the input does not contain '/', the
62+
* function returns it as-is.
63+
*/
64+
private static String convertRateExpressedToPrometheusUnit(String rateExpressedUnit) {
65+
if (!rateExpressedUnit.contains("/")) {
66+
return rateExpressedUnit;
67+
}
68+
String[] rateEntities = rateExpressedUnit.split("/", 2);
69+
// Only convert rate expressed units if it's a valid expression
70+
if (rateEntities[1].equals("")) {
71+
return rateExpressedUnit;
72+
}
73+
return getPrometheusUnit(rateEntities[0]) + "_per_" + getPrometheusPerUnit(rateEntities[1]);
74+
}
75+
76+
/**
77+
* This method drops all characters enclosed within '{}' (including the curly braces) by replacing
78+
* them with an empty string. Note that this method will not produce the intended effect if there
79+
* are nested curly braces within the outer enclosure of '{}'.
80+
*
81+
* <p>For instance, {packet{s}s} => s}.
82+
*
83+
* @param unit The input unit from which text within curly braces needs to be removed.
84+
* @return The resulting unit after removing the text within '{}'.
85+
*/
86+
private static String removeUnitPortionInBraces(String unit) {
87+
return CHARACTERS_BETWEEN_BRACES_PATTERN.matcher(unit).replaceAll("");
88+
}
89+
90+
/**
91+
* Replaces all characters that are not a letter or a digit with '_' to make the resulting string
92+
* Prometheus compliant. This method also removes leading and trailing underscores - this is done
93+
* to keep the resulting unit similar to what is produced from the collector's implementation.
94+
*
95+
* @param string The string input that needs to be made Prometheus compliant.
96+
* @return the cleaned-up Prometheus compliant string.
97+
*/
98+
private static String cleanUpString(String string) {
99+
return SANITIZE_LEADING_UNDERSCORES
100+
.matcher(
101+
SANITIZE_TRAILING_UNDERSCORES
102+
.matcher(
103+
SANITIZE_CONSECUTIVE_UNDERSCORES
104+
.matcher(INVALID_CHARACTERS_PATTERN.matcher(string).replaceAll("_"))
105+
.replaceAll("_"))
106+
.replaceAll(""))
107+
.replaceAll("");
108+
}
109+
110+
/**
111+
* This method retrieves the expanded Prometheus unit name for known abbreviations. OTLP metrics
112+
* use the c/s notation as specified at <a href="https://ucum.org/ucum.html">UCUM</a>. The list of
113+
* mappings is adopted from <a
114+
* href="https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/9a9d4778bbbf242dba233db28e2fbcfda3416959/pkg/translator/prometheus/normalize_name.go#L30">OpenTelemetry
115+
* Collector Contrib</a>.
116+
*
117+
* @param unitAbbreviation The unit that name that needs to be expanded/converted to Prometheus
118+
* units.
119+
* @return The expanded/converted unit name if known, otherwise returns the input unit name as-is.
120+
*/
121+
private static String getPrometheusUnit(String unitAbbreviation) {
122+
switch (unitAbbreviation) {
123+
// Time
124+
case "d":
125+
return "days";
126+
case "h":
127+
return "hours";
128+
case "min":
129+
return "minutes";
130+
case "s":
131+
return "seconds";
132+
case "ms":
133+
return "milliseconds";
134+
case "us":
135+
return "microseconds";
136+
case "ns":
137+
return "nanoseconds";
138+
// Bytes
139+
case "By":
140+
return "bytes";
141+
case "KiBy":
142+
return "kibibytes";
143+
case "MiBy":
144+
return "mebibytes";
145+
case "GiBy":
146+
return "gibibytes";
147+
case "TiBy":
148+
return "tibibytes";
149+
case "KBy":
150+
return "kilobytes";
151+
case "MBy":
152+
return "megabytes";
153+
case "GBy":
154+
return "gigabytes";
155+
case "TBy":
156+
return "terabytes";
157+
case "B":
158+
return "bytes";
159+
case "KB":
160+
return "kilobytes";
161+
case "MB":
162+
return "megabytes";
163+
case "GB":
164+
return "gigabytes";
165+
case "TB":
166+
return "terabytes";
167+
// SI
168+
case "m":
169+
return "meters";
170+
case "V":
171+
return "volts";
172+
case "A":
173+
return "amperes";
174+
case "J":
175+
return "joules";
176+
case "W":
177+
return "watts";
178+
case "g":
179+
return "grams";
180+
// Misc
181+
case "Cel":
182+
return "celsius";
183+
case "Hz":
184+
return "hertz";
185+
case "1":
186+
return "";
187+
case "%":
188+
return "percent";
189+
case "$":
190+
return "dollars";
191+
default:
192+
return unitAbbreviation;
193+
}
194+
}
195+
196+
/**
197+
* This method retrieves the expanded Prometheus unit name to be used with "per" units for known
198+
* units. For example: s => per second (singular)
199+
*
200+
* @param perUnitAbbreviation The unit abbreviation used in a 'per' unit.
201+
* @return The expanded unit equivalent to be used in 'per' unit if the input is a known unit,
202+
* otherwise returns the input as-is.
203+
*/
204+
private static String getPrometheusPerUnit(String perUnitAbbreviation) {
205+
switch (perUnitAbbreviation) {
206+
case "s":
207+
return "second";
208+
case "m":
209+
return "minute";
210+
case "h":
211+
return "hour";
212+
case "d":
213+
return "day";
214+
case "w":
215+
return "week";
216+
case "mo":
217+
return "month";
218+
case "y":
219+
return "year";
220+
default:
221+
return perUnitAbbreviation;
222+
}
223+
}
224+
}

exporters/prometheus/src/main/java/io/opentelemetry/exporter/prometheus/Serializer.java

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ final Set<String> write(Collection<MetricData> metrics, OutputStream output) thr
118118
continue;
119119
}
120120
PrometheusType prometheusType = PrometheusType.forMetric(metric);
121-
String metricName = metricName(metric.getName(), prometheusType);
121+
String metricName = PrometheusMetricNameMapper.INSTANCE.apply(metric, prometheusType);
122122
// Skip metrics which do not pass metricNameFilter
123123
if (!metricNameFilter.test(metricName)) {
124124
continue;
@@ -650,14 +650,6 @@ static Collection<? extends PointData> getPoints(MetricData metricData) {
650650
return Collections.emptyList();
651651
}
652652

653-
private static String metricName(String rawMetricName, PrometheusType type) {
654-
String name = NameSanitizer.INSTANCE.apply(rawMetricName);
655-
if (type == PrometheusType.COUNTER && !name.endsWith("_total")) {
656-
name = name + "_total";
657-
}
658-
return name;
659-
}
660-
661653
private static double getExemplarValue(ExemplarData exemplar) {
662654
return exemplar instanceof DoubleExemplarData
663655
? ((DoubleExemplarData) exemplar).getValue()

0 commit comments

Comments
 (0)