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

UTF-8 support in metric and label names #922

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.prometheus.metrics.config;

import java.util.Map;

public class NamingProperties {

private static final String VALIDATION_SCHEME = "validationScheme";
private final String validationScheme;

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

public String getValidationScheme() {
return validationScheme;
}

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

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

public static class Builder {

private String validationScheme;

private Builder() {}

public Builder validation(String 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 @@ -19,6 +19,7 @@ public class PrometheusProperties {
private final ExporterFilterProperties exporterFilterProperties;
private final ExporterHttpServerProperties exporterHttpServerProperties;
private final ExporterOpenTelemetryProperties exporterOpenTelemetryProperties;
private final NamingProperties namingProperties;

/**
* Get the properties instance. When called for the first time, {@code get()} loads the properties from the following locations:
Expand All @@ -39,14 +40,16 @@ public PrometheusProperties(
ExporterProperties exporterProperties,
ExporterFilterProperties exporterFilterProperties,
ExporterHttpServerProperties httpServerConfig,
ExporterOpenTelemetryProperties otelConfig) {
ExporterOpenTelemetryProperties otelConfig,
NamingProperties namingProperties) {
this.defaultMetricsProperties = defaultMetricsProperties;
this.metricProperties.putAll(metricProperties);
this.exemplarProperties = exemplarProperties;
this.exporterProperties = exporterProperties;
this.exporterFilterProperties = exporterFilterProperties;
this.exporterHttpServerProperties = httpServerConfig;
this.exporterOpenTelemetryProperties = otelConfig;
this.namingProperties = namingProperties;
}

/**
Expand Down Expand Up @@ -83,4 +86,8 @@ public ExporterHttpServerProperties getExporterHttpServerProperties() {
public ExporterOpenTelemetryProperties getExporterOpenTelemetryProperties() {
return exporterOpenTelemetryProperties;
}

public NamingProperties getNamingProperties() {
return namingProperties;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ public static PrometheusProperties load() throws PrometheusPropertiesException {
ExporterFilterProperties exporterFilterProperties = ExporterFilterProperties.load("io.prometheus.exporter.filter", properties);
ExporterHttpServerProperties exporterHttpServerProperties = ExporterHttpServerProperties.load("io.prometheus.exporter.httpServer", properties);
ExporterOpenTelemetryProperties exporterOpenTelemetryProperties = ExporterOpenTelemetryProperties.load("io.prometheus.exporter.opentelemetry", properties);
NamingProperties namingProperties = NamingProperties.load("io.prometheus.naming", properties);
validateAllPropertiesProcessed(properties);
return new PrometheusProperties(defaultMetricsProperties, metricsConfigs, exemplarConfig, exporterProperties, exporterFilterProperties, exporterHttpServerProperties, exporterOpenTelemetryProperties);
return new PrometheusProperties(defaultMetricsProperties, metricsConfigs, exemplarConfig, exporterProperties, exporterFilterProperties, exporterHttpServerProperties, exporterOpenTelemetryProperties, namingProperties);
}

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

import io.prometheus.metrics.model.snapshots.*;
import io.prometheus.metrics.shaded.com_google_protobuf_3_21_7.TextFormat;
import io.prometheus.metrics.core.datapoints.DistributionDataPoint;
import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfigTestUtil;
import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter;
import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_3_21_7.Metrics;
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.tracer.common.SpanContext;
import io.prometheus.metrics.tracer.initializer.SpanContextSupplier;
import org.junit.After;
Expand Down Expand Up @@ -723,7 +718,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);
Assert.assertEquals(expectedTextFormat, out.toString());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.prometheus.metrics.core.metrics;

import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
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.shaded.com_google_protobuf_3_21_7.TextFormat;
Expand Down Expand Up @@ -98,7 +99,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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.prometheus.metrics.expositionformats.ExpositionFormats;
import io.prometheus.metrics.model.registry.MetricNameFilter;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import io.prometheus.metrics.model.snapshots.EscapingScheme;
import io.prometheus.metrics.model.snapshots.MetricSnapshots;

import java.io.ByteArrayOutputStream;
Expand Down Expand Up @@ -51,15 +52,16 @@ public void handleRequest(PrometheusHttpExchange exchange) throws IOException {
PrometheusHttpRequest request = exchange.getRequest();
PrometheusHttpResponse response = exchange.getResponse();
MetricSnapshots snapshots = scrape(request);
if (writeDebugResponse(snapshots, exchange)) {
String acceptHeader = request.getHeader("Accept");
EscapingScheme escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeader);
if (writeDebugResponse(snapshots, exchange, escapingScheme)) {
return;
}
ByteArrayOutputStream responseBuffer = new ByteArrayOutputStream(lastResponseSize.get() + 1024);
String acceptHeader = request.getHeader("Accept");
ExpositionFormatWriter writer = expositionFormats.findWriter(acceptHeader);
writer.write(responseBuffer, snapshots);
writer.write(responseBuffer, snapshots, escapingScheme);
lastResponseSize.set(responseBuffer.size());
response.setHeader("Content-Type", writer.getContentType());
response.setHeader("Content-Type", writer.getContentType() + escapingScheme.toHeaderFormat());

if (shouldUseCompression(request)) {
response.setHeader("Content-Encoding", "gzip");
Expand Down Expand Up @@ -126,7 +128,7 @@ private Predicate<String> makeNameFilter(String[] includedNames) {
return result;
}

private boolean writeDebugResponse(MetricSnapshots snapshots, PrometheusHttpExchange exchange) throws IOException {
private boolean writeDebugResponse(MetricSnapshots snapshots, PrometheusHttpExchange exchange, EscapingScheme escapingScheme) throws IOException {
String debugParam = exchange.getRequest().getParameter("debug");
PrometheusHttpResponse response = exchange.getResponse();
if (debugParam == null) {
Expand All @@ -138,10 +140,10 @@ private boolean writeDebugResponse(MetricSnapshots snapshots, PrometheusHttpExch
OutputStream body = response.sendHeadersAndGetBody(responseStatus, 0);
switch (debugParam) {
case "openmetrics":
expositionFormats.getOpenMetricsTextFormatWriter().write(body, snapshots);
expositionFormats.getOpenMetricsTextFormatWriter().write(body, snapshots, escapingScheme);
break;
case "text":
expositionFormats.getPrometheusTextFormatWriter().write(body, snapshots);
expositionFormats.getPrometheusTextFormatWriter().write(body, snapshots, escapingScheme);
break;
case "prometheus-protobuf":
String debugString = expositionFormats.getPrometheusProtobufWriter().toDebugString(snapshots);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.prometheus.metrics.expositionformats;

import io.prometheus.metrics.model.snapshots.EscapingScheme;
import io.prometheus.metrics.model.snapshots.MetricSnapshots;

import java.io.IOException;
Expand All @@ -11,6 +12,6 @@ public interface ExpositionFormatWriter {
/**
* Text formats use UTF-8 encoding.
*/
void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException;
void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme escapingScheme) throws IOException;
String getContentType();
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,14 @@
package io.prometheus.metrics.expositionformats;

import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets;
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
import io.prometheus.metrics.model.snapshots.DataPointSnapshot;
import io.prometheus.metrics.model.snapshots.DistributionDataPointSnapshot;
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.HistogramSnapshot;
import io.prometheus.metrics.model.snapshots.InfoSnapshot;
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.Quantile;
import io.prometheus.metrics.model.snapshots.StateSetSnapshot;
import io.prometheus.metrics.model.snapshots.SummarySnapshot;
import io.prometheus.metrics.model.snapshots.UnknownSnapshot;
import io.prometheus.metrics.model.snapshots.*;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;

import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeDouble;
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeEscapedLabelValue;
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLabels;
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong;
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeTimestamp;
import static io.prometheus.metrics.expositionformats.TextFormatUtil.*;

/**
* Write the OpenMetrics text format as defined on <a href="https://openmetrics.io/">https://openmetrics.io</a>.
Expand Down Expand Up @@ -60,9 +40,11 @@ public String getContentType() {
return CONTENT_TYPE;
}

public void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException {
public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme escapingScheme) throws IOException {
OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
for (MetricSnapshot snapshot : metricSnapshots) {
for (MetricSnapshot s : metricSnapshots) {
MetricSnapshot snapshot = PrometheusNaming.escapeMetricSnapshot(s, escapingScheme);

if (snapshot.getDataPoints().size() > 0) {
if (snapshot instanceof CounterSnapshot) {
writeCounter(writer, (CounterSnapshot) snapshot);
Expand Down Expand Up @@ -214,15 +196,15 @@ private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot)
}
writer.write(data.getLabels().getPrometheusName(j));
writer.write("=\"");
writeEscapedLabelValue(writer, data.getLabels().getValue(j));
writeEscapedString(writer, data.getLabels().getValue(j));
writer.write("\"");
}
if (!data.getLabels().isEmpty()) {
writer.write(",");
}
writer.write(metadata.getPrometheusName());
writer.write("=\"");
writeEscapedLabelValue(writer, data.getName(i));
writeEscapedString(writer, data.getName(i));
writer.write("\"} ");
if (data.isTrue(i)) {
writer.write("1");
Expand Down Expand Up @@ -289,13 +271,21 @@ private void writeNameAndLabels(OutputStreamWriter writer, String name, String s

private void writeNameAndLabels(OutputStreamWriter writer, String name, String suffix, Labels labels,
String additionalLabelName, double additionalLabelValue) throws IOException {
writer.write(name);
if (suffix != null) {
writer.write(suffix);
boolean metricInsideBraces = false;
// If the name does not pass the legacy validity check, we must put the
// metric name inside the braces.
if (PrometheusNaming.validateLegacyMetricName(name) != null) {
metricInsideBraces = true;
writer.write('{');
}
writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric);

if (!labels.isEmpty() || additionalLabelName != null) {
writeLabels(writer, labels, additionalLabelName, additionalLabelValue);
writeLabels(writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces);
} else if (metricInsideBraces) {
writer.write('}');
}

writer.write(' ');
}

Expand All @@ -306,7 +296,7 @@ private void writeScrapeTimestampAndExemplar(OutputStreamWriter writer, DataPoin
}
if (exemplar != null) {
writer.write(" # ");
writeLabels(writer, exemplar.getLabels(), null, 0);
writeLabels(writer, exemplar.getLabels(), null, 0, false);
writer.write(' ');
writeDouble(writer, exemplar.getValue());
if (exemplar.hasTimestamp()) {
Expand All @@ -319,22 +309,22 @@ private void writeScrapeTimestampAndExemplar(OutputStreamWriter writer, DataPoin

private void writeMetadata(OutputStreamWriter writer, String typeName, MetricMetadata metadata) throws IOException {
writer.write("# TYPE ");
writer.write(metadata.getPrometheusName());
writeName(writer, metadata.getPrometheusName(), NameType.Metric);
writer.write(' ');
writer.write(typeName);
writer.write('\n');
if (metadata.getUnit() != null) {
writer.write("# UNIT ");
writer.write(metadata.getPrometheusName());
writeName(writer, metadata.getPrometheusName(), NameType.Metric);
writer.write(' ');
writeEscapedLabelValue(writer, metadata.getUnit().toString());
writeEscapedString(writer, metadata.getUnit().toString());
writer.write('\n');
}
if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
writer.write("# HELP ");
writer.write(metadata.getPrometheusName());
writeName(writer, metadata.getPrometheusName(), NameType.Metric);
writer.write(' ');
writeEscapedLabelValue(writer, metadata.getHelp());
writeEscapedString(writer, metadata.getHelp());
writer.write('\n');
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,9 @@
package io.prometheus.metrics.expositionformats;

import io.prometheus.metrics.model.snapshots.*;
import io.prometheus.metrics.shaded.com_google_protobuf_3_21_7.TextFormat;
import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_3_21_7.Metrics;
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.GaugeSnapshot;
import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
import io.prometheus.metrics.model.snapshots.InfoSnapshot;
import io.prometheus.metrics.model.snapshots.Labels;
import io.prometheus.metrics.model.snapshots.DataPointSnapshot;
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.Quantiles;
import io.prometheus.metrics.model.snapshots.StateSetSnapshot;
import io.prometheus.metrics.model.snapshots.SummarySnapshot;
import io.prometheus.metrics.model.snapshots.UnknownSnapshot;

import java.io.IOException;
import java.io.OutputStream;
Expand Down Expand Up @@ -61,7 +46,7 @@ public String toDebugString(MetricSnapshots metricSnapshots) {
}

@Override
public void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException {
public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme escapingScheme) throws IOException {
for (MetricSnapshot snapshot : metricSnapshots) {
if (snapshot.getDataPoints().size() > 0) {
convert(snapshot).writeDelimitedTo(out);
Expand Down
Loading