001package io.prometheus.metrics.model.snapshots; 002 003import io.prometheus.metrics.config.EscapingScheme; 004import javax.annotation.Nullable; 005 006/** Immutable container for metric metadata: name, help, unit. */ 007public final class MetricMetadata { 008 009 /** 010 * Name without suffix. 011 * 012 * <p>For example, the name for a counter "http_requests_total" is "http_requests". The name of an 013 * info called "jvm_info" is "jvm". 014 * 015 * <p>We allow dots in label names. Dots are automatically replaced with underscores in Prometheus 016 * exposition formats. However, if metrics from this library are exposed in OpenTelemetry format 017 * dots are retained. 018 * 019 * <p>See {@link #MetricMetadata(String, String, Unit)} for more info on naming conventions. 020 */ 021 private final String name; 022 023 /** 024 * Same as name that all invalid char (without Unicode support) are replaced by _ 025 * 026 * <p>Multiple metrics with the same prometheusName are not allowed, because they would end up in 027 * the same time series in Prometheus if {@link EscapingScheme#UNDERSCORE_ESCAPING} or {@link 028 * EscapingScheme#DOTS_ESCAPING} is used. 029 */ 030 private final String prometheusName; 031 032 /** 033 * The base name for exposition, with unit suffix ensured and type suffix preserved. For example, 034 * for {@code Counter.builder().name("events_total").unit(BYTES)}, this is "events_total_bytes". 035 * Used by format writers for smart-append logic (e.g. deciding whether to append _total). 036 */ 037 private final String expositionBaseName; 038 039 private final String expositionBasePrometheusName; 040 041 /** 042 * The original name as provided by the user, before any modification (no suffix stripping, no 043 * unit appending). For example, for {@code Counter.builder().name("req").unit(BYTES)}, this is 044 * "req". Used by the OTel exporter with {@code preserve_names=true}. 045 */ 046 private final String originalName; 047 048 @Nullable private final String help; 049 @Nullable private final Unit unit; 050 051 /** See {@link #MetricMetadata(String, String, Unit)} */ 052 public MetricMetadata(String name) { 053 this(name, null, null); 054 } 055 056 /** See {@link #MetricMetadata(String, String, Unit)} */ 057 public MetricMetadata(String name, String help) { 058 this(name, help, null); 059 } 060 061 /** 062 * Constructor. 063 * 064 * @param name must not be {@code null}. {@link PrometheusNaming#isValidMetricName(String) 065 * isValidMetricName(name)} must be {@code true}. Use {@link 066 * PrometheusNaming#sanitizeMetricName(String)} to convert arbitrary strings into valid names. 067 * @param help optional. May be {@code null}. 068 * @param unit optional. May be {@code null}. 069 */ 070 public MetricMetadata(String name, @Nullable String help, @Nullable Unit unit) { 071 this(name, name, help, unit); 072 } 073 074 /** 075 * Constructor with exposition base name. 076 * 077 * @param name the base name (with type suffixes stripped, e.g. "events" for a counter named 078 * "events_total") 079 * @param expositionBaseName the name with unit suffix ensured and type suffix preserved, used by 080 * format writers for smart-append logic 081 * @param help optional. May be {@code null}. 082 * @param unit optional. May be {@code null}. 083 */ 084 public MetricMetadata( 085 String name, String expositionBaseName, @Nullable String help, @Nullable Unit unit) { 086 this(name, expositionBaseName, expositionBaseName, help, unit); 087 } 088 089 /** 090 * Constructor with exposition base name and original name. 091 * 092 * @param name the base name (with type suffixes stripped, e.g. "events" for a counter named 093 * "events_total") 094 * @param expositionBaseName the name with unit suffix ensured and type suffix preserved 095 * @param originalName the raw name as provided by the user, before any modification 096 * @param help optional. May be {@code null}. 097 * @param unit optional. May be {@code null}. 098 */ 099 public MetricMetadata( 100 String name, 101 String expositionBaseName, 102 String originalName, 103 @Nullable String help, 104 @Nullable Unit unit) { 105 this.name = name; 106 this.expositionBaseName = expositionBaseName; 107 this.originalName = originalName; 108 this.help = help; 109 this.unit = unit; 110 validate(); 111 this.prometheusName = PrometheusNaming.prometheusName(name); 112 this.expositionBasePrometheusName = PrometheusNaming.prometheusName(expositionBaseName); 113 } 114 115 /** 116 * The name does not include the {@code _total} suffix for counter metrics or the {@code _info} 117 * suffix for Info metrics. 118 * 119 * <p>The name may contain any Unicode chars. Use {@link #getPrometheusName()} to get the name in 120 * legacy Prometheus format, i.e. with all dots and all invalid chars replaced by underscores. 121 */ 122 public String getName() { 123 return name; 124 } 125 126 /** 127 * Same as {@link #getName()} but with all invalid characters and dots replaced by underscores. 128 * 129 * <p>This is used by Prometheus exposition formats. 130 */ 131 public String getPrometheusName() { 132 return prometheusName; 133 } 134 135 /** 136 * The original name as provided by the user, before any modification. For example, if the user 137 * called {@code Counter.builder().name("req").unit(BYTES)}, this returns "req" while {@link 138 * #getName()} returns "req_bytes" and {@link #getExpositionBaseName()} returns "req_bytes". 139 */ 140 public String getOriginalName() { 141 return originalName; 142 } 143 144 /** 145 * The base name for exposition, with unit suffix ensured and type suffix preserved. For example, 146 * if the user called {@code Counter.builder().name("events_total")}, this returns "events_total" 147 * while {@link #getName()} returns "events". 148 */ 149 public String getExpositionBaseName() { 150 return expositionBaseName; 151 } 152 153 /** 154 * Same as {@link #getExpositionBaseName()} but with all invalid characters and dots replaced by 155 * underscores. 156 */ 157 public String getExpositionBasePrometheusName() { 158 return expositionBasePrometheusName; 159 } 160 161 @Nullable 162 public String getHelp() { 163 return help; 164 } 165 166 public boolean hasUnit() { 167 return unit != null; 168 } 169 170 @Nullable 171 public Unit getUnit() { 172 return unit; 173 } 174 175 private void validate() { 176 if (name == null) { 177 throw new IllegalArgumentException("Missing required field: name is null"); 178 } 179 String error = PrometheusNaming.validateMetricName(name); 180 if (error != null) { 181 throw new IllegalArgumentException( 182 "'" 183 + name 184 + "': Illegal metric name. " 185 + error 186 + " Call " 187 + PrometheusNaming.class.getSimpleName() 188 + ".sanitizeMetricName(name) to avoid this error."); 189 } 190 if (hasUnit()) { 191 if (!name.endsWith("_" + unit) && !name.endsWith("." + unit)) { 192 throw new IllegalArgumentException( 193 "'" 194 + name 195 + "': Illegal metric name. If the unit is non-null, " 196 + "the name must end with the unit: _" 197 + unit 198 + "." 199 + " Call " 200 + PrometheusNaming.class.getSimpleName() 201 + ".sanitizeMetricName(name, unit) to avoid this error."); 202 } 203 } 204 } 205 206 MetricMetadata escape(EscapingScheme escapingScheme) { 207 return new MetricMetadata( 208 PrometheusNaming.escapeName(name, escapingScheme), 209 PrometheusNaming.escapeName(expositionBaseName, escapingScheme), 210 PrometheusNaming.escapeName(originalName, escapingScheme), 211 help, 212 unit); 213 } 214}