Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -46,6 +46,7 @@ public class ExporterOpenTelemetryProperties {
private static final String SERVICE_VERSION = "service_version";
private static final String RESOURCE_ATTRIBUTES =
"resource_attributes"; // otel.resource.attributes
private static final String PRESERVE_NAMES = "preserve_names";
private static final String PREFIX = "io.prometheus.exporter.opentelemetry";

@Nullable private final String endpoint;
Expand All @@ -58,6 +59,7 @@ public class ExporterOpenTelemetryProperties {
@Nullable private final String serviceInstanceId;
@Nullable private final String serviceVersion;
private final Map<String, String> resourceAttributes;
@Nullable private final Boolean preserveNames;

private ExporterOpenTelemetryProperties(
@Nullable String protocol,
Expand All @@ -69,7 +71,8 @@ private ExporterOpenTelemetryProperties(
@Nullable String serviceNamespace,
@Nullable String serviceInstanceId,
@Nullable String serviceVersion,
Map<String, String> resourceAttributes) {
Map<String, String> resourceAttributes,
@Nullable Boolean preserveNames) {
this.protocol = protocol;
this.endpoint = endpoint;
this.headers = headers;
Expand All @@ -80,6 +83,7 @@ private ExporterOpenTelemetryProperties(
this.serviceInstanceId = serviceInstanceId;
this.serviceVersion = serviceVersion;
this.resourceAttributes = resourceAttributes;
this.preserveNames = preserveNames;
}

@Nullable
Expand Down Expand Up @@ -130,6 +134,16 @@ public Map<String, String> getResourceAttributes() {
return resourceAttributes;
}

/**
* When {@code true}, metric names are preserved as-is (including suffixes like {@code _total}).
* When {@code false} (default), standard OTel name normalization is applied (stripping unit
* suffix).
*/
@Nullable
public Boolean getPreserveNames() {
return preserveNames;
}

/**
* Note that this will remove entries from {@code propertySource}. This is because we want to know
* if there are unused properties remaining after all properties have been loaded.
Expand All @@ -147,6 +161,7 @@ static ExporterOpenTelemetryProperties load(PropertySource propertySource)
String serviceVersion = Util.loadString(PREFIX, SERVICE_VERSION, propertySource);
Map<String, String> resourceAttributes =
Util.loadMap(PREFIX, RESOURCE_ATTRIBUTES, propertySource);
Boolean preserveNames = Util.loadBoolean(PREFIX, PRESERVE_NAMES, propertySource);
return new ExporterOpenTelemetryProperties(
protocol,
endpoint,
Expand All @@ -157,7 +172,8 @@ static ExporterOpenTelemetryProperties load(PropertySource propertySource)
serviceNamespace,
serviceInstanceId,
serviceVersion,
resourceAttributes);
resourceAttributes,
preserveNames);
}

public static Builder builder() {
Expand All @@ -176,6 +192,7 @@ public static class Builder {
@Nullable private String serviceInstanceId;
@Nullable private String serviceVersion;
private final Map<String, String> resourceAttributes = new HashMap<>();
@Nullable private Boolean preserveNames;

private Builder() {}

Expand Down Expand Up @@ -318,6 +335,15 @@ public Builder resourceAttribute(String name, String value) {
return this;
}

/**
* When {@code true}, metric names are preserved as-is (including suffixes like {@code _total}).
* When {@code false} (default), standard OTel name normalization is applied.
*/
public Builder preserveNames(boolean preserveNames) {
this.preserveNames = preserveNames;
return this;
}

public ExporterOpenTelemetryProperties build() {
return new ExporterOpenTelemetryProperties(
protocol,
Expand All @@ -329,7 +355,8 @@ public ExporterOpenTelemetryProperties build() {
serviceNamespace,
serviceInstanceId,
serviceVersion,
resourceAttributes);
resourceAttributes,
preserveNames);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,39 @@
public class OpenMetrics2Properties {

private static final String PREFIX = "io.prometheus.openmetrics2";
private static final String ENABLED = "enabled";
private static final String CONTENT_NEGOTIATION = "content_negotiation";
private static final String COMPOSITE_VALUES = "composite_values";
private static final String EXEMPLAR_COMPLIANCE = "exemplar_compliance";
private static final String NATIVE_HISTOGRAMS = "native_histograms";

@Nullable private final Boolean enabled;
@Nullable private final Boolean contentNegotiation;
@Nullable private final Boolean compositeValues;
@Nullable private final Boolean exemplarCompliance;
@Nullable private final Boolean nativeHistograms;

private OpenMetrics2Properties(
@Nullable Boolean enabled,
@Nullable Boolean contentNegotiation,
@Nullable Boolean compositeValues,
@Nullable Boolean exemplarCompliance,
@Nullable Boolean nativeHistograms) {
this.enabled = enabled;
this.contentNegotiation = contentNegotiation;
this.compositeValues = compositeValues;
this.exemplarCompliance = exemplarCompliance;
this.nativeHistograms = nativeHistograms;
}

/**
* Enable the OpenMetrics 2.0 text format writer. When {@code true}, the OM2 writer is used
* instead of OM1 for OpenMetrics responses. Default is {@code false}.
*/
public boolean getEnabled() {
return enabled != null && enabled;
}

/** Gate OM2 features behind content negotiation. Default is {@code false}. */
public boolean getContentNegotiation() {
return contentNegotiation != null && contentNegotiation;
Expand All @@ -56,12 +68,13 @@ public boolean getNativeHistograms() {
*/
static OpenMetrics2Properties load(PropertySource propertySource)
throws PrometheusPropertiesException {
Boolean enabled = Util.loadBoolean(PREFIX, ENABLED, propertySource);
Boolean contentNegotiation = Util.loadBoolean(PREFIX, CONTENT_NEGOTIATION, propertySource);
Boolean compositeValues = Util.loadBoolean(PREFIX, COMPOSITE_VALUES, propertySource);
Boolean exemplarCompliance = Util.loadBoolean(PREFIX, EXEMPLAR_COMPLIANCE, propertySource);
Boolean nativeHistograms = Util.loadBoolean(PREFIX, NATIVE_HISTOGRAMS, propertySource);
return new OpenMetrics2Properties(
contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms);
enabled, contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms);
}

public static Builder builder() {
Expand All @@ -70,13 +83,20 @@ public static Builder builder() {

public static class Builder {

@Nullable private Boolean enabled;
@Nullable private Boolean contentNegotiation;
@Nullable private Boolean compositeValues;
@Nullable private Boolean exemplarCompliance;
@Nullable private Boolean nativeHistograms;

private Builder() {}

/** See {@link #getEnabled()} */
public Builder enabled(boolean enabled) {
this.enabled = enabled;
return this;
}

/** See {@link #getContentNegotiation()} */
public Builder contentNegotiation(boolean contentNegotiation) {
this.contentNegotiation = contentNegotiation;
Expand All @@ -103,6 +123,7 @@ public Builder nativeHistograms(boolean nativeHistograms) {

/** Enable all OpenMetrics 2.0 features */
public Builder enableAll() {
this.enabled = true;
this.contentNegotiation = true;
this.compositeValues = true;
this.exemplarCompliance = true;
Expand All @@ -112,7 +133,7 @@ public Builder enableAll() {

public OpenMetrics2Properties build() {
return new OpenMetrics2Properties(
contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms);
enabled, contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ public Builder exporterOpenTelemetryProperties(
}

public Builder enableOpenMetrics2(Consumer<OpenMetrics2Properties.Builder> configurator) {
OpenMetrics2Properties.Builder openMetrics2Builder = OpenMetrics2Properties.builder();
OpenMetrics2Properties.Builder openMetrics2Builder =
OpenMetrics2Properties.builder().enabled(true);
configurator.accept(openMetrics2Builder);
this.openMetrics2Properties = openMetrics2Builder.build();
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ void load() {
load(
new HashMap<>(
Map.of(
"io.prometheus.openmetrics2.enabled",
"true",
"io.prometheus.openmetrics2.content_negotiation",
"true",
"io.prometheus.openmetrics2.composite_values",
Expand All @@ -23,6 +25,7 @@ void load() {
"true",
"io.prometheus.openmetrics2.native_histograms",
"true")));
assertThat(properties.getEnabled()).isTrue();
assertThat(properties.getContentNegotiation()).isTrue();
assertThat(properties.getCompositeValues()).isTrue();
assertThat(properties.getExemplarCompliance()).isTrue();
Expand All @@ -31,6 +34,11 @@ void load() {

@Test
void loadInvalidValue() {
assertThatExceptionOfType(PrometheusPropertiesException.class)
.isThrownBy(
() -> load(new HashMap<>(Map.of("io.prometheus.openmetrics2.enabled", "invalid"))))
.withMessage(
"io.prometheus.openmetrics2.enabled: Expecting 'true' or 'false'. Found: invalid");
assertThatExceptionOfType(PrometheusPropertiesException.class)
.isThrownBy(
() ->
Expand Down Expand Up @@ -79,11 +87,13 @@ private static OpenMetrics2Properties load(Map<String, String> map) {
void builder() {
OpenMetrics2Properties properties =
OpenMetrics2Properties.builder()
.enabled(true)
.contentNegotiation(true)
.compositeValues(false)
.exemplarCompliance(true)
.nativeHistograms(false)
.build();
assertThat(properties.getEnabled()).isTrue();
assertThat(properties.getContentNegotiation()).isTrue();
assertThat(properties.getCompositeValues()).isFalse();
assertThat(properties.getExemplarCompliance()).isTrue();
Expand All @@ -93,6 +103,7 @@ void builder() {
@Test
void builderEnableAll() {
OpenMetrics2Properties properties = OpenMetrics2Properties.builder().enableAll().build();
assertThat(properties.getEnabled()).isTrue();
assertThat(properties.getContentNegotiation()).isTrue();
assertThat(properties.getCompositeValues()).isTrue();
assertThat(properties.getExemplarCompliance()).isTrue();
Expand All @@ -102,6 +113,7 @@ void builderEnableAll() {
@Test
void defaultValues() {
OpenMetrics2Properties properties = OpenMetrics2Properties.builder().build();
assertThat(properties.getEnabled()).isFalse();
assertThat(properties.getContentNegotiation()).isFalse();
assertThat(properties.getCompositeValues()).isFalse();
assertThat(properties.getExemplarCompliance()).isFalse();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ private Builder(PrometheusProperties properties) {
*/
@Override
public Builder name(String name) {
return super.name(stripTotalSuffix(name));
return super.nameWithOriginal(stripTotalSuffix(name), name);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ private Builder(PrometheusProperties config) {
*/
@Override
public Builder name(String name) {
return super.name(stripInfoSuffix(name));
return super.nameWithOriginal(stripInfoSuffix(name), name);
}

/** Throws an {@link UnsupportedOperationException} because Info metrics cannot have a unit. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@ public abstract class MetricWithFixedMetadata extends Metric {

protected MetricWithFixedMetadata(Builder<?, ?> builder) {
super(builder);
String name = makeName(builder.name, builder.unit);
if (builder.originalName == null) {
throw new IllegalArgumentException("Missing required field: name is null");
}
String originalName = builder.originalName;
String expositionBaseName = makeExpositionBaseName(originalName, builder.unit);
this.metadata =
new MetricMetadata(makeName(builder.name, builder.unit), builder.help, builder.unit);
new MetricMetadata(name, expositionBaseName, originalName, builder.help, builder.unit);
this.labelNames = Arrays.copyOf(builder.labelNames, builder.labelNames.length);
}

Expand All @@ -47,6 +53,18 @@ private String makeName(@Nullable String name, @Nullable Unit unit) {
return name;
}

private String makeExpositionBaseName(@Nullable String expositionBaseName, @Nullable Unit unit) {
if (expositionBaseName == null) {
throw new IllegalArgumentException("Missing required field: name is null");
}
if (unit != null) {
if (!expositionBaseName.endsWith("_" + unit) && !expositionBaseName.endsWith("." + unit)) {
expositionBaseName += "_" + unit;
}
}
return expositionBaseName;
}

@Override
public String getPrometheusName() {
return metadata.getPrometheusName();
Expand All @@ -68,6 +86,7 @@ public abstract static class Builder<B extends Builder<B, M>, M extends MetricWi
extends Metric.Builder<B, M> {

@Nullable private String name;
@Nullable private String originalName;
@Nullable private Unit unit;
@Nullable private String help;
private String[] labelNames = new String[0];
Expand All @@ -82,6 +101,25 @@ public B name(String name) {
throw new IllegalArgumentException("'" + name + "': Illegal metric name: " + error);
}
this.name = name;
this.originalName = name;
return self();
}

/**
* Set the metric name and original name separately. Used by Counter and Info builders which
* strip type suffixes from the name but preserve the original for exposition.
*/
protected B nameWithOriginal(String name, String originalName) {
String error = PrometheusNaming.validateMetricName(name);
if (error != null) {
throw new IllegalArgumentException("'" + name + "': Illegal metric name: " + error);
}
error = PrometheusNaming.validateMetricName(originalName);
if (error != null) {
throw new IllegalArgumentException("'" + originalName + "': Illegal metric name: " + error);
}
this.name = name;
this.originalName = originalName;
return self();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static class Builder {
@Nullable String serviceInstanceId;
@Nullable String serviceVersion;
final Map<String, String> resourceAttributes = new HashMap<>();
@Nullable Boolean preserveNames;

private Builder(PrometheusProperties config) {
this.config = config;
Expand Down Expand Up @@ -194,6 +195,15 @@ public Builder resourceAttribute(String name, String value) {
return this;
}

/**
* When {@code true}, metric names are preserved as-is (including suffixes like {@code _total}).
* When {@code false} (default), standard OTel name normalization is applied.
*/
public Builder preserveNames(boolean preserveNames) {
this.preserveNames = preserveNames;
return this;
}

public OpenTelemetryExporter buildAndStart() {
if (registry == null) {
registry = PrometheusRegistry.defaultRegistry;
Expand Down
Loading
Loading