Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Append unit to prometheus metric names #5400

Merged
merged 25 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5b28758
Append units to Prometheus metric names
psx95 Apr 24, 2023
5e5661f
Fix failing tests due to metric names update
psx95 Apr 24, 2023
bccc044
Refactor unit conversion function
psx95 Apr 24, 2023
cb3956b
Fix edge cases for unit conversion
psx95 Apr 25, 2023
d5e7c40
Add tests for unit conversion
psx95 Apr 25, 2023
6e7b601
Fix naming issues to be compliant with the stylecheck
psx95 Apr 25, 2023
6c17344
Remove redundant validity check
psx95 Apr 25, 2023
0525969
Move 'per' unit check inside the coversion method
psx95 Apr 26, 2023
6f20800
Move abbreviated unit mappings: Map to Switch-Case
psx95 Apr 26, 2023
f2fea06
Add unit tests to cover all expansion cases
psx95 Apr 26, 2023
7568167
Add missing test case to increase coverage
psx95 Apr 26, 2023
bab0e57
Add missing documentation
psx95 Apr 27, 2023
19a93fe
Make PrometheusUnitsHelper class internal
psx95 Apr 28, 2023
2937d84
Replace string replace with pattern matching
psx95 Apr 28, 2023
ac96ab0
Refactor: unit name cleanup logic moved to Serializer
psx95 Apr 28, 2023
5d3488f
Add tests for metricName serialization
psx95 Apr 29, 2023
bf33ae0
Cleanup units before returning
psx95 Apr 29, 2023
0cd3aab
Appends units if not present in metric name
psx95 Apr 29, 2023
519ceb8
Merge branch 'main' into issue-4390
psx95 May 5, 2023
c4df7ef
Merge branch 'main' into issue-4390
psx95 Jun 2, 2023
7bf71a2
Convert public method to package-private
psx95 Jun 2, 2023
0f2c3c1
Rename method sampleMetricDataGenerator -> createSampleMetricData
psx95 Jun 2, 2023
38ee5a8
Apply caching mechanism to prometheus metric name mapping
psx95 Jun 3, 2023
cd89da8
Remove 1 as unit for non-gauges in test data
psx95 Jun 3, 2023
c302e91
Creates an AutoValue class for cache mapping keys
psx95 Jun 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.exporter.prometheus;

import io.opentelemetry.api.internal.StringUtils;
import java.util.Collections;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* A utility class that is used to maintain mappings between OTLP unit and Prometheus units. The
* list of mappings is adopted from <a
* href="https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/translator/prometheus/normalize_name.go#L30">OpenTelemetry
* Collector Contrib</a>.
*
* @see <a
* href="https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#units-and-base-units">OpenMetrics
* specification for units</a>
* @see <a href="https://prometheus.io/docs/practices/naming/#base-units">Prometheus best practices
* for units</a>
*/
public final class PrometheusUnitsHelper {
psx95 marked this conversation as resolved.
Show resolved Hide resolved

private static final Pattern INVALID_CHARACTERS_PATTERN = Pattern.compile("[^a-zA-Z0-9]");
private static final String CHARACTERS_BETWEEN_BRACES_REGEX =
psx95 marked this conversation as resolved.
Show resolved Hide resolved
"\\{(.*?)}"; // matches all characters between {}

private static final Map<String, String> PROMETHEUS_UNIT_MAP =
psx95 marked this conversation as resolved.
Show resolved Hide resolved
Stream.of(
new String[][] {
// Time
{"d", "days"},
{"h", "hours"},
{"min", "minutes"},
{"s", "seconds"},
{"ms", "milliseconds"},
{"us", "microseconds"},
{"ns", "nanoseconds"},
// Bytes
{"By", "bytes"},
{"KiBy", "kibibytes"},
{"MiBy", "mebibytes"},
{"GiBy", "gibibytes"},
{"TiBy", "tibibytes"},
{"KBy", "kilobytes"},
{"MBy", "megabytes"},
{"GBy", "gigabytes"},
{"TBy", "terabytes"},
{"B", "bytes"},
{"KB", "kilobytes"},
{"MB", "megabytes"},
{"GB", "gigabytes"},
{"TB", "terabytes"},
// SI
{"m", "meters"},
{"V", "volts"},
{"A", "amperes"},
{"J", "joules"},
{"W", "watts"},
{"g", "grams"},
// Misc
{"Cel", "celsius"},
{"Hz", "hertz"},
{"1", ""},
{"%", "percent"},
{"$", "dollars"}
})
.collect(
Collectors.collectingAndThen(
Collectors.toMap(
keyValuePair -> keyValuePair[0], keyValuePair -> keyValuePair[1]),
Collections::unmodifiableMap));

// The map that translates the "per" unit
// Example: s => per second (singular)
private static final Map<String, String> PROMETHEUS_PER_UNIT_MAP =
Stream.of(
new String[][] {
{"s", "second"},
{"m", "minute"},
{"h", "hour"},
{"d", "day"},
{"w", "week"},
{"mo", "month"},
{"y", "year"}
})
.collect(
Collectors.collectingAndThen(
Collectors.toMap(
keyValuePair -> keyValuePair[0], keyValuePair -> keyValuePair[1]),
Collections::unmodifiableMap));

private PrometheusUnitsHelper() {
// Prevent object creation for utility classes
}

/**
* 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.
* @param metricType The {@link PrometheusType} of the metric whose unit is to be converted.
* @return the computed Prometheus metric unit equivalent of the OTLP metric unit.
*/
public static String getEquivalentPrometheusUnit(
String rawMetricUnitName, PrometheusType metricType) {
if (StringUtils.isNullOrEmpty(rawMetricUnitName)) {
return rawMetricUnitName;
}

// special case
if (rawMetricUnitName.equals("1") && metricType == PrometheusType.GAUGE) {
return "ratio";
}

String convertedMetricUnitName = rawMetricUnitName;
// Drop units specified between curly braces
convertedMetricUnitName = removeUnitPortionInBrackets(convertedMetricUnitName);

// Handling for the "per" unit(s), e.g. foo/bar -> foo_per_bar
if (convertedMetricUnitName.contains("/")) {
convertedMetricUnitName = convertRateExpressedToPrometheusUnit(convertedMetricUnitName);
}

// Converting abbreviated unit names to full names
return cleanUpString(
PROMETHEUS_UNIT_MAP.getOrDefault(convertedMetricUnitName, convertedMetricUnitName));
}

private static String convertRateExpressedToPrometheusUnit(String rateExpressedUnit) {
psx95 marked this conversation as resolved.
Show resolved Hide resolved
String[] rateEntities = rateExpressedUnit.split("/", 2);
// Only convert rate expressed units if it's a valid expression
if (rateEntities[1].equals("")) {
psx95 marked this conversation as resolved.
Show resolved Hide resolved
return rateExpressedUnit;
}
return PROMETHEUS_UNIT_MAP.getOrDefault(rateEntities[0], rateEntities[0])
+ "_per_"
+ PROMETHEUS_PER_UNIT_MAP.getOrDefault(rateEntities[1], rateEntities[1]);
}

private static String removeUnitPortionInBrackets(String unit) {
// This does not handle nested braces
psx95 marked this conversation as resolved.
Show resolved Hide resolved
return unit.replaceAll(CHARACTERS_BETWEEN_BRACES_REGEX, "");
}

/**
* 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.
*
* @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) {
String prometheusCompliant =
INVALID_CHARACTERS_PATTERN.matcher(string).replaceAll("_").replaceAll("[_]{2,}", "_");
prometheusCompliant = prometheusCompliant.replaceAll("_+$", ""); // remove trailing underscore
prometheusCompliant = prometheusCompliant.replaceAll("^_+", ""); // remove leading underscore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for these replacements? Prometheus does not seem to forbid double or trailing underscores in its spec; and it does not seem that the previous operations on the unit are likely to produce trailing or duplicated underscores.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec linked in the PR description states -

 Multiple consecutive `_` characters MUST be replaced with a single `_` character

Based on that I removed leading _, because the serializer was already adding it.

name = name + "_" + prometheusEquivalentUnit;

But looking at this again, I think this would be confusing to do in the PrometheusUnitHelper. I will remove this, also the spec does not explicitly mention that the final unit name cannot have a trailing _, so I think leading _ is fine. I will make changes.

Also, the current name sanitizer does not seem to remove consecutive _, so based on spec this needs to be added as well.

TL;DR

  • The PrometheusUnitHelper will just return the computed unit (with illegal characters replaced with _) without any additional cleaning up.
  • The Sanitizing will be done by the serializer right before returning. The sanitizing will only take care of replacing any remaining Illegal characters with _ and then replacing consecutive _ with a single underscore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to mention here that while not explicitly stated in the spec, this is how opentelemetry-collector works, should we try to keep this in sync with how the collector works ?

return prometheusCompliant;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.internal.StringUtils;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
Expand Down Expand Up @@ -118,7 +119,7 @@ final Set<String> write(Collection<MetricData> metrics, OutputStream output) thr
continue;
}
PrometheusType prometheusType = PrometheusType.forMetric(metric);
String metricName = metricName(metric.getName(), prometheusType);
String metricName = metricName(metric, prometheusType);
// Skip metrics which do not pass metricNameFilter
if (!metricNameFilter.test(metricName)) {
continue;
Expand Down Expand Up @@ -650,8 +651,15 @@ static Collection<? extends PointData> getPoints(MetricData metricData) {
return Collections.emptyList();
}

private static String metricName(String rawMetricName, PrometheusType type) {
String name = NameSanitizer.INSTANCE.apply(rawMetricName);
private static String metricName(MetricData rawMetric, PrometheusType type) {
String name = NameSanitizer.INSTANCE.apply(rawMetric.getName());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you move this line below the next if you can probably get rid of the cleanUpString method in PrometheusUnitsHelper -- no need to perform sanitization twice after all. (And the performance will benefit from the cache in NameSanitizer as well)

String prometheusEquivalentUnit =
PrometheusUnitsHelper.getEquivalentPrometheusUnit(rawMetric.getUnit(), type);
if (!StringUtils.isNullOrEmpty(prometheusEquivalentUnit)
&& !name.contains(prometheusEquivalentUnit)) {
name = name + "_" + prometheusEquivalentUnit;
}

if (type == PrometheusType.COUNTER) {
name = name + "_total";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ void fetch_DuplicateMetrics() {
InstrumentationScopeInfo.create("scope1"),
"foo",
"description1",
"unit1",
"unit",
ImmutableSumData.create(
/* isMonotonic= */ true,
AggregationTemporality.CUMULATIVE,
Expand All @@ -248,7 +248,7 @@ void fetch_DuplicateMetrics() {
InstrumentationScopeInfo.create("scope2"),
"foo",
"description2",
"unit2",
"unit",
ImmutableSumData.create(
/* isMonotonic= */ true,
AggregationTemporality.CUMULATIVE,
Expand All @@ -259,9 +259,9 @@ void fetch_DuplicateMetrics() {
ImmutableMetricData.createLongGauge(
resource,
InstrumentationScopeInfo.create("scope3"),
"foo_total",
"unused",
"foo_unit_total",
"unused",
"unit",
ImmutableGaugeData.create(
Collections.singletonList(
ImmutableLongPointData.create(123, 456, Attributes.empty(), 3))))));
Expand All @@ -283,13 +283,13 @@ void fetch_DuplicateMetrics() {
+ "otel_scope_info{otel_scope_name=\"scope2\"} 1\n"
+ "# TYPE foo_total counter\n"
+ "# HELP foo_total description1\n"
+ "foo_total{otel_scope_name=\"scope1\"} 1.0 0\n"
+ "foo_total{otel_scope_name=\"scope2\"} 2.0 0\n");
+ "foo_unit_total{otel_scope_name=\"scope1\"} 1.0 0\n"
+ "foo_unit_total{otel_scope_name=\"scope2\"} 2.0 0\n");

// Validate conflict warning message
assertThat(logs.getEvents()).hasSize(1);
logs.assertContains(
"Metric conflict(s) detected. Multiple metrics with same name but different type: [foo_total]");
"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();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* 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, PrometheusType metricType) {
assertEquals(
prometheusUnit, PrometheusUnitsHelper.getEquivalentPrometheusUnit(otlpUnit, metricType));
}

private static Stream<Arguments> providePrometheusOTelUnitEquivalentPairs() {
return Stream.of(
// Simple expansion - storage Bytes
Arguments.of("By", "bytes", PrometheusType.GAUGE),
Arguments.of("By", "bytes", PrometheusType.COUNTER),
Arguments.of("By", "bytes", PrometheusType.SUMMARY),
Arguments.of("By", "bytes", PrometheusType.HISTOGRAM),
Arguments.of("B", "bytes", PrometheusType.GAUGE),
Arguments.of("B", "bytes", PrometheusType.COUNTER),
Arguments.of("B", "bytes", PrometheusType.SUMMARY),
Arguments.of("B", "bytes", PrometheusType.HISTOGRAM),
// Simple expansion - Time unit
Arguments.of("s", "seconds", PrometheusType.GAUGE),
Arguments.of("s", "seconds", PrometheusType.COUNTER),
Arguments.of("s", "seconds", PrometheusType.SUMMARY),
Arguments.of("s", "seconds", PrometheusType.HISTOGRAM),
// Unit not found - Case sensitive
Arguments.of("S", "S", PrometheusType.GAUGE),
Arguments.of("S", "S", PrometheusType.COUNTER),
Arguments.of("S", "S", PrometheusType.SUMMARY),
Arguments.of("S", "S", PrometheusType.HISTOGRAM),
// Special case - 1
Arguments.of("1", "ratio", PrometheusType.GAUGE),
Arguments.of("1", "", PrometheusType.COUNTER),
Arguments.of("1", "", PrometheusType.SUMMARY),
Arguments.of("1", "", PrometheusType.HISTOGRAM),
// Special Case - Drop metric units in {}
Arguments.of("{packets}", "", PrometheusType.GAUGE),
Arguments.of("{packets}", "", PrometheusType.COUNTER),
Arguments.of("{packets}", "", PrometheusType.SUMMARY),
Arguments.of("{packets}", "", PrometheusType.HISTOGRAM),
// Special Case - Dropped metric units only in {}
Arguments.of("{packets}m", "meters", PrometheusType.GAUGE),
Arguments.of("{packets}m", "meters", PrometheusType.COUNTER),
Arguments.of("{packets}m", "meters", PrometheusType.SUMMARY),
Arguments.of("{packets}m", "meters", PrometheusType.HISTOGRAM),
// Special Case - Dropped metric units with 'per' unit handling applicable
Arguments.of("{scanned}/{returned}", "", PrometheusType.GAUGE),
Arguments.of("{scanned}/{returned}", "", PrometheusType.COUNTER),
Arguments.of("{scanned}/{returned}", "", PrometheusType.SUMMARY),
Arguments.of("{scanned}/{returned}", "", PrometheusType.HISTOGRAM),
// Special Case - Dropped metric units with 'per' unit handling applicable
Arguments.of("{objects}/s", "per_second", PrometheusType.GAUGE),
Arguments.of("{objects}/s", "per_second", PrometheusType.COUNTER),
Arguments.of("{objects}/s", "per_second", PrometheusType.SUMMARY),
Arguments.of("{objects}/s", "per_second", PrometheusType.HISTOGRAM),
// Units expressing rate - 'per' units
Arguments.of("km/h", "km_per_hour", PrometheusType.GAUGE),
Arguments.of("km/h", "km_per_hour", PrometheusType.COUNTER),
Arguments.of("km/h", "km_per_hour", PrometheusType.SUMMARY),
Arguments.of("km/h", "km_per_hour", PrometheusType.HISTOGRAM),
// Units expressing rate - 'per' units, both units expanded
Arguments.of("m/s", "meters_per_second", PrometheusType.GAUGE),
Arguments.of("m/s", "meters_per_second", PrometheusType.COUNTER),
Arguments.of("m/s", "meters_per_second", PrometheusType.SUMMARY),
Arguments.of("m/s", "meters_per_second", PrometheusType.HISTOGRAM),
// Misc - unsupported symbols are replaced with _
Arguments.of("°F", "F", PrometheusType.GAUGE),
Arguments.of("°F", "F", PrometheusType.COUNTER),
Arguments.of("°F", "F", PrometheusType.SUMMARY),
Arguments.of("°F", "F", PrometheusType.HISTOGRAM),
// Misc - multiple unsupported symbols are replaced with single _
Arguments.of("unit+=.:,!* & #unused", "unit_unused", PrometheusType.GAUGE),
Arguments.of("unit+=.:,!* & #unused", "unit_unused", PrometheusType.COUNTER),
Arguments.of("unit+=.:,!* & #unused", "unit_unused", PrometheusType.SUMMARY),
Arguments.of("unit+=.:,!* & #unused", "unit_unused", PrometheusType.HISTOGRAM),
// Misc - unsupported runes in 'per' units
Arguments.of("__test $/°C", "test_per_C", PrometheusType.GAUGE),
Arguments.of("__test $/°C", "test_per_C", PrometheusType.COUNTER),
Arguments.of("__test $/°C", "test_per_C", PrometheusType.SUMMARY),
Arguments.of("__test $/°C", "test_per_C", PrometheusType.HISTOGRAM),
// Misc - Special supported symbols
Arguments.of("$", "dollars", PrometheusType.GAUGE),
Arguments.of("$", "dollars", PrometheusType.COUNTER),
Arguments.of("$", "dollars", PrometheusType.SUMMARY),
Arguments.of("$", "dollars", PrometheusType.HISTOGRAM),
// Empty Units - whitespace
Arguments.of("\t", "", PrometheusType.GAUGE),
Arguments.of("\t", "", PrometheusType.COUNTER),
Arguments.of("\t", "", PrometheusType.SUMMARY),
Arguments.of("\t", "", PrometheusType.HISTOGRAM),
// Null unit
Arguments.of(null, null, PrometheusType.GAUGE),
Arguments.of(null, null, PrometheusType.COUNTER),
Arguments.of(null, null, PrometheusType.SUMMARY),
Arguments.of(null, null, PrometheusType.HISTOGRAM),
// Misc - unit cleanup - no case match special char
Arguments.of("$1000", "1000", PrometheusType.GAUGE),
Arguments.of("$1000", "1000", PrometheusType.COUNTER),
Arguments.of("$1000", "1000", PrometheusType.SUMMARY),
Arguments.of("$1000", "1000", PrometheusType.HISTOGRAM),
// Misc - unit cleanup - no case match whitespace
Arguments.of("a b !!", "a_b", PrometheusType.GAUGE),
Arguments.of("a b !!", "a_b", PrometheusType.COUNTER),
Arguments.of("a b !!", "a_b", PrometheusType.SUMMARY),
Arguments.of("a b !!", "a_b", PrometheusType.HISTOGRAM));
}
}
Loading