001package io.prometheus.metrics.model.snapshots; 002 003import io.prometheus.metrics.annotations.StableApi; 004import io.prometheus.metrics.config.EscapingScheme; 005import javax.annotation.Nullable; 006 007/** Immutable container for metric metadata: name, help, unit. */ 008public final class MetricMetadata { 009 010 /** 011 * Name without suffix. 012 * 013 * <p>For example, the name for a counter "http_requests_total" is "http_requests". The name of an 014 * info called "jvm_info" is "jvm". 015 * 016 * <p>We allow dots in label names. Dots are automatically replaced with underscores in Prometheus 017 * exposition formats. However, if metrics from this library are exposed in OpenTelemetry format 018 * dots are retained. 019 * 020 * <p>See {@link #MetricMetadata(String, String, Unit)} for more info on naming conventions. 021 */ 022 private final String name; 023 024 /** 025 * Same as name that all invalid char (without Unicode support) are replaced by _ 026 * 027 * <p>Multiple metrics with the same prometheusName are not allowed, because they would end up in 028 * the same time series in Prometheus if {@link EscapingScheme#UNDERSCORE_ESCAPING} or {@link 029 * EscapingScheme#DOTS_ESCAPING} is used. 030 */ 031 private final String prometheusName; 032 033 /** 034 * The base name for exposition, with unit suffix ensured and type suffix preserved. For example, 035 * for {@code Counter.builder().name("events_total").unit(BYTES)}, this is "events_total_bytes". 036 * Used by format writers for smart-append logic (e.g. deciding whether to append _total). 037 */ 038 private final String expositionBaseName; 039 040 private final String expositionBasePrometheusName; 041 042 /** 043 * The original name as provided by the user, before any modification (no suffix stripping, no 044 * unit appending). For example, for {@code Counter.builder().name("req").unit(BYTES)}, this is 045 * "req". Used by the OTel exporter with {@code preserve_names=true}. 046 */ 047 private final String originalName; 048 049 @Nullable private final String help; 050 @Nullable private final Unit unit; 051 052 /** See {@link #MetricMetadata(String, String, Unit)} */ 053 @StableApi 054 public MetricMetadata(String name) { 055 this(name, null, null); 056 } 057 058 /** See {@link #MetricMetadata(String, String, Unit)} */ 059 @StableApi 060 public MetricMetadata(String name, String help) { 061 this(name, help, null); 062 } 063 064 /** 065 * Constructor. 066 * 067 * @param name must not be {@code null}. {@link PrometheusNaming#isValidMetricName(String) 068 * isValidMetricName(name)} must be {@code true}. Use {@link 069 * PrometheusNaming#sanitizeMetricName(String)} to convert arbitrary strings into valid names. 070 * @param help optional. May be {@code null}. 071 * @param unit optional. May be {@code null}. 072 */ 073 @StableApi 074 public MetricMetadata(String name, @Nullable String help, @Nullable Unit unit) { 075 this(name, name, help, unit); 076 } 077 078 /** 079 * Creates a builder for {@link MetricMetadata}. 080 * 081 * <p>Use the builder instead of the multi-arg constructors for cleaner, more readable code: 082 * 083 * <pre>{@code 084 * MetricMetadata.builder() 085 * .name("http_requests") 086 * .help("Total HTTP requests") 087 * .unit(Unit.BYTES) 088 * .counterSuffix(true) 089 * .build(); 090 * }</pre> 091 */ 092 @StableApi 093 public static Builder builder() { 094 return new Builder(); 095 } 096 097 /** Builder for {@link MetricMetadata}. */ 098 public static final class Builder { 099 @Nullable private String name; 100 @Nullable private String expositionBaseName; 101 @Nullable private String originalName; 102 @Nullable private String help; 103 @Nullable private Unit unit; 104 private boolean counterSuffix; 105 106 private Builder() {} 107 108 /** Required. The base metric name (without type suffix like {@code _total}). */ 109 @StableApi 110 public Builder name(String name) { 111 this.name = name; 112 if (originalName == null) { 113 this.originalName = name; 114 } 115 return this; 116 } 117 118 /** 119 * Internal use only. Not part of the stable API. 120 * 121 * <p>Allows internal callers to preserve a separate exposition base name. 122 */ 123 public Builder expositionBaseName(String expositionBaseName) { 124 this.expositionBaseName = expositionBaseName; 125 return this; 126 } 127 128 /** 129 * Internal use only. Not part of the stable API. 130 * 131 * <p>Allows internal callers to preserve the raw name before normalization. 132 */ 133 public Builder originalName(String originalName) { 134 this.originalName = originalName; 135 return this; 136 } 137 138 /** Optional. Human-readable description of the metric. */ 139 @StableApi 140 public Builder help(@Nullable String help) { 141 this.help = help; 142 return this; 143 } 144 145 /** Optional. The unit of measurement. Appended to the name if not already present. */ 146 @StableApi 147 public Builder unit(@Nullable Unit unit) { 148 this.unit = unit; 149 return this; 150 } 151 152 /** 153 * Optional. When {@code true}, the writer appends {@code _total} to the exposition name. Use 154 * this for counter metrics, especially UTF-8 names where the writer cannot infer it from the 155 * snapshot type alone. 156 */ 157 @StableApi 158 public Builder counterSuffix(boolean counterSuffix) { 159 this.counterSuffix = counterSuffix; 160 return this; 161 } 162 163 /** Builds the {@link MetricMetadata}. Throws if {@code name} was not set. */ 164 @StableApi 165 public MetricMetadata build() { 166 if (name == null) { 167 throw new IllegalArgumentException("name is required"); 168 } 169 String baseName = appendUnitIfMissing(name, unit); 170 String originalName = this.originalName == null ? name : this.originalName; 171 String expositionBaseName = 172 appendUnitIfMissing( 173 this.expositionBaseName == null ? baseName : this.expositionBaseName, unit); 174 if (counterSuffix 175 && !expositionBaseName.endsWith("_total") 176 && !expositionBaseName.endsWith(".total")) { 177 expositionBaseName = expositionBaseName + "_total"; 178 } 179 return new MetricMetadata(baseName, expositionBaseName, originalName, help, unit); 180 } 181 } 182 183 /** 184 * Constructor with exposition base name. 185 * 186 * @param name the base name (with type suffixes stripped, e.g. "events" for a counter named 187 * "events_total") 188 * @param expositionBaseName the name with unit suffix ensured and type suffix preserved, used by 189 * format writers for smart-append logic 190 * @param help optional. May be {@code null}. 191 * @param unit optional. May be {@code null}. 192 * @deprecated Use {@link #builder()} instead. 193 */ 194 @StableApi 195 @Deprecated 196 public MetricMetadata( 197 String name, String expositionBaseName, @Nullable String help, @Nullable Unit unit) { 198 this(name, expositionBaseName, expositionBaseName, help, unit); 199 } 200 201 /** 202 * Constructor with exposition base name and original name. 203 * 204 * @param name the base name (with type suffixes stripped, e.g. "events" for a counter named 205 * "events_total") 206 * @param expositionBaseName the name with unit suffix ensured and type suffix preserved 207 * @param originalName the raw name as provided by the user, before any modification 208 * @param help optional. May be {@code null}. 209 * @param unit optional. May be {@code null}. 210 * @deprecated Use {@link #builder()} instead. 211 */ 212 @StableApi 213 @Deprecated 214 public MetricMetadata( 215 String name, 216 String expositionBaseName, 217 String originalName, 218 @Nullable String help, 219 @Nullable Unit unit) { 220 this.name = name; 221 this.expositionBaseName = expositionBaseName; 222 this.originalName = originalName; 223 this.help = help; 224 this.unit = unit; 225 validate(); 226 this.prometheusName = PrometheusNaming.prometheusName(name); 227 this.expositionBasePrometheusName = PrometheusNaming.prometheusName(expositionBaseName); 228 } 229 230 /** 231 * The name does not include the {@code _total} suffix for counter metrics or the {@code _info} 232 * suffix for Info metrics. 233 * 234 * <p>The name may contain any Unicode chars. Use {@link #getPrometheusName()} to get the name in 235 * legacy Prometheus format, i.e. with all dots and all invalid chars replaced by underscores. 236 */ 237 @StableApi 238 public String getName() { 239 return name; 240 } 241 242 /** 243 * Same as {@link #getName()} but with all invalid characters and dots replaced by underscores. 244 * 245 * <p>This is used by Prometheus exposition formats. 246 */ 247 @StableApi 248 public String getPrometheusName() { 249 return prometheusName; 250 } 251 252 /** 253 * The original name as provided by the user, before any modification. For example, if the user 254 * called {@code Counter.builder().name("req").unit(BYTES)}, this returns "req" while {@link 255 * #getName()} returns "req_bytes" and {@link #getExpositionBaseName()} returns "req_bytes". 256 */ 257 @StableApi 258 public String getOriginalName() { 259 return originalName; 260 } 261 262 /** 263 * The base name for exposition, with unit suffix ensured and type suffix preserved. For example, 264 * if the user called {@code Counter.builder().name("events_total")}, this returns "events_total" 265 * while {@link #getName()} returns "events". 266 */ 267 @StableApi 268 public String getExpositionBaseName() { 269 return expositionBaseName; 270 } 271 272 /** 273 * Same as {@link #getExpositionBaseName()} but with all invalid characters and dots replaced by 274 * underscores. 275 */ 276 @StableApi 277 public String getExpositionBasePrometheusName() { 278 return expositionBasePrometheusName; 279 } 280 281 @StableApi 282 @Nullable 283 public String getHelp() { 284 return help; 285 } 286 287 @StableApi 288 public boolean hasUnit() { 289 return unit != null; 290 } 291 292 @StableApi 293 @Nullable 294 public Unit getUnit() { 295 return unit; 296 } 297 298 private static String appendUnitIfMissing(String name, @Nullable Unit unit) { 299 if (unit != null && !name.endsWith("_" + unit) && !name.endsWith("." + unit)) { 300 return name + "_" + unit; 301 } 302 return name; 303 } 304 305 private void validate() { 306 if (name == null) { 307 throw new IllegalArgumentException("Missing required field: name is null"); 308 } 309 String error = PrometheusNaming.validateMetricName(name); 310 if (error != null) { 311 throw new IllegalArgumentException( 312 "'" 313 + name 314 + "': Illegal metric name. " 315 + error 316 + " Call " 317 + PrometheusNaming.class.getSimpleName() 318 + ".sanitizeMetricName(name) to avoid this error."); 319 } 320 if (hasUnit()) { 321 if (!name.endsWith("_" + unit) && !name.endsWith("." + unit)) { 322 throw new IllegalArgumentException( 323 "'" 324 + name 325 + "': Illegal metric name. If the unit is non-null, " 326 + "the name must end with the unit: _" 327 + unit 328 + "." 329 + " Call " 330 + PrometheusNaming.class.getSimpleName() 331 + ".sanitizeMetricName(name, unit) to avoid this error."); 332 } 333 } 334 } 335 336 MetricMetadata escape(EscapingScheme escapingScheme) { 337 return MetricMetadata.builder() 338 .name(PrometheusNaming.escapeName(name, escapingScheme)) 339 .expositionBaseName(PrometheusNaming.escapeName(expositionBaseName, escapingScheme)) 340 .originalName(PrometheusNaming.escapeName(originalName, escapingScheme)) 341 .help(help) 342 .unit(unit) 343 .build(); 344 } 345}