Skip to content

UTF-8 support in metric and label names #1255

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

Open
wants to merge 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5445c1d
Add UTF-8 support in metric and label names
fedetorres93 Jan 3, 2025
7939aba
Add grouping key escaping for URLs in Pushgateway exporter
fedetorres93 Jan 7, 2025
b5260be
Merge branch 'main' into ftorres/utf-8
fedetorres93 Jan 9, 2025
8444ae9
Merge branch 'main' into ftorres/utf-8
fedetorres93 Jan 20, 2025
a593d4e
Fix escaping bugs
fedetorres93 Jan 21, 2025
5f9726f
Merge branch 'main' into ftorres/utf-8
fedetorres93 Jul 8, 2025
f9ce942
Fix tests
fedetorres93 Jul 10, 2025
a64dda8
Make statics in PrometheusNaming final
fedetorres93 Jul 15, 2025
afc3156
Fix write errors in tests
fedetorres93 Jul 19, 2025
314838d
Add getter for nameValidationScheme
fedetorres93 Jul 25, 2025
ce9aeb2
Change NameType accessibility
fedetorres93 Aug 6, 2025
65bfb6b
Remove prefix argument from load method in NamingProperties
fedetorres93 Aug 6, 2025
e918ca3
Remove redundant null check in initValidationScheme
fedetorres93 Aug 6, 2025
e2aacd5
Refactor escaped snapshots creation
fedetorres93 Aug 6, 2025
7204288
Extract common logic in escaping tests
fedetorres93 Aug 7, 2025
891d28d
Parameterize testFindWriter
fedetorres93 Aug 7, 2025
8143f69
Add escaping scheme to ExporterPushgatewayProperties
fedetorres93 Aug 7, 2025
2f5bd9c
Formatting
fedetorres93 Aug 7, 2025
861b8a2
Update comment
fedetorres93 Aug 7, 2025
b98e054
Remove variable reassignment in escaping tests
fedetorres93 Aug 7, 2025
263d805
use string builder for performance
zeitlinger Aug 8, 2025
27362ee
naming
zeitlinger Aug 8, 2025
16a8fd8
Merge remote-tracking branch 'origin/main' into ftorres/utf-8
zeitlinger Aug 8, 2025
c5de174
fix merge
zeitlinger Aug 8, 2025
3418c7e
format
zeitlinger Aug 8, 2025
717dbfb
use parameterized tests
zeitlinger Aug 8, 2025
1a60037
use parameterized tests
zeitlinger Aug 8, 2025
a1b3226
use parameterized tests
zeitlinger Aug 8, 2025
2d11b31
use parameterized tests
zeitlinger Aug 8, 2025
5734772
avoid mocks
zeitlinger Aug 8, 2025
5eb637a
avoid mocks
zeitlinger Aug 8, 2025
ed474d6
format
zeitlinger Aug 8, 2025
81802aa
format
zeitlinger Aug 8, 2025
075ce06
format
zeitlinger Aug 8, 2025
2949497
format
zeitlinger Aug 8, 2025
5017849
format
zeitlinger Aug 8, 2025
15e98bb
format
zeitlinger Aug 8, 2025
3b61ad1
format
zeitlinger Aug 8, 2025
00835eb
format
zeitlinger Aug 8, 2025
be6bd43
cleanup
zeitlinger Aug 8, 2025
6498aca
be strict about validation scheme validation
zeitlinger Aug 8, 2025
c5b70bc
coverage
zeitlinger Aug 8, 2025
bc859f3
reject empty labels
zeitlinger Aug 8, 2025
4e406d0
coverage
zeitlinger Aug 8, 2025
01d21c6
fix test
zeitlinger Aug 8, 2025
109e077
coverage
zeitlinger Aug 8, 2025
cddd829
ensure binary compatibility
zeitlinger Aug 8, 2025
f37fe6b
javadoc
zeitlinger Aug 8, 2025
cca01f6
coverage
zeitlinger Aug 8, 2025
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
Expand Up @@ -3,6 +3,7 @@
import io.prometheus.metrics.expositionformats.ExpositionFormatWriter;
import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter;
import io.prometheus.metrics.model.snapshots.EscapingScheme;
import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
import io.prometheus.metrics.model.snapshots.GaugeSnapshot.GaugeDataPointSnapshot;
import io.prometheus.metrics.model.snapshots.Labels;
Expand Down Expand Up @@ -69,14 +70,15 @@ public OutputStream openMetricsWriteToByteArray(WriterState writerState) throws
// avoid growing the array
ByteArrayOutputStream byteArrayOutputStream = writerState.byteArrayOutputStream;
byteArrayOutputStream.reset();
OPEN_METRICS_TEXT_FORMAT_WRITER.write(byteArrayOutputStream, SNAPSHOTS);
OPEN_METRICS_TEXT_FORMAT_WRITER.write(
byteArrayOutputStream, SNAPSHOTS, EscapingScheme.NO_ESCAPING);
return byteArrayOutputStream;
}

@Benchmark
public OutputStream openMetricsWriteToNull() throws IOException {
OutputStream nullOutputStream = NullOutputStream.INSTANCE;
OPEN_METRICS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS);
OPEN_METRICS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS, EscapingScheme.NO_ESCAPING);
return nullOutputStream;
}

Expand All @@ -85,14 +87,15 @@ public OutputStream prometheusWriteToByteArray(WriterState writerState) throws I
// avoid growing the array
ByteArrayOutputStream byteArrayOutputStream = writerState.byteArrayOutputStream;
byteArrayOutputStream.reset();
PROMETHEUS_TEXT_FORMAT_WRITER.write(byteArrayOutputStream, SNAPSHOTS);
PROMETHEUS_TEXT_FORMAT_WRITER.write(
byteArrayOutputStream, SNAPSHOTS, EscapingScheme.NO_ESCAPING);
return byteArrayOutputStream;
}

@Benchmark
public OutputStream prometheusWriteToNull() throws IOException {
OutputStream nullOutputStream = NullOutputStream.INSTANCE;
PROMETHEUS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS);
PROMETHEUS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS, EscapingScheme.NO_ESCAPING);
return nullOutputStream;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ public class ExporterPushgatewayProperties {
private static final String ADDRESS = "address";
private static final String JOB = "job";
private static final String SCHEME = "scheme";
private static final String ESCAPING_SCHEME = "escapingScheme";
private static final String PREFIX = "io.prometheus.exporter.pushgateway";
private final String scheme;
private final String address;
private final String job;
private final String escapingScheme;

private ExporterPushgatewayProperties(String address, String job, String scheme) {
private ExporterPushgatewayProperties(
String address, String job, String scheme, String escapingScheme) {
this.address = address;
this.job = job;
this.scheme = scheme;
this.escapingScheme = escapingScheme;
}

/** Address of the Pushgateway in the form {@code host:port}. Default is {@code localhost:9091} */
Expand All @@ -39,6 +43,14 @@ public String getScheme() {
return scheme;
}

/**
* Escaping scheme to be used when pushing metric data to the pushgateway. Valid values:
* "no-escaping", "values", "underscores", "dots". Default is "no-escaping".
*/
public String getEscapingScheme() {
return escapingScheme;
}

/**
* Note that this will remove entries from {@code properties}. This is because we want to know if
* there are unused properties remaining after all properties have been loaded.
Expand All @@ -48,6 +60,8 @@ static ExporterPushgatewayProperties load(Map<Object, Object> properties)
String address = Util.loadString(PREFIX + "." + ADDRESS, properties);
String job = Util.loadString(PREFIX + "." + JOB, properties);
String scheme = Util.loadString(PREFIX + "." + SCHEME, properties);
String escapingScheme = Util.loadString(PREFIX + "." + ESCAPING_SCHEME, properties);

if (scheme != null) {
if (!scheme.equals("http") && !scheme.equals("https")) {
throw new PrometheusPropertiesException(
Expand All @@ -56,6 +70,20 @@ static ExporterPushgatewayProperties load(Map<Object, Object> properties)
PREFIX, SCHEME, scheme));
}
}
return new ExporterPushgatewayProperties(address, job, scheme);

if (escapingScheme != null) {
if (!escapingScheme.equals("no-escaping")
&& !escapingScheme.equals("values")
&& !escapingScheme.equals("underscores")
&& !escapingScheme.equals("dots")) {
throw new PrometheusPropertiesException(
String.format(
"%s.%s: Illegal value. Expecting 'no-escaping', 'values', 'underscores', "
+ "or 'dots'. Found: %s",
PREFIX, ESCAPING_SCHEME, escapingScheme));
}
}

return new ExporterPushgatewayProperties(address, job, scheme, escapingScheme);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.prometheus.metrics.config;

import java.util.Map;
import javax.annotation.Nullable;

public class NamingProperties {

private static final String PREFIX = "io.prometheus.naming";
private static final String VALIDATION_SCHEME = "validationScheme";
private final ValidationScheme validationScheme;

private NamingProperties(ValidationScheme validation) {
this.validationScheme = validation;
}

public ValidationScheme getValidationScheme() {
return validationScheme;
}

static NamingProperties load(Map<Object, Object> properties)
throws PrometheusPropertiesException {
String validationScheme = Util.loadString(PREFIX + "." + VALIDATION_SCHEME, properties);
return new NamingProperties(parseValidationScheme(validationScheme));
}

static ValidationScheme parseValidationScheme(@Nullable String scheme) {
if (scheme == null || scheme.isEmpty()) {
return ValidationScheme.LEGACY_VALIDATION;
}

switch (scheme) {
case "utf-8":
return ValidationScheme.UTF_8_VALIDATION;
case "legacy":
return ValidationScheme.LEGACY_VALIDATION;
default:
throw new PrometheusPropertiesException(
"Unknown validation scheme: " + scheme + ". Valid values are: utf-8, legacy.");
}
}

public static Builder builder() {
return new Builder();
}

public static class Builder {

private ValidationScheme validationScheme;

private Builder() {}

public Builder validation(ValidationScheme validationScheme) {
this.validationScheme = validationScheme;
return this;
}

public NamingProperties build() {
return new NamingProperties(validationScheme);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class PrometheusProperties {
private final ExporterHttpServerProperties exporterHttpServerProperties;
private final ExporterOpenTelemetryProperties exporterOpenTelemetryProperties;
private final ExporterPushgatewayProperties exporterPushgatewayProperties;
private final NamingProperties namingProperties;

/**
* Get the properties instance. When called for the first time, {@code get()} loads the properties
Expand Down Expand Up @@ -48,7 +49,8 @@ public PrometheusProperties(
ExporterFilterProperties exporterFilterProperties,
ExporterHttpServerProperties httpServerConfig,
ExporterPushgatewayProperties pushgatewayProperties,
ExporterOpenTelemetryProperties otelConfig) {
ExporterOpenTelemetryProperties otelConfig,
NamingProperties namingProperties) {
this.defaultMetricsProperties = defaultMetricsProperties;
this.metricProperties.putAll(metricProperties);
this.exemplarProperties = exemplarProperties;
Expand All @@ -57,6 +59,7 @@ public PrometheusProperties(
this.exporterHttpServerProperties = httpServerConfig;
this.exporterPushgatewayProperties = pushgatewayProperties;
this.exporterOpenTelemetryProperties = otelConfig;
this.namingProperties = namingProperties;
}

/**
Expand Down Expand Up @@ -100,6 +103,10 @@ public ExporterOpenTelemetryProperties getExporterOpenTelemetryProperties() {
return exporterOpenTelemetryProperties;
}

public NamingProperties getNamingProperties() {
return namingProperties;
}

public static class Builder {
private MetricsProperties defaultMetricsProperties;
private Map<String, MetricsProperties> metricProperties = new HashMap<>();
Expand All @@ -109,6 +116,7 @@ public static class Builder {
private ExporterHttpServerProperties exporterHttpServerProperties;
private ExporterPushgatewayProperties pushgatewayProperties;
private ExporterOpenTelemetryProperties otelConfig;
private NamingProperties namingProperties;

private Builder() {}

Expand Down Expand Up @@ -160,6 +168,11 @@ public Builder exporterOpenTelemetryProperties(
return this;
}

public Builder namingProperties(NamingProperties namingProperties) {
this.namingProperties = namingProperties;
return this;
}

public PrometheusProperties build() {
return new PrometheusProperties(
defaultMetricsProperties,
Expand All @@ -169,7 +182,8 @@ public PrometheusProperties build() {
exporterFilterProperties,
exporterHttpServerProperties,
pushgatewayProperties,
otelConfig);
otelConfig,
namingProperties);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public static PrometheusProperties load(Map<Object, Object> externalProperties)
ExporterPushgatewayProperties.load(properties);
ExporterOpenTelemetryProperties exporterOpenTelemetryProperties =
ExporterOpenTelemetryProperties.load(properties);
NamingProperties namingProperties = NamingProperties.load(properties);
validateAllPropertiesProcessed(properties);
return new PrometheusProperties(
defaultMetricsProperties,
Expand All @@ -51,7 +52,8 @@ public static PrometheusProperties load(Map<Object, Object> externalProperties)
exporterFilterProperties,
exporterHttpServerProperties,
exporterPushgatewayProperties,
exporterOpenTelemetryProperties);
exporterOpenTelemetryProperties,
namingProperties);
}

// This will remove entries from properties when they are processed.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.prometheus.metrics.config;

/**
* ValidationScheme is an enum for determining how metric and label names will be validated by this
* library.
*/
public enum ValidationScheme {
/**
* LEGACY_VALIDATION is a setting that requires that metric and label names conform to the
* original character requirements.
*/
LEGACY_VALIDATION,

/** UTF_8_VALIDATION only requires that metric and label names be valid UTF-8 strings. */
UTF_8_VALIDATION
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.prometheus.metrics.config;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;

import org.junit.jupiter.api.Test;

class NamingPropertiesTest {
@Test
void testBuilder() {
NamingProperties properties =
NamingProperties.builder().validation(ValidationScheme.UTF_8_VALIDATION).build();
assertThat(properties.getValidationScheme()).isEqualTo(ValidationScheme.UTF_8_VALIDATION);
}

@Test
void parseValidationScheme() {
assertThat(NamingProperties.parseValidationScheme("utf-8"))
.isEqualTo(ValidationScheme.UTF_8_VALIDATION);
assertThat(NamingProperties.parseValidationScheme("legacy"))
.isEqualTo(ValidationScheme.LEGACY_VALIDATION);
assertThat(NamingProperties.parseValidationScheme(null))
.isEqualTo(ValidationScheme.LEGACY_VALIDATION);
assertThatCode(() -> NamingProperties.parseValidationScheme("unknown"))
.isInstanceOf(PrometheusPropertiesException.class)
.hasMessageContaining(
"Unknown validation scheme: unknown. Valid values are: utf-8, legacy.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Properties;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -37,15 +38,20 @@ public void testBuilder() {
PrometheusProperties defaults = PrometheusPropertiesLoader.load(new HashMap<>());
PrometheusProperties.Builder builder = PrometheusProperties.builder();
builder.defaultMetricsProperties(defaults.getDefaultMetricProperties());
builder.metricProperties(
Collections.singletonMap(
"http_duration_seconds",
MetricsProperties.builder().histogramClassicUpperBounds(0.1, 0.2, 0.5, 1.0).build()));
builder.exemplarProperties(defaults.getExemplarProperties());
builder.defaultMetricsProperties(defaults.getDefaultMetricProperties());
builder.exporterFilterProperties(defaults.getExporterFilterProperties());
builder.exporterHttpServerProperties(defaults.getExporterHttpServerProperties());
builder.exporterOpenTelemetryProperties(defaults.getExporterOpenTelemetryProperties());
builder.pushgatewayProperties(defaults.getExporterPushgatewayProperties());
builder.exporterProperties(defaults.getExporterProperties());
builder.namingProperties(defaults.getNamingProperties());
PrometheusProperties result = builder.build();
assertThat(result.getDefaultMetricProperties()).isSameAs(defaults.getDefaultMetricProperties());
assertThat(result.getDefaultMetricProperties()).isSameAs(defaults.getDefaultMetricProperties());
assertThat(result.getExemplarProperties()).isSameAs(defaults.getExemplarProperties());
assertThat(result.getExporterFilterProperties())
.isSameAs(defaults.getExporterFilterProperties());
Expand All @@ -55,5 +61,12 @@ public void testBuilder() {
.isSameAs(defaults.getExporterOpenTelemetryProperties());
assertThat(result.getExporterPushgatewayProperties())
.isSameAs(defaults.getExporterPushgatewayProperties());
assertThat(result.getMetricProperties("http_duration_seconds"))
.usingRecursiveComparison()
.isEqualTo(
MetricsProperties.builder().histogramClassicUpperBounds(0.1, 0.2, 0.5, 1.0).build());
assertThat(result.getMetricProperties("unknown_metric")).isNull();
assertThat(result.getExporterProperties()).isSameAs(defaults.getExporterProperties());
assertThat(result.getNamingProperties()).isSameAs(defaults.getNamingProperties());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@
import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_31_1.Metrics;
import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl;
import io.prometheus.metrics.expositionformats.internal.ProtobufUtil;
import io.prometheus.metrics.model.snapshots.ClassicHistogramBucket;
import io.prometheus.metrics.model.snapshots.Exemplar;
import io.prometheus.metrics.model.snapshots.Exemplars;
import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
import io.prometheus.metrics.model.snapshots.Labels;
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
import io.prometheus.metrics.model.snapshots.*;
import io.prometheus.metrics.tracer.common.SpanContext;
import io.prometheus.metrics.tracer.initializer.SpanContextSupplier;
import java.io.ByteArrayOutputStream;
Expand Down Expand Up @@ -952,7 +947,7 @@ public void testDefaults() throws IOException {
// text
ByteArrayOutputStream out = new ByteArrayOutputStream();
OpenMetricsTextFormatWriter writer = new OpenMetricsTextFormatWriter(false, true);
writer.write(out, MetricSnapshots.of(snapshot));
writer.write(out, MetricSnapshots.of(snapshot), EscapingScheme.NO_ESCAPING);
assertThat(out).hasToString(expectedTextFormat);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_31_1.Metrics;
import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl;
import io.prometheus.metrics.expositionformats.internal.ProtobufUtil;
import io.prometheus.metrics.model.snapshots.EscapingScheme;
import io.prometheus.metrics.model.snapshots.Labels;
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
import io.prometheus.metrics.model.snapshots.Unit;
Expand Down Expand Up @@ -126,7 +127,7 @@ public void testConstLabelsDuplicate2() {
private void assertTextFormat(String expected, Info info) throws IOException {
OpenMetricsTextFormatWriter writer = new OpenMetricsTextFormatWriter(true, true);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
writer.write(outputStream, MetricSnapshots.of(info.collect()));
writer.write(outputStream, MetricSnapshots.of(info.collect()), EscapingScheme.NO_ESCAPING);
String result = outputStream.toString(StandardCharsets.UTF_8.name());
if (!result.contains(expected)) {
throw new AssertionError(expected + " is not contained in the following output:\n" + result);
Expand Down
Loading