001package io.prometheus.metrics.expositionformats; 002 003import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeDouble; 004import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeEscapedString; 005import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLabels; 006import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong; 007import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeName; 008import static io.prometheus.metrics.expositionformats.TextFormatUtil.writePrometheusTimestamp; 009import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.escapeMetricSnapshot; 010import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName; 011import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getLegacyGaugeName; 012import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getMetadataName; 013import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName; 014 015import io.prometheus.metrics.annotations.StableApi; 016import io.prometheus.metrics.config.EscapingScheme; 017import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets; 018import io.prometheus.metrics.model.snapshots.CounterSnapshot; 019import io.prometheus.metrics.model.snapshots.DataPointSnapshot; 020import io.prometheus.metrics.model.snapshots.GaugeSnapshot; 021import io.prometheus.metrics.model.snapshots.HistogramSnapshot; 022import io.prometheus.metrics.model.snapshots.InfoSnapshot; 023import io.prometheus.metrics.model.snapshots.Labels; 024import io.prometheus.metrics.model.snapshots.MetricMetadata; 025import io.prometheus.metrics.model.snapshots.MetricSnapshot; 026import io.prometheus.metrics.model.snapshots.MetricSnapshots; 027import io.prometheus.metrics.model.snapshots.PrometheusNaming; 028import io.prometheus.metrics.model.snapshots.Quantile; 029import io.prometheus.metrics.model.snapshots.StateSetSnapshot; 030import io.prometheus.metrics.model.snapshots.SummarySnapshot; 031import io.prometheus.metrics.model.snapshots.UnknownSnapshot; 032import java.io.BufferedWriter; 033import java.io.IOException; 034import java.io.OutputStream; 035import java.io.OutputStreamWriter; 036import java.io.Writer; 037import java.nio.charset.StandardCharsets; 038import javax.annotation.Nullable; 039 040/** 041 * Write the Prometheus text format. This is the default if you view a Prometheus endpoint with your 042 * Web browser. 043 */ 044@StableApi 045public class PrometheusTextFormatWriter implements ExpositionFormatWriter { 046 047 public static final String CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"; 048 049 private final boolean writeCreatedTimestamps; 050 private final boolean timestampsInMs; 051 052 public static class Builder { 053 boolean includeCreatedTimestamps; 054 boolean timestampsInMs = true; 055 056 private Builder() {} 057 058 /** 059 * @param includeCreatedTimestamps whether to include the _created timestamp in the output 060 */ 061 public Builder setIncludeCreatedTimestamps(boolean includeCreatedTimestamps) { 062 this.includeCreatedTimestamps = includeCreatedTimestamps; 063 return this; 064 } 065 066 /** 067 * @deprecated Use {@link #build()} with the default millisecond timestamps instead. 068 */ 069 @Deprecated 070 public Builder setTimestampsInMs(boolean timestampsInMs) { 071 this.timestampsInMs = timestampsInMs; 072 return this; 073 } 074 075 public PrometheusTextFormatWriter build() { 076 return new PrometheusTextFormatWriter(includeCreatedTimestamps, timestampsInMs); 077 } 078 } 079 080 /** 081 * @param writeCreatedTimestamps whether to include the _created timestamp in the output - This 082 * will produce an invalid OpenMetrics output, but is kept for backwards compatibility. 083 * @deprecated this constructor is deprecated and will be removed in the next major version - 084 * {@link #builder()} or {@link #create()} instead 085 */ 086 @Deprecated 087 public PrometheusTextFormatWriter(boolean writeCreatedTimestamps) { 088 this(writeCreatedTimestamps, false); 089 } 090 091 private PrometheusTextFormatWriter(boolean writeCreatedTimestamps, boolean timestampsInMs) { 092 this.writeCreatedTimestamps = writeCreatedTimestamps; 093 this.timestampsInMs = timestampsInMs; 094 } 095 096 public static PrometheusTextFormatWriter.Builder builder() { 097 return new Builder(); 098 } 099 100 public static PrometheusTextFormatWriter create() { 101 return builder().build(); 102 } 103 104 @Override 105 public boolean accepts(@Nullable String acceptHeader) { 106 if (acceptHeader == null) { 107 return false; 108 } else { 109 return acceptHeader.contains("text/plain"); 110 } 111 } 112 113 @Override 114 public String getContentType() { 115 return CONTENT_TYPE; 116 } 117 118 @Override 119 public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme scheme) 120 throws IOException { 121 // See https://prometheus.io/docs/instrumenting/exposition_formats/ 122 // "unknown", "gauge", "counter", "stateset", "info", "histogram", "gaugehistogram", and 123 // "summary". 124 Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); 125 MetricSnapshots merged = TextFormatUtil.mergeDuplicates(metricSnapshots); 126 for (MetricSnapshot s : merged) { 127 MetricSnapshot snapshot = escapeMetricSnapshot(s, scheme); 128 if (!snapshot.getDataPoints().isEmpty()) { 129 if (snapshot instanceof CounterSnapshot) { 130 writeCounter(writer, (CounterSnapshot) snapshot, scheme); 131 } else if (snapshot instanceof GaugeSnapshot) { 132 writeGauge(writer, (GaugeSnapshot) snapshot, s.getMetadata().getOriginalName(), scheme); 133 } else if (snapshot instanceof HistogramSnapshot) { 134 writeHistogram(writer, (HistogramSnapshot) snapshot, scheme); 135 } else if (snapshot instanceof SummarySnapshot) { 136 writeSummary(writer, (SummarySnapshot) snapshot, scheme); 137 } else if (snapshot instanceof InfoSnapshot) { 138 writeInfo(writer, (InfoSnapshot) snapshot, scheme); 139 } else if (snapshot instanceof StateSetSnapshot) { 140 writeStateSet(writer, (StateSetSnapshot) snapshot, scheme); 141 } else if (snapshot instanceof UnknownSnapshot) { 142 writeUnknown(writer, (UnknownSnapshot) snapshot, scheme); 143 } 144 } 145 } 146 if (writeCreatedTimestamps) { 147 for (MetricSnapshot s : merged) { 148 MetricSnapshot snapshot = escapeMetricSnapshot(s, scheme); 149 if (!snapshot.getDataPoints().isEmpty()) { 150 if (snapshot instanceof CounterSnapshot) { 151 writeCreated(writer, snapshot, scheme); 152 } else if (snapshot instanceof HistogramSnapshot) { 153 writeCreated(writer, snapshot, scheme); 154 } else if (snapshot instanceof SummarySnapshot) { 155 writeCreated(writer, snapshot, scheme); 156 } 157 } 158 } 159 } 160 writer.flush(); 161 } 162 163 public void writeCreated(Writer writer, MetricSnapshot snapshot, EscapingScheme scheme) 164 throws IOException { 165 boolean metadataWritten = false; 166 MetricMetadata metadata = snapshot.getMetadata(); 167 String baseName = getMetadataName(metadata, scheme); 168 if (snapshot instanceof CounterSnapshot) { 169 baseName = resolveBaseName(resolveExpositionName(metadata, "_total", scheme), "_total"); 170 } 171 for (DataPointSnapshot data : snapshot.getDataPoints()) { 172 if (data.hasCreatedTimestamp()) { 173 if (!metadataWritten) { 174 writeMetadataWithFullName(writer, baseName + "_created", "gauge", metadata); 175 metadataWritten = true; 176 } 177 writeNameAndLabels(writer, baseName, "_created", data.getLabels(), scheme); 178 writePrometheusTimestamp(writer, data.getCreatedTimestampMillis(), timestampsInMs); 179 writeScrapeTimestampAndNewline(writer, data); 180 } 181 } 182 } 183 184 private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme) 185 throws IOException { 186 if (!snapshot.getDataPoints().isEmpty()) { 187 MetricMetadata metadata = snapshot.getMetadata(); 188 String counterName = resolveExpositionName(metadata, "_total", scheme); 189 writeMetadataWithFullName(writer, counterName, "counter", metadata); 190 for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { 191 writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme); 192 writeDouble(writer, data.getValue()); 193 writeScrapeTimestampAndNewline(writer, data); 194 } 195 } 196 } 197 198 private void writeGauge( 199 Writer writer, GaugeSnapshot snapshot, String rawOriginalName, EscapingScheme scheme) 200 throws IOException { 201 MetricMetadata metadata = snapshot.getMetadata(); 202 String gaugeName = getLegacyGaugeName(metadata, rawOriginalName, scheme); 203 writeMetadataWithFullName(writer, gaugeName, "gauge", metadata); 204 for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { 205 writeNameAndLabels(writer, gaugeName, null, data.getLabels(), scheme); 206 writeDouble(writer, data.getValue()); 207 writeScrapeTimestampAndNewline(writer, data); 208 } 209 } 210 211 private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme) 212 throws IOException { 213 MetricMetadata metadata = snapshot.getMetadata(); 214 writeMetadata(writer, null, "histogram", metadata, scheme); 215 String name = getMetadataName(metadata, scheme); 216 String bucketName = name + "_bucket"; 217 String countName = name + "_count"; 218 String sumName = name + "_sum"; 219 for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { 220 ClassicHistogramBuckets buckets = getClassicBuckets(data); 221 long cumulativeCount = 0; 222 for (int i = 0; i < buckets.size(); i++) { 223 cumulativeCount += buckets.getCount(i); 224 writeNameAndLabels( 225 writer, bucketName, null, data.getLabels(), scheme, "le", buckets.getUpperBound(i)); 226 writeLong(writer, cumulativeCount); 227 writeScrapeTimestampAndNewline(writer, data); 228 } 229 if (!snapshot.isGaugeHistogram()) { 230 if (data.hasCount()) { 231 writeNameAndLabels(writer, countName, null, data.getLabels(), scheme); 232 writeLong(writer, data.getCount()); 233 writeScrapeTimestampAndNewline(writer, data); 234 } 235 if (data.hasSum()) { 236 writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme); 237 writeDouble(writer, data.getSum()); 238 writeScrapeTimestampAndNewline(writer, data); 239 } 240 } 241 } 242 if (snapshot.isGaugeHistogram()) { 243 writeGaugeCountSum(writer, snapshot, metadata, scheme); 244 } 245 } 246 247 private ClassicHistogramBuckets getClassicBuckets( 248 HistogramSnapshot.HistogramDataPointSnapshot data) { 249 if (data.getClassicBuckets().isEmpty()) { 250 return ClassicHistogramBuckets.of( 251 new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()}); 252 } else { 253 return data.getClassicBuckets(); 254 } 255 } 256 257 private void writeGaugeCountSum( 258 Writer writer, HistogramSnapshot snapshot, MetricMetadata metadata, EscapingScheme scheme) 259 throws IOException { 260 // Prometheus text format does not support gaugehistogram's _gcount and _gsum. 261 // So we append _gcount and _gsum as gauge metrics. 262 String baseName = getMetadataName(metadata, scheme); 263 String gaugeCountName = baseName + "_gcount"; 264 String gaugeSumName = baseName + "_gsum"; 265 boolean metadataWritten = false; 266 for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { 267 if (data.hasCount()) { 268 if (!metadataWritten) { 269 writeMetadata(writer, "_gcount", "gauge", metadata, scheme); 270 metadataWritten = true; 271 } 272 writeNameAndLabels(writer, gaugeCountName, null, data.getLabels(), scheme); 273 writeLong(writer, data.getCount()); 274 writeScrapeTimestampAndNewline(writer, data); 275 } 276 } 277 metadataWritten = false; 278 for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { 279 if (data.hasSum()) { 280 if (!metadataWritten) { 281 writeMetadata(writer, "_gsum", "gauge", metadata, scheme); 282 metadataWritten = true; 283 } 284 writeNameAndLabels(writer, gaugeSumName, null, data.getLabels(), scheme); 285 writeDouble(writer, data.getSum()); 286 writeScrapeTimestampAndNewline(writer, data); 287 } 288 } 289 } 290 291 private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme) 292 throws IOException { 293 boolean metadataWritten = false; 294 MetricMetadata metadata = snapshot.getMetadata(); 295 String name = getMetadataName(metadata, scheme); 296 String countName = name + "_count"; 297 String sumName = name + "_sum"; 298 for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { 299 if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { 300 continue; 301 } 302 if (!metadataWritten) { 303 writeMetadata(writer, null, "summary", metadata, scheme); 304 metadataWritten = true; 305 } 306 for (Quantile quantile : data.getQuantiles()) { 307 writeNameAndLabels( 308 writer, name, null, data.getLabels(), scheme, "quantile", quantile.getQuantile()); 309 writeDouble(writer, quantile.getValue()); 310 writeScrapeTimestampAndNewline(writer, data); 311 } 312 if (data.hasCount()) { 313 writeNameAndLabels(writer, countName, null, data.getLabels(), scheme); 314 writeLong(writer, data.getCount()); 315 writeScrapeTimestampAndNewline(writer, data); 316 } 317 if (data.hasSum()) { 318 writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme); 319 writeDouble(writer, data.getSum()); 320 writeScrapeTimestampAndNewline(writer, data); 321 } 322 } 323 } 324 325 private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) 326 throws IOException { 327 MetricMetadata metadata = snapshot.getMetadata(); 328 String infoName = resolveExpositionName(metadata, "_info", scheme); 329 writeMetadataWithFullName(writer, infoName, "gauge", metadata); 330 for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { 331 writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme); 332 writer.write("1"); 333 writeScrapeTimestampAndNewline(writer, data); 334 } 335 } 336 337 private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme) 338 throws IOException { 339 MetricMetadata metadata = snapshot.getMetadata(); 340 writeMetadata(writer, null, "gauge", metadata, scheme); 341 String name = getMetadataName(metadata, scheme); 342 for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { 343 for (int i = 0; i < data.size(); i++) { 344 writer.write(name); 345 writer.write('{'); 346 for (int j = 0; j < data.getLabels().size(); j++) { 347 if (j > 0) { 348 writer.write(","); 349 } 350 writer.write(getSnapshotLabelName(data.getLabels(), j, scheme)); 351 writer.write("=\""); 352 writeEscapedString(writer, data.getLabels().getValue(j)); 353 writer.write("\""); 354 } 355 if (!data.getLabels().isEmpty()) { 356 writer.write(","); 357 } 358 writer.write(name); 359 writer.write("=\""); 360 writeEscapedString(writer, data.getName(i)); 361 writer.write("\"} "); 362 if (data.isTrue(i)) { 363 writer.write("1"); 364 } else { 365 writer.write("0"); 366 } 367 writeScrapeTimestampAndNewline(writer, data); 368 } 369 } 370 } 371 372 private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme) 373 throws IOException { 374 MetricMetadata metadata = snapshot.getMetadata(); 375 writeMetadata(writer, null, "untyped", metadata, scheme); 376 String name = getMetadataName(metadata, scheme); 377 for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { 378 writeNameAndLabels(writer, name, null, data.getLabels(), scheme); 379 writeDouble(writer, data.getValue()); 380 writeScrapeTimestampAndNewline(writer, data); 381 } 382 } 383 384 private void writeNameAndLabels( 385 Writer writer, 386 String name, 387 @Nullable String suffix, 388 Labels labels, 389 EscapingScheme escapingScheme) 390 throws IOException { 391 writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0); 392 } 393 394 private void writeNameAndLabels( 395 Writer writer, 396 String name, 397 @Nullable String suffix, 398 Labels labels, 399 EscapingScheme scheme, 400 @Nullable String additionalLabelName, 401 double additionalLabelValue) 402 throws IOException { 403 boolean metricInsideBraces = false; 404 // If the name does not pass the legacy validity check, we must put the 405 // metric name inside the braces. 406 if (!PrometheusNaming.isValidLegacyMetricName(name)) { 407 metricInsideBraces = true; 408 writer.write('{'); 409 } 410 writeName(writer, suffix != null ? name + suffix : name, NameType.Metric); 411 if (!labels.isEmpty() || additionalLabelName != null) { 412 writeLabels( 413 writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces, scheme); 414 } else if (metricInsideBraces) { 415 writer.write('}'); 416 } 417 writer.write(' '); 418 } 419 420 private void writeMetadata( 421 Writer writer, 422 @Nullable String suffix, 423 String typeString, 424 MetricMetadata metadata, 425 EscapingScheme scheme) 426 throws IOException { 427 String baseName = getMetadataName(metadata, scheme); 428 String name = suffix != null ? baseName + suffix : baseName; 429 if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { 430 writer.write("# HELP "); 431 writeName(writer, name, NameType.Metric); 432 writer.write(' '); 433 writeEscapedHelp(writer, metadata.getHelp()); 434 writer.write('\n'); 435 } 436 writer.write("# TYPE "); 437 writeName(writer, name, NameType.Metric); 438 writer.write(' '); 439 writer.write(typeString); 440 writer.write('\n'); 441 } 442 443 private void writeMetadataWithFullName( 444 Writer writer, String fullName, String typeString, MetricMetadata metadata) 445 throws IOException { 446 if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { 447 writer.write("# HELP "); 448 writeName(writer, fullName, NameType.Metric); 449 writer.write(' '); 450 writeEscapedHelp(writer, metadata.getHelp()); 451 writer.write('\n'); 452 } 453 writer.write("# TYPE "); 454 writeName(writer, fullName, NameType.Metric); 455 writer.write(' '); 456 writer.write(typeString); 457 writer.write('\n'); 458 } 459 460 /** 461 * Returns the full exposition name for a metric. If the original name already ends with the given 462 * suffix (e.g. "_total" for counters), uses the original name directly. Otherwise, appends the 463 * suffix to the base name. 464 */ 465 private static String resolveExpositionName( 466 MetricMetadata metadata, String suffix, EscapingScheme scheme) { 467 String expositionBaseName = getExpositionBaseMetadataName(metadata, scheme); 468 if (expositionBaseName.endsWith(suffix)) { 469 return expositionBaseName; 470 } 471 return getMetadataName(metadata, scheme) + suffix; 472 } 473 474 private static String resolveBaseName(String fullName, String suffix) { 475 if (fullName.endsWith(suffix)) { 476 return fullName.substring(0, fullName.length() - suffix.length()); 477 } 478 return fullName; 479 } 480 481 private void writeEscapedHelp(Writer writer, String s) throws IOException { 482 for (int i = 0; i < s.length(); i++) { 483 char c = s.charAt(i); 484 switch (c) { 485 case '\\': 486 writer.append("\\\\"); 487 break; 488 case '\n': 489 writer.append("\\n"); 490 break; 491 default: 492 writer.append(c); 493 } 494 } 495 } 496 497 private void writeScrapeTimestampAndNewline(Writer writer, DataPointSnapshot data) 498 throws IOException { 499 if (data.hasScrapeTimestamp()) { 500 writer.write(' '); 501 writePrometheusTimestamp(writer, data.getScrapeTimestampMillis(), timestampsInMs); 502 } 503 writer.write('\n'); 504 } 505}