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