Skip to content

Commit 16ca78a

Browse files
authored
Merge pull request #50 from ringcentral/prometheus-sanitizing-label-names
#49: Support sanitizing label names in PrometheusMetricsExporter, introduce PrometheusMetricsExporterBuilder
2 parents b679308 + 576bf99 commit 16ca78a

File tree

11 files changed

+354
-437
lines changed

11 files changed

+354
-437
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.ringcentral.platform.metrics.reporters.prometheus;
2+
3+
import io.prometheus.client.Collector;
4+
5+
import java.util.List;
6+
import java.util.regex.Pattern;
7+
8+
import static com.ringcentral.platform.metrics.reporters.prometheus.PrometheusMetricsExporter.NAME_PARTS_DELIMITER;
9+
import static java.lang.Character.isLetter;
10+
import static java.util.Objects.requireNonNull;
11+
import static java.util.stream.Collectors.toList;
12+
13+
public class DefaultPrometheusMetricSanitizer implements PrometheusMetricSanitizer {
14+
15+
public static final boolean DEFAULT_SANITIZE_METRIC_NAME = true;
16+
public static final boolean DEFAULT_SANITIZE_LABEL_NAME = true;
17+
public static final String DEFAULT_NON_LETTER_LABEL_NAME_FIRST_CHAR_PREFIX = "label_";
18+
19+
private static final Pattern LABEL_NAME_FORBIDDEN_CHAR_PATTERN = Pattern.compile("\\W");
20+
21+
private final boolean sanitizeMetricName;
22+
private final boolean sanitizeLabelName;
23+
private final String nonLetterLabelNameFirstCharPrefix;
24+
25+
public DefaultPrometheusMetricSanitizer() {
26+
this(
27+
DEFAULT_SANITIZE_METRIC_NAME,
28+
DEFAULT_SANITIZE_LABEL_NAME,
29+
DEFAULT_NON_LETTER_LABEL_NAME_FIRST_CHAR_PREFIX);
30+
}
31+
32+
public DefaultPrometheusMetricSanitizer(boolean sanitizeMetricName, boolean sanitizeLabelName) {
33+
this(sanitizeMetricName, sanitizeLabelName, DEFAULT_NON_LETTER_LABEL_NAME_FIRST_CHAR_PREFIX);
34+
}
35+
36+
public DefaultPrometheusMetricSanitizer(
37+
boolean sanitizeMetricName,
38+
boolean sanitizeLabelName,
39+
String nonLetterLabelNameFirstCharPrefix) {
40+
41+
this.sanitizeMetricName = sanitizeMetricName;
42+
this.sanitizeLabelName = sanitizeLabelName;
43+
this.nonLetterLabelNameFirstCharPrefix = requireNonNull(nonLetterLabelNameFirstCharPrefix);
44+
}
45+
46+
@Override
47+
public String sanitizeMetricName(String metricName) {
48+
return sanitizeMetricName ? Collector.sanitizeMetricName(metricName) : metricName;
49+
}
50+
51+
@Override
52+
public List<String> sanitizeLabelNames(List<String> labelNames) {
53+
return
54+
sanitizeLabelName ?
55+
labelNames.stream().map(this::sanitizeLabelName).collect(toList()) :
56+
labelNames;
57+
}
58+
59+
@Override
60+
public String sanitizeLabelName(String labelName) {
61+
if (!sanitizeLabelName) {
62+
return labelName;
63+
}
64+
65+
String result = LABEL_NAME_FORBIDDEN_CHAR_PATTERN.matcher(labelName).replaceAll(NAME_PARTS_DELIMITER);
66+
return isLetter(result.charAt(0)) ? result : nonLetterLabelNameFirstCharPrefix + result;
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.ringcentral.platform.metrics.reporters.prometheus;
2+
3+
import java.util.List;
4+
5+
/**
6+
* Provides methods for sanitizing Prometheus metric names and label names based on the following specification:
7+
* <a href="https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels">Prometheus Data Model</a>
8+
*/
9+
public interface PrometheusMetricSanitizer {
10+
/**
11+
* Sanitizes a Prometheus metric name according to the data model specification.
12+
*
13+
* @param metricName the metric name to sanitize
14+
* @return the sanitized metric name
15+
*/
16+
String sanitizeMetricName(String metricName);
17+
18+
/**
19+
* Sanitizes a list of Prometheus label names according to the data model specification.
20+
*
21+
* @param labelNames the list of label names to sanitize
22+
* @return the sanitized list of label names
23+
*/
24+
List<String> sanitizeLabelNames(List<String> labelNames);
25+
26+
/**
27+
* Sanitizes a single Prometheus label name according to the data model specification.
28+
*
29+
* @param labelName the label name to sanitize
30+
* @return the sanitized label name
31+
*/
32+
String sanitizeLabelName(String labelName);
33+
}

metrics-facade-prometheus/src/main/java/com/ringcentral/platform/metrics/reporters/prometheus/PrometheusMetricsExporter.java

Lines changed: 16 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@
33
import com.ringcentral.platform.metrics.MetricRegistry;
44
import com.ringcentral.platform.metrics.names.MetricName;
55
import com.ringcentral.platform.metrics.reporters.MetricsExporter;
6-
import com.ringcentral.platform.metrics.samples.CompositeInstanceSamplesProvider;
76
import com.ringcentral.platform.metrics.samples.InstanceSamplesProvider;
87
import com.ringcentral.platform.metrics.samples.prometheus.PrometheusInstanceSample;
98
import com.ringcentral.platform.metrics.samples.prometheus.PrometheusInstanceSamplesProvider;
109
import com.ringcentral.platform.metrics.samples.prometheus.PrometheusSample;
11-
import com.ringcentral.platform.metrics.samples.prometheus.collectorRegistry.SimpleCollectorRegistryPrometheusInstanceSamplesProvider;
1210
import com.ringcentral.platform.metrics.utils.StringBuilderWriter;
13-
import io.prometheus.client.CollectorRegistry;
1411
import io.prometheus.client.exporter.common.TextFormat;
1512
import org.slf4j.Logger;
1613

@@ -20,10 +17,10 @@
2017

2118
import static com.ringcentral.platform.metrics.reporters.prometheus.PrometheusMetricsExporter.Format.PROMETHEUS_TEXT_O_O_4;
2219
import static io.prometheus.client.Collector.MetricFamilySamples;
23-
import static io.prometheus.client.Collector.sanitizeMetricName;
2420
import static java.lang.String.join;
2521
import static java.util.Collections.emptyList;
2622
import static java.util.Collections.enumeration;
23+
import static java.util.Objects.requireNonNull;
2724
import static java.util.stream.Collectors.toList;
2825
import static org.slf4j.LoggerFactory.getLogger;
2926

@@ -42,109 +39,38 @@ public enum Format {
4239

4340
public static final Format DEFAULT_FORMAT = PROMETHEUS_TEXT_O_O_4;
4441
public static final String NAME_PARTS_DELIMITER = "_";
45-
public static final boolean DEFAULT_LOWER_CASE_NAME = false;
42+
public static final boolean DEFAULT_CONVERT_NAME_TO_LOWER_CASE = false;
4643
public static final Locale DEFAULT_LOCALE = Locale.ENGLISH;
4744

4845
private final Format defaultFormat;
49-
private final InstanceSamplesProvider<? extends PrometheusSample, ? extends PrometheusInstanceSample> instanceSamplesProvider;
5046
private final boolean convertNameToLowercase;
5147
private final Locale locale;
48+
private final PrometheusMetricSanitizer sanitizer;
49+
private final InstanceSamplesProvider<? extends PrometheusSample, ? extends PrometheusInstanceSample> instanceSamplesProvider;
5250

5351
private static final Logger logger = getLogger(PrometheusMetricsExporter.class);
5452

55-
public PrometheusMetricsExporter(MetricRegistry registry) {
56-
this(new PrometheusInstanceSamplesProvider(registry));
57-
}
58-
59-
public PrometheusMetricsExporter(MetricRegistry registry, CollectorRegistry... collectorRegistries) {
60-
this(
61-
new PrometheusInstanceSamplesProvider(registry),
62-
new SimpleCollectorRegistryPrometheusInstanceSamplesProvider(collectorRegistries));
63-
}
64-
65-
public PrometheusMetricsExporter(
66-
MetricRegistry registry,
67-
boolean convertNameToLowercase,
68-
Locale locale) {
69-
70-
this(
71-
convertNameToLowercase,
72-
locale,
73-
new PrometheusInstanceSamplesProvider(registry));
74-
}
75-
76-
public PrometheusMetricsExporter(InstanceSamplesProvider<? extends PrometheusSample, ? extends PrometheusInstanceSample> instanceSamplesProvider) {
77-
this(DEFAULT_FORMAT, instanceSamplesProvider);
78-
}
79-
80-
public PrometheusMetricsExporter(
81-
Format defaultFormat,
82-
InstanceSamplesProvider<? extends PrometheusSample, ? extends PrometheusInstanceSample> instanceSamplesProvider) {
83-
84-
this(
85-
defaultFormat,
86-
DEFAULT_LOWER_CASE_NAME,
87-
DEFAULT_LOCALE,
88-
instanceSamplesProvider);
89-
}
90-
91-
public PrometheusMetricsExporter(
92-
Format defaultFormat,
93-
boolean convertNameToLowercase,
94-
Locale locale,
95-
InstanceSamplesProvider<? extends PrometheusSample, ? extends PrometheusInstanceSample> instanceSamplesProvider) {
96-
97-
this.defaultFormat = defaultFormat;
98-
this.convertNameToLowercase = convertNameToLowercase;
99-
this.locale = locale != null ? locale : DEFAULT_LOCALE;
100-
this.instanceSamplesProvider = instanceSamplesProvider;
101-
}
102-
103-
@SafeVarargs
104-
public PrometheusMetricsExporter(InstanceSamplesProvider<? extends PrometheusSample, ? extends PrometheusInstanceSample>... instanceSamplesProviders) {
105-
this(
106-
DEFAULT_FORMAT,
107-
DEFAULT_LOWER_CASE_NAME,
108-
DEFAULT_LOCALE,
109-
instanceSamplesProviders);
110-
}
111-
112-
@SafeVarargs
113-
public PrometheusMetricsExporter(
114-
boolean convertNameToLowercase,
115-
Locale locale,
116-
InstanceSamplesProvider<? extends PrometheusSample, ? extends PrometheusInstanceSample>... instanceSamplesProviders) {
117-
53+
public PrometheusMetricsExporter(MetricRegistry metricRegistry) {
11854
this(
11955
DEFAULT_FORMAT,
120-
convertNameToLowercase,
121-
locale,
122-
instanceSamplesProviders);
123-
}
124-
125-
@SafeVarargs
126-
public PrometheusMetricsExporter(
127-
Format defaultFormat,
128-
InstanceSamplesProvider<? extends PrometheusSample, ? extends PrometheusInstanceSample>... instanceSamplesProviders) {
129-
130-
this(
131-
defaultFormat,
132-
DEFAULT_LOWER_CASE_NAME,
56+
DEFAULT_CONVERT_NAME_TO_LOWER_CASE,
13357
DEFAULT_LOCALE,
134-
instanceSamplesProviders);
58+
new DefaultPrometheusMetricSanitizer(),
59+
new PrometheusInstanceSamplesProvider(metricRegistry));
13560
}
13661

137-
@SafeVarargs
13862
public PrometheusMetricsExporter(
13963
Format defaultFormat,
14064
boolean convertNameToLowercase,
14165
Locale locale,
142-
InstanceSamplesProvider<? extends PrometheusSample, ? extends PrometheusInstanceSample>... instanceSamplesProviders) {
66+
PrometheusMetricSanitizer sanitizer,
67+
InstanceSamplesProvider<? extends PrometheusSample, ? extends PrometheusInstanceSample> instanceSamplesProvider) {
14368

144-
this.defaultFormat = defaultFormat;
69+
this.defaultFormat = requireNonNull(defaultFormat);
14570
this.convertNameToLowercase = convertNameToLowercase;
14671
this.locale = locale != null ? locale : DEFAULT_LOCALE;
147-
this.instanceSamplesProvider = new CompositeInstanceSamplesProvider<>(List.of(instanceSamplesProviders));
72+
this.sanitizer = requireNonNull(sanitizer);
73+
this.instanceSamplesProvider = requireNonNull(instanceSamplesProvider);
14874
}
14975

15076
@Override
@@ -232,21 +158,21 @@ private MetricFamilySamples toMetricFamilySamples(PrometheusInstanceSample is) {
232158

233159
return new MetricFamilySamples.Sample(
234160
sampleName,
235-
s.labelNames() != null ? s.labelNames() : emptyList(),
161+
s.labelNames() != null ? sanitizer.sanitizeLabelNames(s.labelNames()) : emptyList(),
236162
s.labelValues() != null ? s.labelValues() : emptyList(),
237163
s.value());
238164
})
239165
.collect(toList()));
240166
}
241167

242168
private String buildName(MetricName name) {
243-
final var sanitizedName = sanitizeMetricName(join(NAME_PARTS_DELIMITER, name));
169+
final var sanitizedName = sanitizer.sanitizeMetricName(join(NAME_PARTS_DELIMITER, name));
244170
return convertNameToLowercase ? sanitizedName.toLowerCase(locale) : sanitizedName;
245171
}
246172

247173
private String buildNameSuffix(PrometheusSample ps) {
248174
final var suffix = ps.nameSuffix();
249-
final var sanitizedSuffix = sanitizeMetricName(suffix);
175+
final var sanitizedSuffix = sanitizer.sanitizeMetricName(suffix);
250176
return convertNameToLowercase ? sanitizedSuffix.toLowerCase(locale) : sanitizedSuffix;
251177
}
252178

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.ringcentral.platform.metrics.reporters.prometheus;
2+
3+
import com.ringcentral.platform.metrics.MetricRegistry;
4+
import com.ringcentral.platform.metrics.samples.CompositeInstanceSamplesProvider;
5+
import com.ringcentral.platform.metrics.samples.InstanceSamplesProvider;
6+
import com.ringcentral.platform.metrics.samples.prometheus.PrometheusInstanceSample;
7+
import com.ringcentral.platform.metrics.samples.prometheus.PrometheusInstanceSamplesProvider;
8+
import com.ringcentral.platform.metrics.samples.prometheus.PrometheusSample;
9+
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
import java.util.Locale;
13+
14+
import static com.ringcentral.platform.metrics.samples.prometheus.PrometheusInstanceSamplesProviderBuilder.prometheusInstanceSamplesProvider;
15+
import static java.util.Objects.requireNonNull;
16+
17+
public class PrometheusMetricsExporterBuilder {
18+
19+
private PrometheusMetricsExporter.Format defaultFormat = PrometheusMetricsExporter.DEFAULT_FORMAT;
20+
private boolean convertNameToLowercase = PrometheusMetricsExporter.DEFAULT_CONVERT_NAME_TO_LOWER_CASE;
21+
private Locale locale = PrometheusMetricsExporter.DEFAULT_LOCALE;
22+
private PrometheusMetricSanitizer sanitizer = new DefaultPrometheusMetricSanitizer();
23+
private final List<InstanceSamplesProvider<? extends PrometheusSample, ? extends PrometheusInstanceSample>> instanceSamplesProviders = new ArrayList<>();
24+
25+
public static PrometheusMetricsExporterBuilder prometheusMetricsExporter() {
26+
return new PrometheusMetricsExporterBuilder();
27+
}
28+
29+
public static PrometheusMetricsExporterBuilder prometheusMetricsExporterBuilder() {
30+
return new PrometheusMetricsExporterBuilder();
31+
}
32+
33+
public static PrometheusMetricsExporterBuilder prometheusMetricsExporter(MetricRegistry metricRegistry) {
34+
PrometheusInstanceSamplesProvider instanceSamplesProvider = prometheusInstanceSamplesProvider(requireNonNull(metricRegistry)).build();
35+
return prometheusMetricsExporter().addInstanceSamplesProvider(instanceSamplesProvider);
36+
}
37+
38+
public PrometheusMetricsExporterBuilder defaultFormat(PrometheusMetricsExporter.Format defaultFormat) {
39+
this.defaultFormat = requireNonNull(defaultFormat);
40+
return this;
41+
}
42+
43+
public PrometheusMetricsExporterBuilder convertNameToLowercase() {
44+
return convertNameToLowercase(true);
45+
}
46+
47+
public PrometheusMetricsExporterBuilder convertNameToLowercase(boolean convertNameToLowercase) {
48+
this.convertNameToLowercase = convertNameToLowercase;
49+
return this;
50+
}
51+
52+
public PrometheusMetricsExporterBuilder locale(Locale locale) {
53+
this.locale = requireNonNull(locale);
54+
return this;
55+
}
56+
57+
public PrometheusMetricsExporterBuilder sanitizer(PrometheusMetricSanitizer sanitizer) {
58+
this.sanitizer = requireNonNull(sanitizer);
59+
return this;
60+
}
61+
62+
public PrometheusMetricsExporterBuilder addInstanceSamplesProvider(InstanceSamplesProvider<? extends PrometheusSample, ? extends PrometheusInstanceSample> instanceSamplesProvider) {
63+
this.instanceSamplesProviders.add(requireNonNull(instanceSamplesProvider));
64+
return this;
65+
}
66+
67+
public PrometheusMetricsExporter build() {
68+
if (instanceSamplesProviders.isEmpty()) {
69+
throw new IllegalStateException("instanceSamplesProviders not configured");
70+
}
71+
72+
return new PrometheusMetricsExporter(
73+
defaultFormat,
74+
convertNameToLowercase,
75+
locale,
76+
sanitizer,
77+
instanceSamplesProviders.size() == 1 ? instanceSamplesProviders.get(0) : new CompositeInstanceSamplesProvider<>(instanceSamplesProviders));
78+
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.ringcentral.platform.metrics.reporters.prometheus;
2+
3+
import org.junit.Test;
4+
5+
import java.util.List;
6+
7+
import static org.hamcrest.CoreMatchers.is;
8+
import static org.hamcrest.MatcherAssert.assertThat;
9+
10+
public class DefaultPrometheusMetricSanitizerTest {
11+
12+
static final String UNSANITIZED = ".a.1.:._.й.|";
13+
PrometheusMetricSanitizer sanitizer = new DefaultPrometheusMetricSanitizer();
14+
15+
@Test
16+
public void sanitizingDisabled() {
17+
var sanitizer = new DefaultPrometheusMetricSanitizer(false, false);
18+
assertThat(sanitizer.sanitizeMetricName(UNSANITIZED), is(UNSANITIZED));
19+
assertThat(sanitizer.sanitizeLabelName(UNSANITIZED), is(UNSANITIZED));
20+
assertThat(sanitizer.sanitizeLabelNames(List.of(UNSANITIZED)), is(List.of(UNSANITIZED)));
21+
}
22+
23+
@Test
24+
public void sanitizingMetricName() {
25+
assertThat(sanitizer.sanitizeMetricName(".a.1.:._.й.|"), is("_a_1_:______"));
26+
}
27+
28+
@Test
29+
public void sanitizingLabelName() {
30+
assertThat(sanitizer.sanitizeLabelName("a.1.:._.й.|"), is("a_1________"));
31+
assertThat(sanitizer.sanitizeLabelNames(List.of(".a.1.:._.й.|")), is(List.of("label__a_1________")));
32+
}
33+
}

0 commit comments

Comments
 (0)