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