001package io.prometheus.metrics.expositionformats; 002 003import io.prometheus.metrics.config.EscapingScheme; 004import io.prometheus.metrics.model.snapshots.CounterSnapshot; 005import io.prometheus.metrics.model.snapshots.DataPointSnapshot; 006import io.prometheus.metrics.model.snapshots.GaugeSnapshot; 007import io.prometheus.metrics.model.snapshots.HistogramSnapshot; 008import io.prometheus.metrics.model.snapshots.InfoSnapshot; 009import io.prometheus.metrics.model.snapshots.Labels; 010import io.prometheus.metrics.model.snapshots.MetricSnapshot; 011import io.prometheus.metrics.model.snapshots.MetricSnapshots; 012import io.prometheus.metrics.model.snapshots.PrometheusNaming; 013import io.prometheus.metrics.model.snapshots.SnapshotEscaper; 014import io.prometheus.metrics.model.snapshots.StateSetSnapshot; 015import io.prometheus.metrics.model.snapshots.SummarySnapshot; 016import io.prometheus.metrics.model.snapshots.UnknownSnapshot; 017import java.io.IOException; 018import java.io.Writer; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.LinkedHashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Objects; 025import javax.annotation.Nullable; 026 027/** 028 * Utility methods for writing Prometheus text exposition formats. 029 * 030 * <p>This class provides low-level formatting utilities used by both Prometheus text format and 031 * OpenMetrics format writers. It handles escaping, label formatting, timestamp conversion, and 032 * merging of duplicate metric names. 033 */ 034public class TextFormatUtil { 035 /** 036 * Merges snapshots with duplicate Prometheus names by combining their data points. This ensures 037 * only one HELP/TYPE declaration per metric family. 038 */ 039 public static MetricSnapshots mergeDuplicates(MetricSnapshots metricSnapshots) { 040 if (metricSnapshots.size() <= 1) { 041 return metricSnapshots; 042 } 043 044 Map<String, List<MetricSnapshot>> grouped = new LinkedHashMap<>(); 045 046 for (MetricSnapshot snapshot : metricSnapshots) { 047 String prometheusName = snapshot.getMetadata().getPrometheusName(); 048 List<MetricSnapshot> list = grouped.get(prometheusName); 049 if (list == null) { 050 list = new ArrayList<>(); 051 grouped.put(prometheusName, list); 052 } 053 list.add(snapshot); 054 } 055 056 MetricSnapshots.Builder builder = MetricSnapshots.builder(); 057 for (List<MetricSnapshot> group : grouped.values()) { 058 if (group.size() == 1) { 059 builder.metricSnapshot(group.get(0)); 060 } else { 061 MetricSnapshot merged = mergeSnapshots(group); 062 builder.metricSnapshot(merged); 063 } 064 } 065 066 return builder.build(); 067 } 068 069 static void writeLong(Writer writer, long value) throws IOException { 070 if (value == Long.MIN_VALUE) { 071 writer.write("-9223372036854775808"); 072 return; 073 } 074 char[] buf = new char[20]; 075 int pos = 20; 076 boolean negative = value < 0; 077 long v = negative ? -value : value; 078 do { 079 buf[--pos] = (char) ('0' + (v % 10)); 080 v /= 10; 081 } while (v > 0); 082 if (negative) { 083 buf[--pos] = '-'; 084 } 085 writer.write(buf, pos, 20 - pos); 086 } 087 088 static void writeDouble(Writer writer, double d) throws IOException { 089 if (d == Double.POSITIVE_INFINITY) { 090 writer.write("+Inf"); 091 } else if (d == Double.NEGATIVE_INFINITY) { 092 writer.write("-Inf"); 093 } else { 094 writer.write(Double.toString(d)); 095 } 096 } 097 098 static void writePrometheusTimestamp(Writer writer, long timestampMs, boolean timestampsInMs) 099 throws IOException { 100 if (timestampsInMs) { 101 // correct for prometheus exposition format 102 // https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-details 103 writeLong(writer, timestampMs); 104 } else { 105 // incorrect for prometheus exposition format - 106 // but we need to support it for backwards compatibility 107 writeOpenMetricsTimestamp(writer, timestampMs); 108 } 109 } 110 111 static void writeOpenMetricsTimestamp(Writer writer, long timestampMs) throws IOException { 112 writeLong(writer, timestampMs / 1000L); 113 writer.write("."); 114 long ms = timestampMs % 1000; 115 if (ms < 100) { 116 writer.write("0"); 117 } 118 if (ms < 10) { 119 writer.write("0"); 120 } 121 writeLong(writer, ms); 122 } 123 124 static void writeEscapedString(Writer writer, String s) throws IOException { 125 // optimize for the common case where no escaping is needed 126 int start = 0; 127 // #indexOf is a vectorized intrinsic 128 int backslashIndex = s.indexOf('\\', start); 129 int quoteIndex = s.indexOf('\"', start); 130 int newlineIndex = s.indexOf('\n', start); 131 132 int allEscapesIndex = backslashIndex & quoteIndex & newlineIndex; 133 while (allEscapesIndex != -1) { 134 int escapeStart = Integer.MAX_VALUE; 135 if (backslashIndex != -1) { 136 escapeStart = backslashIndex; 137 } 138 if (quoteIndex != -1) { 139 escapeStart = Math.min(escapeStart, quoteIndex); 140 } 141 if (newlineIndex != -1) { 142 escapeStart = Math.min(escapeStart, newlineIndex); 143 } 144 145 // bulk write up to the first character that needs to be escaped 146 if (escapeStart > start) { 147 writer.write(s, start, escapeStart - start); 148 } 149 char c = s.charAt(escapeStart); 150 start = escapeStart + 1; 151 switch (c) { 152 case '\\': 153 writer.write("\\\\"); 154 backslashIndex = s.indexOf('\\', start); 155 break; 156 case '\"': 157 writer.write("\\\""); 158 quoteIndex = s.indexOf('\"', start); 159 break; 160 case '\n': 161 writer.write("\\n"); 162 newlineIndex = s.indexOf('\n', start); 163 break; 164 } 165 166 allEscapesIndex = backslashIndex & quoteIndex & newlineIndex; 167 } 168 // up until the end nothing needs to be escaped anymore 169 int remaining = s.length() - start; 170 if (remaining > 0) { 171 writer.write(s, start, remaining); 172 } 173 } 174 175 static void writeLabels( 176 Writer writer, 177 Labels labels, 178 @Nullable String additionalLabelName, 179 double additionalLabelValue, 180 boolean metricInsideBraces, 181 EscapingScheme scheme) 182 throws IOException { 183 if (!metricInsideBraces) { 184 writer.write('{'); 185 } 186 for (int i = 0; i < labels.size(); i++) { 187 if (i > 0 || metricInsideBraces) { 188 writer.write(","); 189 } 190 writeName(writer, SnapshotEscaper.getSnapshotLabelName(labels, i, scheme), NameType.Label); 191 writer.write("=\""); 192 writeEscapedString(writer, labels.getValue(i)); 193 writer.write("\""); 194 } 195 if (additionalLabelName != null) { 196 if (!labels.isEmpty() || metricInsideBraces) { 197 writer.write(","); 198 } 199 writer.write(additionalLabelName); 200 writer.write("=\""); 201 writeDouble(writer, additionalLabelValue); 202 writer.write("\""); 203 } 204 writer.write('}'); 205 } 206 207 static void writeName(Writer writer, String name, NameType nameType) throws IOException { 208 switch (nameType) { 209 case Metric: 210 if (PrometheusNaming.isValidLegacyMetricName(name)) { 211 writer.write(name); 212 return; 213 } 214 break; 215 case Label: 216 if (PrometheusNaming.isValidLegacyLabelName(name)) { 217 writer.write(name); 218 return; 219 } 220 break; 221 default: 222 throw new RuntimeException("Invalid name type requested: " + nameType); 223 } 224 writer.write('"'); 225 writeEscapedString(writer, name); 226 writer.write('"'); 227 } 228 229 /** 230 * Merges multiple snapshots of the same type into a single snapshot with combined data points. 231 */ 232 @SuppressWarnings("unchecked") 233 private static MetricSnapshot mergeSnapshots(List<MetricSnapshot> snapshots) { 234 MetricSnapshot first = snapshots.get(0); 235 236 int totalDataPoints = 0; 237 for (MetricSnapshot snapshot : snapshots) { 238 if (snapshot.getClass() != first.getClass()) { 239 throw new IllegalArgumentException( 240 "Cannot merge snapshots of different types: " 241 + first.getClass().getName() 242 + " and " 243 + snapshot.getClass().getName()); 244 } 245 if (first instanceof HistogramSnapshot) { 246 HistogramSnapshot histogramFirst = (HistogramSnapshot) first; 247 HistogramSnapshot histogramSnapshot = (HistogramSnapshot) snapshot; 248 if (histogramFirst.isGaugeHistogram() != histogramSnapshot.isGaugeHistogram()) { 249 throw new IllegalArgumentException( 250 "Cannot merge histograms: gauge histogram and classic histogram"); 251 } 252 } 253 // Validate metadata consistency so we don't silently pick one help/unit when they differ. 254 if (!Objects.equals( 255 first.getMetadata().getPrometheusName(), snapshot.getMetadata().getPrometheusName())) { 256 throw new IllegalArgumentException("Cannot merge snapshots: inconsistent metric name"); 257 } 258 if (!Objects.equals(first.getMetadata().getUnit(), snapshot.getMetadata().getUnit())) { 259 throw new IllegalArgumentException( 260 "Cannot merge snapshots: conflicting unit for metric " 261 + first.getMetadata().getPrometheusName()); 262 } 263 totalDataPoints += snapshot.getDataPoints().size(); 264 } 265 266 List<DataPointSnapshot> allDataPoints = new ArrayList<>(totalDataPoints); 267 for (MetricSnapshot snapshot : snapshots) { 268 allDataPoints.addAll(snapshot.getDataPoints()); 269 } 270 271 if (first instanceof CounterSnapshot) { 272 return new CounterSnapshot( 273 first.getMetadata(), 274 (Collection<CounterSnapshot.CounterDataPointSnapshot>) (Object) allDataPoints); 275 } else if (first instanceof GaugeSnapshot) { 276 return new GaugeSnapshot( 277 first.getMetadata(), 278 (Collection<GaugeSnapshot.GaugeDataPointSnapshot>) (Object) allDataPoints); 279 } else if (first instanceof HistogramSnapshot) { 280 HistogramSnapshot histFirst = (HistogramSnapshot) first; 281 return new HistogramSnapshot( 282 histFirst.isGaugeHistogram(), 283 first.getMetadata(), 284 (Collection<HistogramSnapshot.HistogramDataPointSnapshot>) (Object) allDataPoints); 285 } else if (first instanceof SummarySnapshot) { 286 return new SummarySnapshot( 287 first.getMetadata(), 288 (Collection<SummarySnapshot.SummaryDataPointSnapshot>) (Object) allDataPoints); 289 } else if (first instanceof InfoSnapshot) { 290 return new InfoSnapshot( 291 first.getMetadata(), 292 (Collection<InfoSnapshot.InfoDataPointSnapshot>) (Object) allDataPoints); 293 } else if (first instanceof StateSetSnapshot) { 294 return new StateSetSnapshot( 295 first.getMetadata(), 296 (Collection<StateSetSnapshot.StateSetDataPointSnapshot>) (Object) allDataPoints); 297 } else if (first instanceof UnknownSnapshot) { 298 return new UnknownSnapshot( 299 first.getMetadata(), 300 (Collection<UnknownSnapshot.UnknownDataPointSnapshot>) (Object) allDataPoints); 301 } else { 302 throw new IllegalArgumentException("Unknown snapshot type: " + first.getClass().getName()); 303 } 304 } 305}