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 writer.append(Long.toString(value)); 071 } 072 073 static void writeDouble(Writer writer, double d) throws IOException { 074 if (d == Double.POSITIVE_INFINITY) { 075 writer.write("+Inf"); 076 } else if (d == Double.NEGATIVE_INFINITY) { 077 writer.write("-Inf"); 078 } else { 079 writer.write(Double.toString(d)); 080 // FloatingDecimal.getBinaryToASCIIConverter(d).appendTo(writer); 081 } 082 } 083 084 static void writePrometheusTimestamp(Writer writer, long timestampMs, boolean timestampsInMs) 085 throws IOException { 086 if (timestampsInMs) { 087 // correct for prometheus exposition format 088 // https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-details 089 writer.write(Long.toString(timestampMs)); 090 } else { 091 // incorrect for prometheus exposition format - 092 // but we need to support it for backwards compatibility 093 writeOpenMetricsTimestamp(writer, timestampMs); 094 } 095 } 096 097 static void writeOpenMetricsTimestamp(Writer writer, long timestampMs) throws IOException { 098 writer.write(Long.toString(timestampMs / 1000L)); 099 writer.write("."); 100 long ms = timestampMs % 1000; 101 if (ms < 100) { 102 writer.write("0"); 103 } 104 if (ms < 10) { 105 writer.write("0"); 106 } 107 writer.write(Long.toString(ms)); 108 } 109 110 static void writeEscapedString(Writer writer, String s) throws IOException { 111 // optimize for the common case where no escaping is needed 112 int start = 0; 113 // #indexOf is a vectorized intrinsic 114 int backslashIndex = s.indexOf('\\', start); 115 int quoteIndex = s.indexOf('\"', start); 116 int newlineIndex = s.indexOf('\n', start); 117 118 int allEscapesIndex = backslashIndex & quoteIndex & newlineIndex; 119 while (allEscapesIndex != -1) { 120 int escapeStart = Integer.MAX_VALUE; 121 if (backslashIndex != -1) { 122 escapeStart = backslashIndex; 123 } 124 if (quoteIndex != -1) { 125 escapeStart = Math.min(escapeStart, quoteIndex); 126 } 127 if (newlineIndex != -1) { 128 escapeStart = Math.min(escapeStart, newlineIndex); 129 } 130 131 // bulk write up to the first character that needs to be escaped 132 if (escapeStart > start) { 133 writer.write(s, start, escapeStart - start); 134 } 135 char c = s.charAt(escapeStart); 136 start = escapeStart + 1; 137 switch (c) { 138 case '\\': 139 writer.write("\\\\"); 140 backslashIndex = s.indexOf('\\', start); 141 break; 142 case '\"': 143 writer.write("\\\""); 144 quoteIndex = s.indexOf('\"', start); 145 break; 146 case '\n': 147 writer.write("\\n"); 148 newlineIndex = s.indexOf('\n', start); 149 break; 150 } 151 152 allEscapesIndex = backslashIndex & quoteIndex & newlineIndex; 153 } 154 // up until the end nothing needs to be escaped anymore 155 int remaining = s.length() - start; 156 if (remaining > 0) { 157 writer.write(s, start, remaining); 158 } 159 } 160 161 static void writeLabels( 162 Writer writer, 163 Labels labels, 164 @Nullable String additionalLabelName, 165 double additionalLabelValue, 166 boolean metricInsideBraces, 167 EscapingScheme scheme) 168 throws IOException { 169 if (!metricInsideBraces) { 170 writer.write('{'); 171 } 172 for (int i = 0; i < labels.size(); i++) { 173 if (i > 0 || metricInsideBraces) { 174 writer.write(","); 175 } 176 writeName(writer, SnapshotEscaper.getSnapshotLabelName(labels, i, scheme), NameType.Label); 177 writer.write("=\""); 178 writeEscapedString(writer, labels.getValue(i)); 179 writer.write("\""); 180 } 181 if (additionalLabelName != null) { 182 if (!labels.isEmpty() || metricInsideBraces) { 183 writer.write(","); 184 } 185 writer.write(additionalLabelName); 186 writer.write("=\""); 187 writeDouble(writer, additionalLabelValue); 188 writer.write("\""); 189 } 190 writer.write('}'); 191 } 192 193 static void writeName(Writer writer, String name, NameType nameType) throws IOException { 194 switch (nameType) { 195 case Metric: 196 if (PrometheusNaming.isValidLegacyMetricName(name)) { 197 writer.write(name); 198 return; 199 } 200 break; 201 case Label: 202 if (PrometheusNaming.isValidLegacyLabelName(name)) { 203 writer.write(name); 204 return; 205 } 206 break; 207 default: 208 throw new RuntimeException("Invalid name type requested: " + nameType); 209 } 210 writer.write('"'); 211 writeEscapedString(writer, name); 212 writer.write('"'); 213 } 214 215 /** 216 * Merges multiple snapshots of the same type into a single snapshot with combined data points. 217 */ 218 @SuppressWarnings("unchecked") 219 private static MetricSnapshot mergeSnapshots(List<MetricSnapshot> snapshots) { 220 MetricSnapshot first = snapshots.get(0); 221 222 int totalDataPoints = 0; 223 for (MetricSnapshot snapshot : snapshots) { 224 if (snapshot.getClass() != first.getClass()) { 225 throw new IllegalArgumentException( 226 "Cannot merge snapshots of different types: " 227 + first.getClass().getName() 228 + " and " 229 + snapshot.getClass().getName()); 230 } 231 if (first instanceof HistogramSnapshot) { 232 HistogramSnapshot histogramFirst = (HistogramSnapshot) first; 233 HistogramSnapshot histogramSnapshot = (HistogramSnapshot) snapshot; 234 if (histogramFirst.isGaugeHistogram() != histogramSnapshot.isGaugeHistogram()) { 235 throw new IllegalArgumentException( 236 "Cannot merge histograms: gauge histogram and classic histogram"); 237 } 238 } 239 // Validate metadata consistency so we don't silently pick one help/unit when they differ. 240 if (!Objects.equals( 241 first.getMetadata().getPrometheusName(), snapshot.getMetadata().getPrometheusName())) { 242 throw new IllegalArgumentException("Cannot merge snapshots: inconsistent metric name"); 243 } 244 if (!Objects.equals(first.getMetadata().getUnit(), snapshot.getMetadata().getUnit())) { 245 throw new IllegalArgumentException( 246 "Cannot merge snapshots: conflicting unit for metric " 247 + first.getMetadata().getPrometheusName()); 248 } 249 totalDataPoints += snapshot.getDataPoints().size(); 250 } 251 252 List<DataPointSnapshot> allDataPoints = new ArrayList<>(totalDataPoints); 253 for (MetricSnapshot snapshot : snapshots) { 254 allDataPoints.addAll(snapshot.getDataPoints()); 255 } 256 257 if (first instanceof CounterSnapshot) { 258 return new CounterSnapshot( 259 first.getMetadata(), 260 (Collection<CounterSnapshot.CounterDataPointSnapshot>) (Object) allDataPoints); 261 } else if (first instanceof GaugeSnapshot) { 262 return new GaugeSnapshot( 263 first.getMetadata(), 264 (Collection<GaugeSnapshot.GaugeDataPointSnapshot>) (Object) allDataPoints); 265 } else if (first instanceof HistogramSnapshot) { 266 HistogramSnapshot histFirst = (HistogramSnapshot) first; 267 return new HistogramSnapshot( 268 histFirst.isGaugeHistogram(), 269 first.getMetadata(), 270 (Collection<HistogramSnapshot.HistogramDataPointSnapshot>) (Object) allDataPoints); 271 } else if (first instanceof SummarySnapshot) { 272 return new SummarySnapshot( 273 first.getMetadata(), 274 (Collection<SummarySnapshot.SummaryDataPointSnapshot>) (Object) allDataPoints); 275 } else if (first instanceof InfoSnapshot) { 276 return new InfoSnapshot( 277 first.getMetadata(), 278 (Collection<InfoSnapshot.InfoDataPointSnapshot>) (Object) allDataPoints); 279 } else if (first instanceof StateSetSnapshot) { 280 return new StateSetSnapshot( 281 first.getMetadata(), 282 (Collection<StateSetSnapshot.StateSetDataPointSnapshot>) (Object) allDataPoints); 283 } else if (first instanceof UnknownSnapshot) { 284 return new UnknownSnapshot( 285 first.getMetadata(), 286 (Collection<UnknownSnapshot.UnknownDataPointSnapshot>) (Object) allDataPoints); 287 } else { 288 throw new IllegalArgumentException("Unknown snapshot type: " + first.getClass().getName()); 289 } 290 } 291}