Skip to content

Commit 4a93472

Browse files
committed
Add method for sanitizing Unit names
Signed-off-by: Fabian Stäber <[email protected]>
1 parent c0a3827 commit 4a93472

File tree

3 files changed

+138
-6
lines changed

3 files changed

+138
-6
lines changed

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

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public class PrometheusNaming {
2020
*/
2121
private static final Pattern LABEL_NAME_PATTERN = Pattern.compile("^[a-zA-Z_.][a-zA-Z0-9_.]*$");
2222

23+
/**
24+
* Legal characters for unit names, including dot.
25+
*/
26+
private static final Pattern UNIT_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_.:]+$");
27+
2328
/**
2429
* According to OpenMetrics {@code _count} and {@code _sum} (and {@code _gcount}, {@code _gsum}) should also be
2530
* reserved metric name suffixes. However, popular instrumentation libraries have Gauges with names
@@ -83,6 +88,32 @@ public static boolean isValidLabelName(String name) {
8388
!(name.startsWith("__") || name.startsWith("._") || name.startsWith("..") || name.startsWith("_."));
8489
}
8590

91+
/**
92+
* Units may not have illegal characters, and they may not end with a reserved suffix like 'total'.
93+
*/
94+
public static boolean isValidUnitName(String name) {
95+
return validateUnitName(name) == null;
96+
}
97+
98+
/**
99+
* Same as {@link #isValidUnitName(String)} but returns an error message.
100+
*/
101+
public static String validateUnitName(String name) {
102+
if (name.isEmpty()) {
103+
return "The unit name must not be empty.";
104+
}
105+
for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) {
106+
String suffixName = reservedSuffix.substring(1);
107+
if (name.endsWith(suffixName)) {
108+
return suffixName + " is a reserved suffix in Prometheus";
109+
}
110+
}
111+
if (!UNIT_NAME_PATTERN.matcher(name).matches()) {
112+
return "The unit name contains unsupported characters";
113+
}
114+
return null;
115+
}
116+
86117
/**
87118
* Get the metric or label name that is used in Prometheus exposition format.
88119
*
@@ -149,6 +180,42 @@ public static String sanitizeLabelName(String labelName) {
149180
return sanitizedName;
150181
}
151182

183+
/**
184+
* Convert an arbitrary string to a name where {@link #isValidUnitName(String) isValidUnitName(name)} is true.
185+
*
186+
* @throws IllegalArgumentException if the {@code unitName} cannot be converted, for example if you call {@code sanitizeUnitName("total")} or {@code sanitizeUnitName("")}.
187+
* @throws NullPointerException if {@code unitName} is null.
188+
*/
189+
public static String sanitizeUnitName(String unitName) {
190+
if (unitName.isEmpty()) {
191+
throw new IllegalArgumentException("Cannot convert an empty string to a valid unit name.");
192+
}
193+
String sanitizedName = replaceIllegalCharsInUnitName(unitName);
194+
boolean modified = true;
195+
while (modified) {
196+
modified = false;
197+
while (sanitizedName.startsWith("_") || sanitizedName.startsWith(".")) {
198+
sanitizedName = sanitizedName.substring(1);
199+
modified = true;
200+
}
201+
while (sanitizedName.endsWith(".") || sanitizedName.endsWith("_")) {
202+
sanitizedName = sanitizedName.substring(0, sanitizedName.length()-1);
203+
modified = true;
204+
}
205+
for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) {
206+
String suffixName = reservedSuffix.substring(1);
207+
if (sanitizedName.endsWith(suffixName)) {
208+
sanitizedName = sanitizedName.substring(0, sanitizedName.length() - suffixName.length());
209+
modified = true;
210+
}
211+
}
212+
}
213+
if (sanitizedName.isEmpty()) {
214+
throw new IllegalArgumentException("Cannot convert '" + unitName + "' into a valid unit name.");
215+
}
216+
return sanitizedName;
217+
}
218+
152219
/**
153220
* Returns a string that matches {@link #METRIC_NAME_PATTERN}.
154221
*/
@@ -189,4 +256,25 @@ private static String replaceIllegalCharsInLabelName(String name) {
189256
}
190257
return new String(sanitized);
191258
}
259+
260+
/**
261+
* Returns a string that matches {@link #UNIT_NAME_PATTERN}.
262+
*/
263+
private static String replaceIllegalCharsInUnitName(String name) {
264+
int length = name.length();
265+
char[] sanitized = new char[length];
266+
for (int i = 0; i < length; i++) {
267+
char ch = name.charAt(i);
268+
if (ch == ':' ||
269+
ch == '.' ||
270+
(ch >= 'a' && ch <= 'z') ||
271+
(ch >= 'A' && ch <= 'Z') ||
272+
(ch >= '0' && ch <= '9')) {
273+
sanitized[i] = ch;
274+
} else {
275+
sanitized[i] = '_';
276+
}
277+
}
278+
return new String(sanitized);
279+
}
192280
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ public Unit(String name) {
2828
if (name == null) {
2929
throw new NullPointerException("Unit name cannot be null.");
3030
}
31-
if (name.trim().isEmpty()) {
32-
throw new IllegalArgumentException("Unit name cannot be empty.");
31+
name = name.trim();
32+
String error = PrometheusNaming.validateUnitName(name);
33+
if (error != null) {
34+
throw new IllegalArgumentException(name + ": Illegal unit name: " + error);
3335
}
34-
this.name = name.trim();
36+
this.name = name;
3537
}
3638

3739
@Override

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

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
import org.junit.Assert;
44
import org.junit.Test;
55

6-
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
7-
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeLabelName;
8-
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.sanitizeMetricName;
6+
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.*;
97

108
public class PrometheusNamingTest {
119

@@ -19,6 +17,8 @@ public void testSanitizeMetricName() {
1917
Assert.assertEquals("jvm", sanitizeMetricName("jvm_info"));
2018
Assert.assertEquals("jvm", sanitizeMetricName("jvm.info"));
2119
Assert.assertEquals("a.b", sanitizeMetricName("a.b"));
20+
Assert.assertEquals("total", sanitizeMetricName("_total"));
21+
Assert.assertEquals("total", sanitizeMetricName("total"));
2222
}
2323

2424
@Test
@@ -31,4 +31,46 @@ public void testSanitizeLabelName() {
3131
Assert.assertEquals("abc.def", sanitizeLabelName("abc.def"));
3232
Assert.assertEquals("abc.def2", sanitizeLabelName("abc.def2"));
3333
}
34+
35+
@Test
36+
public void testValidateUnitName() {
37+
Assert.assertNotNull(validateUnitName("secondstotal"));
38+
Assert.assertNotNull(validateUnitName("total"));
39+
Assert.assertNotNull(validateUnitName("seconds_total"));
40+
Assert.assertNotNull(validateUnitName("_total"));
41+
Assert.assertNotNull(validateUnitName(""));
42+
43+
Assert.assertNull(validateUnitName("seconds"));
44+
Assert.assertNull(validateUnitName("2"));
45+
}
46+
47+
@Test
48+
public void testSanitizeUnitName() {
49+
Assert.assertEquals("seconds", sanitizeUnitName("seconds"));
50+
Assert.assertEquals("seconds", sanitizeUnitName("seconds_total"));
51+
Assert.assertEquals("seconds", sanitizeUnitName("seconds_total_total"));
52+
Assert.assertEquals("m_s", sanitizeUnitName("m/s"));
53+
Assert.assertEquals("seconds", sanitizeUnitName("secondstotal"));
54+
Assert.assertEquals("2", sanitizeUnitName("2"));
55+
}
56+
57+
@Test(expected = IllegalArgumentException.class)
58+
public void testInvalidUnitName1() {
59+
sanitizeUnitName("total");
60+
}
61+
62+
@Test(expected = IllegalArgumentException.class)
63+
public void testInvalidUnitName2() {
64+
sanitizeUnitName("_total");
65+
}
66+
67+
@Test(expected = IllegalArgumentException.class)
68+
public void testInvalidUnitName3() {
69+
sanitizeUnitName("%");
70+
}
71+
72+
@Test(expected = IllegalArgumentException.class)
73+
public void testEmptyUnitName() {
74+
sanitizeUnitName("");
75+
}
3476
}

0 commit comments

Comments
 (0)