001package io.prometheus.metrics.model.snapshots; 002 003import java.util.regex.Pattern; 004 005/** 006 * Utility for Prometheus Metric and Label naming. 007 * <p> 008 * Note that this library allows dots in metric and label names. Dots will automatically be replaced with underscores 009 * in Prometheus exposition formats. However, if metrics are exposed in OpenTelemetry format the dots are retained. 010 */ 011public class PrometheusNaming { 012 013 /** 014 * Legal characters for metric names, including dot. 015 */ 016 private static final Pattern METRIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z_.:][a-zA-Z0-9_.:]*$"); 017 018 /** 019 * Legal characters for label names, including dot. 020 */ 021 private static final Pattern LABEL_NAME_PATTERN = Pattern.compile("^[a-zA-Z_.][a-zA-Z0-9_.]*$"); 022 023 /** 024 * Legal characters for unit names, including dot. 025 */ 026 private static final Pattern UNIT_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_.:]+$"); 027 028 /** 029 * According to OpenMetrics {@code _count} and {@code _sum} (and {@code _gcount}, {@code _gsum}) should also be 030 * reserved metric name suffixes. However, popular instrumentation libraries have Gauges with names 031 * ending in {@code _count}. 032 * Examples: 033 * <ul> 034 * <li>Micrometer: {@code jvm_buffer_count}</li> 035 * <li>OpenTelemetry: {@code process_runtime_jvm_buffer_count}</li> 036 * </ul> 037 * We do not treat {@code _count} and {@code _sum} as reserved suffixes here for compatibility with these libraries. 038 * However, there is a risk of name conflict if someone creates a gauge named {@code my_data_count} and a 039 * histogram or summary named {@code my_data}, because the histogram or summary will implicitly have a sample 040 * named {@code my_data_count}. 041 */ 042 private static final String[] RESERVED_METRIC_NAME_SUFFIXES = { 043 "_total", "_created", "_bucket", "_info", 044 ".total", ".created", ".bucket", ".info" 045 }; 046 047 /** 048 * Test if a metric name is valid. Rules: 049 * <ul> 050 * <li>The name must match {@link #METRIC_NAME_PATTERN}.</li> 051 * <li>The name MUST NOT end with one of the {@link #RESERVED_METRIC_NAME_SUFFIXES}.</li> 052 * </ul> 053 * If a metric has a {@link Unit}, the metric name SHOULD end with the unit as a suffix. 054 * Note that <a href="https://openmetrics.io/">OpenMetrics</a> requires metric names to have their unit as suffix, 055 * and we implement this in {@code prometheus-metrics-core}. However, {@code prometheus-metrics-model} 056 * does not enforce Unit suffixes. 057 * <p> 058 * Example: If you create a Counter for a processing time with Unit {@link Unit#SECONDS SECONDS}, 059 * the name should be {@code processing_time_seconds}. When exposed in OpenMetrics Text format, 060 * this will be represented as two values: {@code processing_time_seconds_total} for the counter value, 061 * and the optional {@code processing_time_seconds_created} timestamp. 062 * <p> 063 * Use {@link #sanitizeMetricName(String)} to convert arbitrary Strings to valid metric names. 064 */ 065 public static boolean isValidMetricName(String name) { 066 return validateMetricName(name) == null; 067 } 068 069 /** 070 * Same as {@link #isValidMetricName(String)}, but produces an error message. 071 * <p> 072 * The name is valid if the error message is {@code null}. 073 */ 074 static String validateMetricName(String name) { 075 for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) { 076 if (name.endsWith(reservedSuffix)) { 077 return "The metric name must not include the '" + reservedSuffix + "' suffix."; 078 } 079 } 080 if (!METRIC_NAME_PATTERN.matcher(name).matches()) { 081 return "The metric name contains unsupported characters"; 082 } 083 return null; 084 } 085 086 public static boolean isValidLabelName(String name) { 087 return LABEL_NAME_PATTERN.matcher(name).matches() && 088 !(name.startsWith("__") || name.startsWith("._") || name.startsWith("..") || name.startsWith("_.")); 089 } 090 091 /** 092 * Units may not have illegal characters, and they may not end with a reserved suffix like 'total'. 093 */ 094 public static boolean isValidUnitName(String name) { 095 return validateUnitName(name) == null; 096 } 097 098 /** 099 * 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 117 /** 118 * Get the metric or label name that is used in Prometheus exposition format. 119 * 120 * @param name must be a valid metric or label name, 121 * i.e. {@link #isValidMetricName(String) isValidMetricName(name)} 122 * or {@link #isValidLabelName(String) isValidLabelName(name)} must be true. 123 * @return the name with dots replaced by underscores. 124 */ 125 public static String prometheusName(String name) { 126 return name.replace(".", "_"); 127 } 128 129 /** 130 * Convert an arbitrary string to a name where {@link #isValidMetricName(String) isValidMetricName(name)} is true. 131 */ 132 public static String sanitizeMetricName(String metricName) { 133 if (metricName.isEmpty()) { 134 throw new IllegalArgumentException("Cannot convert an empty string to a valid metric name."); 135 } 136 String sanitizedName = replaceIllegalCharsInMetricName(metricName); 137 boolean modified = true; 138 while (modified) { 139 modified = false; 140 for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) { 141 if (sanitizedName.equals(reservedSuffix)) { 142 // This is for the corner case when you call sanitizeMetricName("_total"). 143 // In that case the result will be "total". 144 return reservedSuffix.substring(1); 145 } 146 if (sanitizedName.endsWith(reservedSuffix)) { 147 sanitizedName = sanitizedName.substring(0, sanitizedName.length() - reservedSuffix.length()); 148 modified = true; 149 } 150 } 151 } 152 return sanitizedName; 153 } 154 155 /** 156 * Like {@link #sanitizeMetricName(String)}, but also makes sure that the unit is appended 157 * as a suffix if the unit is not {@code null}. 158 */ 159 public static String sanitizeMetricName(String metricName, Unit unit) { 160 String result = sanitizeMetricName(metricName); 161 if (unit != null) { 162 if (!result.endsWith("_" + unit) && !result.endsWith("." + unit)) { 163 result += "_" + unit; 164 } 165 } 166 return result; 167 } 168 169 /** 170 * Convert an arbitrary string to a name where {@link #isValidLabelName(String) isValidLabelName(name)} is true. 171 */ 172 public static String sanitizeLabelName(String labelName) { 173 if (labelName.isEmpty()) { 174 throw new IllegalArgumentException("Cannot convert an empty string to a valid label name."); 175 } 176 String sanitizedName = replaceIllegalCharsInLabelName(labelName); 177 while (sanitizedName.startsWith("__") || sanitizedName.startsWith("_.") || sanitizedName.startsWith("._") || sanitizedName.startsWith("..")) { 178 sanitizedName = sanitizedName.substring(1); 179 } 180 return sanitizedName; 181 } 182 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 219 /** 220 * Returns a string that matches {@link #METRIC_NAME_PATTERN}. 221 */ 222 private static String replaceIllegalCharsInMetricName(String name) { 223 int length = name.length(); 224 char[] sanitized = new char[length]; 225 for (int i = 0; i < length; i++) { 226 char ch = name.charAt(i); 227 if (ch == '.' || 228 (ch >= 'a' && ch <= 'z') || 229 (ch >= 'A' && ch <= 'Z') || 230 (i > 0 && ch >= '0' && ch <= '9')) { 231 sanitized[i] = ch; 232 } else { 233 sanitized[i] = '_'; 234 } 235 } 236 return new String(sanitized); 237 } 238 239 /** 240 * Returns a string that matches {@link #LABEL_NAME_PATTERN}. 241 */ 242 private static String replaceIllegalCharsInLabelName(String name) { 243 int length = name.length(); 244 char[] sanitized = new char[length]; 245 for (int i = 0; i < length; i++) { 246 char ch = name.charAt(i); 247 if (ch == '.' || 248 (ch >= 'a' && ch <= 'z') || 249 (ch >= 'A' && ch <= 'Z') || 250 (i > 0 && ch >= '0' && ch <= '9')) { 251 sanitized[i] = ch; 252 } else { 253 sanitized[i] = '_'; 254 } 255 } 256 return new String(sanitized); 257 } 258 259 /** 260 * Returns a string that matches {@link #UNIT_NAME_PATTERN}. 261 */ 262 private static String replaceIllegalCharsInUnitName(String name) { 263 int length = name.length(); 264 char[] sanitized = new char[length]; 265 for (int i = 0; i < length; i++) { 266 char ch = name.charAt(i); 267 if (ch == ':' || 268 ch == '.' || 269 (ch >= 'a' && ch <= 'z') || 270 (ch >= 'A' && ch <= 'Z') || 271 (ch >= '0' && ch <= '9')) { 272 sanitized[i] = ch; 273 } else { 274 sanitized[i] = '_'; 275 } 276 } 277 return new String(sanitized); 278 } 279}