Skip to content

Commit 25efcde

Browse files
authored
Merge branch 'main' into renovate/protobuf
2 parents e8ea54a + 2d0f508 commit 25efcde

5 files changed

Lines changed: 156 additions & 25 deletions

File tree

examples/example-otel-jvm-runtime-metrics/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<dependency>
2929
<groupId>io.opentelemetry.instrumentation</groupId>
3030
<artifactId>opentelemetry-instrumentation-bom-alpha</artifactId>
31-
<version>2.27.0-alpha</version>
31+
<version>2.28.0-alpha</version>
3232
<type>pom</type>
3333
<scope>import</scope>
3434
</dependency>
@@ -51,7 +51,7 @@
5151
</dependency>
5252
<dependency>
5353
<groupId>io.opentelemetry.instrumentation</groupId>
54-
<artifactId>opentelemetry-runtime-telemetry-java8</artifactId>
54+
<artifactId>opentelemetry-runtime-telemetry</artifactId>
5555
</dependency>
5656
</dependencies>
5757

examples/example-otel-jvm-runtime-metrics/src/main/java/io/prometheus/metrics/examples/otelruntimemetrics/Main.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package io.prometheus.metrics.examples.otelruntimemetrics;
22

33
import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
4-
import io.opentelemetry.instrumentation.runtimemetrics.java8.RuntimeMetrics;
4+
import io.opentelemetry.instrumentation.runtimetelemetry.RuntimeTelemetry;
55
import io.opentelemetry.sdk.OpenTelemetrySdk;
66
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
77
import io.prometheus.metrics.core.metrics.Counter;
@@ -47,11 +47,7 @@ public static void main(String[] args) throws IOException, InterruptedException
4747
.build();
4848

4949
// 4. Start OTel JVM runtime metrics collection.
50-
// - captureGcCause() adds a jvm.gc.cause attribute to jvm.gc.duration
51-
// - emitExperimentalTelemetry() enables buffer pools, extended CPU,
52-
// extended memory pools, and file descriptor metrics
53-
RuntimeMetrics runtimeMetrics =
54-
RuntimeMetrics.builder(openTelemetry).captureGcCause().emitExperimentalTelemetry().build();
50+
RuntimeTelemetry runtimeMetrics = RuntimeTelemetry.create(openTelemetry);
5551

5652
// 5. Expose both Prometheus and OTel metrics on a single endpoint.
5753
HTTPServer server = HTTPServer.builder().port(9400).registry(registry).buildAndStart();

prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717
*/
1818
public class PrometheusNaming {
1919

20+
/**
21+
* Reserved metric name suffixes. These suffixes are automatically appended by Prometheus
22+
* exposition format writers for specific metric types: {@code _total} and {@code _created} for
23+
* counters, {@code _info} for info metrics, and {@code _bucket} for histograms. Including these
24+
* in a base metric name via {@link #sanitizeMetricName(String)} would cause confusion or
25+
* double-suffixing, so they are stripped during sanitization.
26+
*/
27+
static final String[] RESERVED_METRIC_NAME_SUFFIXES = {
28+
"_total", "_created", "_bucket", "_info", ".total", ".created", ".bucket", ".info"
29+
};
30+
2031
/**
2132
* Test if a metric name is valid. Any non-empty valid UTF-8 string is accepted.
2233
*
@@ -35,7 +46,9 @@ public class PrometheusNaming {
3546
* format, this will be represented as two values: {@code processing_time_seconds_total} for the
3647
* counter value, and the optional {@code processing_time_seconds_created} timestamp.
3748
*
38-
* <p>Use {@link #sanitizeMetricName(String)} to convert arbitrary Strings to valid metric names.
49+
* <p>Use {@link #sanitizeMetricName(String)} for compatibility-preserving sanitization that
50+
* strips reserved suffixes, or {@link #normalizeMetricName(String)} for permissive normalization
51+
* that keeps the original suffixes intact.
3952
*/
4053
public static boolean isValidMetricName(String name) {
4154
return validateMetricName(name) == null;
@@ -153,16 +166,50 @@ public static String prometheusName(String name) {
153166
}
154167

155168
/**
156-
* Convert an arbitrary string to a valid metric name. Since any non-empty valid UTF-8 string is a
157-
* valid metric name, this simply returns the input unchanged.
169+
* Convert an arbitrary string to a valid metric name.
170+
*
171+
* <p>Reserved metric name suffixes ({@code _total}, {@code _created}, {@code _bucket}, {@code
172+
* _info} and their dot variants) are stripped. These suffixes are appended automatically by
173+
* Prometheus exposition format writers, so including them in a base metric name would result in
174+
* double-suffixing or unintended type inference. For example, a JMX attribute named {@code
175+
* RequestTotal} would be sanitized from {@code kafka_consumer_request_total} to {@code
176+
* kafka_consumer_request}, and the counter writer would add {@code _total} back at scrape time.
177+
*
178+
* <p>This behaviour was present in client_java 1.5.x and is restored here to fix a regression
179+
* introduced in 1.6.0 that affected downstream tools (e.g. the JMX Exporter and the simpleclient
180+
* bridge) which relied on {@code sanitizeMetricName} to strip these suffixes before passing names
181+
* to the snapshot builders.
182+
*
183+
* <p>If you want permissive normalization that keeps reserved suffixes intact, use {@link
184+
* #normalizeMetricName(String)} instead.
158185
*
159186
* @throws IllegalArgumentException if the input is empty
160187
*/
161188
public static String sanitizeMetricName(String metricName) {
162189
if (metricName.isEmpty()) {
163190
throw new IllegalArgumentException("Cannot convert an empty string to a valid metric name.");
164191
}
165-
return metricName;
192+
String sanitizedName = metricName;
193+
boolean stripped = true;
194+
while (stripped) {
195+
stripped = false;
196+
// When the name equals the suffix exactly, drop the leading separator character to avoid
197+
// returning an empty string (e.g. "_total" → "total", ".info" → "info").
198+
for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) {
199+
if (sanitizedName.equals(reservedSuffix)) {
200+
return reservedSuffix.substring(1);
201+
}
202+
}
203+
for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) {
204+
if (sanitizedName.endsWith(reservedSuffix)) {
205+
sanitizedName =
206+
sanitizedName.substring(0, sanitizedName.length() - reservedSuffix.length());
207+
stripped = true;
208+
break; // restart the outer loop to re-check all suffixes on the shortened name
209+
}
210+
}
211+
}
212+
return sanitizedName;
166213
}
167214

168215
/**
@@ -179,6 +226,37 @@ public static String sanitizeMetricName(String metricName, Unit unit) {
179226
return result;
180227
}
181228

229+
/**
230+
* Convert an arbitrary string to a valid metric name without stripping reserved suffixes.
231+
*
232+
* <p>Any non-empty valid UTF-8 string is accepted and returned unchanged. This is the permissive
233+
* normalization behavior introduced in 1.6.0. Use this method for new integrations that want to
234+
* preserve the original metric name and rely on registration-time collision detection instead of
235+
* suffix stripping.
236+
*
237+
* @throws IllegalArgumentException if the input is empty
238+
*/
239+
public static String normalizeMetricName(String metricName) {
240+
if (metricName.isEmpty()) {
241+
throw new IllegalArgumentException("Cannot convert an empty string to a valid metric name.");
242+
}
243+
return metricName;
244+
}
245+
246+
/**
247+
* Like {@link #normalizeMetricName(String)}, but also makes sure that the unit is appended as a
248+
* suffix if the unit is not {@code null}.
249+
*/
250+
public static String normalizeMetricName(String metricName, Unit unit) {
251+
String result = normalizeMetricName(metricName);
252+
if (unit != null) {
253+
if (!result.endsWith("_" + unit) && !result.endsWith("." + unit)) {
254+
result += "_" + unit;
255+
}
256+
}
257+
return result;
258+
}
259+
182260
/**
183261
* Convert an arbitrary string to a name where {@link #isValidLabelName(String)
184262
* isValidLabelName(name)} is true.

prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/MetricMetadataTest.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,30 @@ void testSanitizationIllegalCharacters() {
3535

3636
@Test
3737
void testNameWithTotalSuffix() {
38+
// sanitizeMetricName strips the reserved _total suffix.
3839
MetricMetadata metadata = new MetricMetadata(sanitizeMetricName("my_events_total"));
39-
assertThat(metadata.getName()).isEqualTo("my_events_total");
40+
assertThat(metadata.getName()).isEqualTo("my_events");
4041
}
4142

4243
@Test
4344
void testNameWithInfoSuffix() {
45+
// sanitizeMetricName strips the reserved _info suffix.
4446
MetricMetadata metadata = new MetricMetadata(sanitizeMetricName("target_info"));
45-
assertThat(metadata.getName()).isEqualTo("target_info");
47+
assertThat(metadata.getName()).isEqualTo("target");
4648
}
4749

4850
@Test
4951
void testNameWithCreatedSuffix() {
52+
// sanitizeMetricName strips the reserved _created suffix.
5053
MetricMetadata metadata = new MetricMetadata(sanitizeMetricName("my_events_created"));
51-
assertThat(metadata.getName()).isEqualTo("my_events_created");
54+
assertThat(metadata.getName()).isEqualTo("my_events");
5255
}
5356

5457
@Test
5558
void testNameWithBucketSuffix() {
59+
// sanitizeMetricName strips the reserved _bucket suffix.
5660
MetricMetadata metadata = new MetricMetadata(sanitizeMetricName("my_histogram_bucket"));
57-
assertThat(metadata.getName()).isEqualTo("my_histogram_bucket");
61+
assertThat(metadata.getName()).isEqualTo("my_histogram");
5862
}
5963

6064
@Test

prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/snapshots/PrometheusNamingTest.java

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.escapeName;
44
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.isValidLabelName;
5+
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.normalizeMetricName;
56
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
67
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeLabelName;
78
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeMetricName;
@@ -22,27 +23,79 @@ class PrometheusNamingTest {
2223

2324
@Test
2425
void testSanitizeMetricName() {
25-
assertThat(sanitizeMetricName("my_counter_total")).isEqualTo("my_counter_total");
26-
assertThat(sanitizeMetricName("jvm.info")).isEqualTo("jvm.info");
27-
assertThat(sanitizeMetricName("jvm_info")).isEqualTo("jvm_info");
26+
// Reserved suffixes are stripped to avoid confusion with Prometheus type conventions.
27+
assertThat(sanitizeMetricName("my_counter_total")).isEqualTo("my_counter");
28+
assertThat(sanitizeMetricName("jvm.info")).isEqualTo("jvm");
29+
assertThat(sanitizeMetricName("jvm_info")).isEqualTo("jvm");
2830
assertThat(sanitizeMetricName("a.b")).isEqualTo("a.b");
29-
assertThat(sanitizeMetricName("_total")).isEqualTo("_total");
31+
// "_total" / ".total" corner cases: the suffix is the entire name, so the separator
32+
// character is dropped to avoid returning an empty string.
33+
assertThat(sanitizeMetricName("_total")).isEqualTo("total");
34+
assertThat(sanitizeMetricName(".total")).isEqualTo("total");
3035
assertThat(sanitizeMetricName("total")).isEqualTo("total");
31-
assertThat(sanitizeMetricName("my_events_created")).isEqualTo("my_events_created");
32-
assertThat(sanitizeMetricName("my_histogram_bucket")).isEqualTo("my_histogram_bucket");
36+
assertThat(sanitizeMetricName("my_events_created")).isEqualTo("my_events");
37+
assertThat(sanitizeMetricName("my_histogram_bucket")).isEqualTo("my_histogram");
38+
}
39+
40+
/**
41+
* Regression test: reserved suffixes must be stripped even when the raw name comes from an
42+
* external system (e.g. JMX Exporter converting a JMX attribute named {@code "Total"} into a
43+
* Prometheus name {@code kafka_consumer_request_total}).
44+
*
45+
* <p>Without stripping, an UNKNOWN metric would be stored under {@code
46+
* kafka_consumer_request_total} instead of {@code kafka_consumer_request}, breaking registry
47+
* lookups by the expected base name and potentially triggering unintended counter-type inference
48+
* in tools that check for the {@code _total} suffix.
49+
*/
50+
@Test
51+
void testSanitizeMetricNameStripsReservedSuffixForDownstreamTools() {
52+
// A JMX attribute "Total" produces "kafka_consumer_request_total" as the raw name.
53+
// sanitizeMetricName must strip "_total" so that the metric is stored and looked up under
54+
// "kafka_consumer_request", not "kafka_consumer_request_total".
55+
assertThat(sanitizeMetricName("kafka_consumer_request_total"))
56+
.isEqualTo("kafka_consumer_request");
57+
// Dot variant is stripped too.
58+
assertThat(sanitizeMetricName("kafka_consumer_request.total"))
59+
.isEqualTo("kafka_consumer_request");
60+
// Multiple chained reserved suffixes are stripped iteratively.
61+
assertThat(sanitizeMetricName("events_total_created")).isEqualTo("events");
3362
}
3463

3564
@Test
3665
void testSanitizeMetricNameWithUnit() {
3766
assertThat(prometheusName(sanitizeMetricName("def", Unit.RATIO)))
3867
.isEqualTo("def_" + Unit.RATIO);
68+
// _total is stripped first, then the unit is appended.
3969
assertThat(prometheusName(sanitizeMetricName("my_counter_total", Unit.RATIO)))
40-
.isEqualTo("my_counter_total_" + Unit.RATIO);
41-
assertThat(sanitizeMetricName("jvm.info", Unit.RATIO)).isEqualTo("jvm.info_" + Unit.RATIO);
42-
assertThat(sanitizeMetricName("_total", Unit.RATIO)).isEqualTo("_total_" + Unit.RATIO);
70+
.isEqualTo("my_counter_" + Unit.RATIO);
71+
assertThat(sanitizeMetricName("jvm.info", Unit.RATIO)).isEqualTo("jvm_" + Unit.RATIO);
72+
assertThat(sanitizeMetricName("_total", Unit.RATIO)).isEqualTo("total_" + Unit.RATIO);
4373
assertThat(sanitizeMetricName("total", Unit.RATIO)).isEqualTo("total_" + Unit.RATIO);
4474
}
4575

76+
@Test
77+
void testNormalizeMetricName() {
78+
assertThat(normalizeMetricName("my_counter_total")).isEqualTo("my_counter_total");
79+
assertThat(normalizeMetricName("jvm.info")).isEqualTo("jvm.info");
80+
assertThat(normalizeMetricName("jvm_info")).isEqualTo("jvm_info");
81+
assertThat(normalizeMetricName("a.b")).isEqualTo("a.b");
82+
assertThat(normalizeMetricName("_total")).isEqualTo("_total");
83+
assertThat(normalizeMetricName(".total")).isEqualTo(".total");
84+
assertThat(normalizeMetricName("my_events_created")).isEqualTo("my_events_created");
85+
assertThat(normalizeMetricName("my_histogram_bucket")).isEqualTo("my_histogram_bucket");
86+
}
87+
88+
@Test
89+
void testNormalizeMetricNameWithUnit() {
90+
assertThat(prometheusName(normalizeMetricName("def", Unit.RATIO)))
91+
.isEqualTo("def_" + Unit.RATIO);
92+
assertThat(prometheusName(normalizeMetricName("my_counter_total", Unit.RATIO)))
93+
.isEqualTo("my_counter_total_" + Unit.RATIO);
94+
assertThat(normalizeMetricName("jvm.info", Unit.RATIO)).isEqualTo("jvm.info_" + Unit.RATIO);
95+
assertThat(normalizeMetricName("_total", Unit.RATIO)).isEqualTo("_total_" + Unit.RATIO);
96+
assertThat(normalizeMetricName("total", Unit.RATIO)).isEqualTo("total_" + Unit.RATIO);
97+
}
98+
4699
@Test
47100
void testSanitizeLabelName() {
48101
assertThat(prometheusName(sanitizeLabelName("0abc.def"))).isEqualTo("_abc_def");

0 commit comments

Comments
 (0)