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 * According to OpenMetrics {@code _count} and {@code _sum} (and {@code _gcount}, {@code _gsum}) should also be 025 * reserved metric name suffixes. However, popular instrumentation libraries have Gauges with names 026 * ending in {@code _count}. 027 * Examples: 028 * <ul> 029 * <li>Micrometer: {@code jvm_buffer_count}</li> 030 * <li>OpenTelemetry: {@code process_runtime_jvm_buffer_count}</li> 031 * </ul> 032 * We do not treat {@code _count} and {@code _sum} as reserved suffixes here for compatibility with these libraries. 033 * However, there is a risk of name conflict if someone creates a gauge named {@code my_data_count} and a 034 * histogram or summary named {@code my_data}, because the histogram or summary will implicitly have a sample 035 * named {@code my_data_count}. 036 */ 037 private static final String[] RESERVED_METRIC_NAME_SUFFIXES = { 038 "_total", "_created", "_bucket", "_info", 039 ".total", ".created", ".bucket", ".info" 040 }; 041 042 /** 043 * Test if a metric name is valid. Rules: 044 * <ul> 045 * <li>The name must match {@link #METRIC_NAME_PATTERN}.</li> 046 * <li>The name MUST NOT end with one of the {@link #RESERVED_METRIC_NAME_SUFFIXES}.</li> 047 * </ul> 048 * If a metric has a {@link Unit}, the metric name SHOULD end with the unit as a suffix. 049 * Note that <a href="https://openmetrics.io/">OpenMetrics</a> requires metric names to have their unit as suffix, 050 * and we implement this in {@code prometheus-metrics-core}. However, {@code prometheus-metrics-model} 051 * does not enforce Unit suffixes. 052 * <p> 053 * Example: If you create a Counter for a processing time with Unit {@link Unit#SECONDS SECONDS}, 054 * the name should be {@code processing_time_seconds}. When exposed in OpenMetrics Text format, 055 * this will be represented as two values: {@code processing_time_seconds_total} for the counter value, 056 * and the optional {@code processing_time_seconds_created} timestamp. 057 * <p> 058 * Use {@link #sanitizeMetricName(String)} to convert arbitrary Strings to valid metric names. 059 */ 060 public static boolean isValidMetricName(String name) { 061 return validateMetricName(name) == null; 062 } 063 064 /** 065 * Same as {@link #isValidMetricName(String)}, but produces an error message. 066 * <p> 067 * The name is valid if the error message is {@code null}. 068 */ 069 static String validateMetricName(String name) { 070 for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) { 071 if (name.endsWith(reservedSuffix)) { 072 return "The metric name must not include the '" + reservedSuffix + "' suffix."; 073 } 074 } 075 if (!METRIC_NAME_PATTERN.matcher(name).matches()) { 076 return "The metric name contains unsupported characters"; 077 } 078 return null; 079 } 080 081 public static boolean isValidLabelName(String name) { 082 return LABEL_NAME_PATTERN.matcher(name).matches() && 083 !(name.startsWith("__") || name.startsWith("._") || name.startsWith("..") || name.startsWith("_.")); 084 } 085 086 /** 087 * Get the metric or label name that is used in Prometheus exposition format. 088 * 089 * @param name must be a valid metric or label name, 090 * i.e. {@link #isValidMetricName(String) isValidMetricName(name)} 091 * or {@link #isValidLabelName(String) isValidLabelName(name)} must be true. 092 * @return the name with dots replaced by underscores. 093 */ 094 public static String prometheusName(String name) { 095 return name.replace(".", "_"); 096 } 097 098 /** 099 * Convert an arbitrary string to a name where {@link #isValidMetricName(String) isValidMetricName(name)} is true. 100 */ 101 public static String sanitizeMetricName(String metricName) { 102 if (metricName.isEmpty()) { 103 throw new IllegalArgumentException("Cannot convert an empty string to a valid metric name."); 104 } 105 String sanitizedName = replaceIllegalCharsInMetricName(metricName); 106 boolean modified = true; 107 while (modified) { 108 modified = false; 109 for (String reservedSuffix : RESERVED_METRIC_NAME_SUFFIXES) { 110 if (sanitizedName.equals(reservedSuffix)) { 111 // This is for the corner case when you call sanitizeMetricName("_total"). 112 // In that case the result will be "total". 113 return reservedSuffix.substring(1); 114 } 115 if (sanitizedName.endsWith(reservedSuffix)) { 116 sanitizedName = sanitizedName.substring(0, sanitizedName.length() - reservedSuffix.length()); 117 modified = true; 118 } 119 } 120 } 121 return sanitizedName; 122 } 123 124 /** 125 * Convert an arbitrary string to a name where {@link #isValidLabelName(String) isValidLabelName(name)} is true. 126 */ 127 public static String sanitizeLabelName(String labelName) { 128 if (labelName.isEmpty()) { 129 throw new IllegalArgumentException("Cannot convert an empty string to a valid label name."); 130 } 131 String sanitizedName = replaceIllegalCharsInLabelName(labelName); 132 while (sanitizedName.startsWith("__") || sanitizedName.startsWith("_.") || sanitizedName.startsWith("._") || sanitizedName.startsWith("..")) { 133 sanitizedName = sanitizedName.substring(1); 134 } 135 return sanitizedName; 136 } 137 138 /** 139 * Returns a string that matches {@link #METRIC_NAME_PATTERN}. 140 */ 141 private static String replaceIllegalCharsInMetricName(String name) { 142 int length = name.length(); 143 char[] sanitized = new char[length]; 144 for (int i = 0; i < length; i++) { 145 char ch = name.charAt(i); 146 if (ch == ':' || 147 ch == '.' || 148 (ch >= 'a' && ch <= 'z') || 149 (ch >= 'A' && ch <= 'Z') || 150 (i > 0 && ch >= '0' && ch <= '9')) { 151 sanitized[i] = ch; 152 } else { 153 sanitized[i] = '_'; 154 } 155 } 156 return new String(sanitized); 157 } 158 159 /** 160 * Returns a string that matches {@link #LABEL_NAME_PATTERN}. 161 */ 162 private static String replaceIllegalCharsInLabelName(String name) { 163 int length = name.length(); 164 char[] sanitized = new char[length]; 165 for (int i = 0; i < length; i++) { 166 char ch = name.charAt(i); 167 if (ch == '.' || 168 (ch >= 'a' && ch <= 'z') || 169 (ch >= 'A' && ch <= 'Z') || 170 (i > 0 && ch >= '0' && ch <= '9')) { 171 sanitized[i] = ch; 172 } else { 173 sanitized[i] = '_'; 174 } 175 } 176 return new String(sanitized); 177 } 178}