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 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(baseName); 172 for (DataPointSnapshot data : snapshot.getDataPoints()) { 173 if (data.hasCreatedTimestamp()) { 174 if (!metadataWritten) { 175 writeMetadataWithFullName(writer, baseName + "_created", "gauge", metadata); 176 metadataWritten = true; 177 } 178 writeNameAndLabels(writer, baseName, "_created", data.getLabels(), scheme, nameValidLegacy); 179 writePrometheusTimestamp(writer, data.getCreatedTimestampMillis(), timestampsInMs); 180 writeScrapeTimestampAndNewline(writer, data); 181 } 182 } 183 } 184 185 private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme) 186 throws IOException { 187 if (!snapshot.getDataPoints().isEmpty()) { 188 MetricMetadata metadata = snapshot.getMetadata(); 189 String counterName = resolveExpositionName(metadata, "_total", scheme); 190 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(counterName); 191 writeMetadataWithFullName(writer, counterName, "counter", metadata); 192 for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { 193 writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme, nameValidLegacy); 194 writeDouble(writer, data.getValue()); 195 writeScrapeTimestampAndNewline(writer, data); 196 } 197 } 198 } 199 200 private void writeGauge( 201 Writer writer, GaugeSnapshot snapshot, String rawOriginalName, EscapingScheme scheme) 202 throws IOException { 203 MetricMetadata metadata = snapshot.getMetadata(); 204 String gaugeName = getLegacyGaugeName(metadata, rawOriginalName, scheme); 205 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(gaugeName); 206 writeMetadataWithFullName(writer, gaugeName, "gauge", metadata); 207 for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { 208 writeNameAndLabels(writer, gaugeName, null, data.getLabels(), scheme, nameValidLegacy); 209 writeDouble(writer, data.getValue()); 210 writeScrapeTimestampAndNewline(writer, data); 211 } 212 } 213 214 private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme) 215 throws IOException { 216 MetricMetadata metadata = snapshot.getMetadata(); 217 writeMetadata(writer, null, "histogram", metadata, scheme); 218 String name = getMetadataName(metadata, scheme); 219 String bucketName = name + "_bucket"; 220 String countName = name + "_count"; 221 String sumName = name + "_sum"; 222 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 223 for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { 224 ClassicHistogramBuckets buckets = getClassicBuckets(data); 225 long cumulativeCount = 0; 226 for (int i = 0; i < buckets.size(); i++) { 227 cumulativeCount += buckets.getCount(i); 228 writeNameAndLabels( 229 writer, 230 bucketName, 231 null, 232 data.getLabels(), 233 scheme, 234 "le", 235 buckets.getUpperBound(i), 236 nameValidLegacy); 237 writeLong(writer, cumulativeCount); 238 writeScrapeTimestampAndNewline(writer, data); 239 } 240 if (!snapshot.isGaugeHistogram()) { 241 if (data.hasCount()) { 242 writeNameAndLabels(writer, countName, null, data.getLabels(), scheme, nameValidLegacy); 243 writeLong(writer, data.getCount()); 244 writeScrapeTimestampAndNewline(writer, data); 245 } 246 if (data.hasSum()) { 247 writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme, nameValidLegacy); 248 writeDouble(writer, data.getSum()); 249 writeScrapeTimestampAndNewline(writer, data); 250 } 251 } 252 } 253 if (snapshot.isGaugeHistogram()) { 254 writeGaugeCountSum(writer, snapshot, metadata, scheme); 255 } 256 } 257 258 private ClassicHistogramBuckets getClassicBuckets( 259 HistogramSnapshot.HistogramDataPointSnapshot data) { 260 if (data.getClassicBuckets().isEmpty()) { 261 return ClassicHistogramBuckets.of( 262 new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()}); 263 } else { 264 return data.getClassicBuckets(); 265 } 266 } 267 268 private void writeGaugeCountSum( 269 Writer writer, HistogramSnapshot snapshot, MetricMetadata metadata, EscapingScheme scheme) 270 throws IOException { 271 // Prometheus text format does not support gaugehistogram's _gcount and _gsum. 272 // So we append _gcount and _gsum as gauge metrics. 273 String baseName = getMetadataName(metadata, scheme); 274 String gaugeCountName = baseName + "_gcount"; 275 String gaugeSumName = baseName + "_gsum"; 276 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(baseName); 277 boolean metadataWritten = false; 278 for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { 279 if (data.hasCount()) { 280 if (!metadataWritten) { 281 writeMetadata(writer, "_gcount", "gauge", metadata, scheme); 282 metadataWritten = true; 283 } 284 writeNameAndLabels(writer, gaugeCountName, null, data.getLabels(), scheme, nameValidLegacy); 285 writeLong(writer, data.getCount()); 286 writeScrapeTimestampAndNewline(writer, data); 287 } 288 } 289 metadataWritten = false; 290 for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { 291 if (data.hasSum()) { 292 if (!metadataWritten) { 293 writeMetadata(writer, "_gsum", "gauge", metadata, scheme); 294 metadataWritten = true; 295 } 296 writeNameAndLabels(writer, gaugeSumName, null, data.getLabels(), scheme, nameValidLegacy); 297 writeDouble(writer, data.getSum()); 298 writeScrapeTimestampAndNewline(writer, data); 299 } 300 } 301 } 302 303 private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme) 304 throws IOException { 305 boolean metadataWritten = false; 306 MetricMetadata metadata = snapshot.getMetadata(); 307 String name = getMetadataName(metadata, scheme); 308 String countName = name + "_count"; 309 String sumName = name + "_sum"; 310 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 311 for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { 312 if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { 313 continue; 314 } 315 if (!metadataWritten) { 316 writeMetadata(writer, null, "summary", metadata, scheme); 317 metadataWritten = true; 318 } 319 for (Quantile quantile : data.getQuantiles()) { 320 writeNameAndLabels( 321 writer, 322 name, 323 null, 324 data.getLabels(), 325 scheme, 326 "quantile", 327 quantile.getQuantile(), 328 nameValidLegacy); 329 writeDouble(writer, quantile.getValue()); 330 writeScrapeTimestampAndNewline(writer, data); 331 } 332 if (data.hasCount()) { 333 writeNameAndLabels(writer, countName, null, data.getLabels(), scheme, nameValidLegacy); 334 writeLong(writer, data.getCount()); 335 writeScrapeTimestampAndNewline(writer, data); 336 } 337 if (data.hasSum()) { 338 writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme, nameValidLegacy); 339 writeDouble(writer, data.getSum()); 340 writeScrapeTimestampAndNewline(writer, data); 341 } 342 } 343 } 344 345 private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) 346 throws IOException { 347 MetricMetadata metadata = snapshot.getMetadata(); 348 String infoName = resolveExpositionName(metadata, "_info", scheme); 349 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(infoName); 350 writeMetadataWithFullName(writer, infoName, "gauge", metadata); 351 for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { 352 writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme, nameValidLegacy); 353 writer.write("1"); 354 writeScrapeTimestampAndNewline(writer, data); 355 } 356 } 357 358 private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme) 359 throws IOException { 360 MetricMetadata metadata = snapshot.getMetadata(); 361 writeMetadata(writer, null, "gauge", metadata, scheme); 362 String name = getMetadataName(metadata, scheme); 363 for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { 364 for (int i = 0; i < data.size(); i++) { 365 writer.write(name); 366 writer.write('{'); 367 for (int j = 0; j < data.getLabels().size(); j++) { 368 if (j > 0) { 369 writer.write(","); 370 } 371 writer.write(getSnapshotLabelName(data.getLabels(), j, scheme)); 372 writer.write("=\""); 373 writeEscapedString(writer, data.getLabels().getValue(j)); 374 writer.write("\""); 375 } 376 if (!data.getLabels().isEmpty()) { 377 writer.write(","); 378 } 379 writer.write(name); 380 writer.write("=\""); 381 writeEscapedString(writer, data.getName(i)); 382 writer.write("\"} "); 383 if (data.isTrue(i)) { 384 writer.write("1"); 385 } else { 386 writer.write("0"); 387 } 388 writeScrapeTimestampAndNewline(writer, data); 389 } 390 } 391 } 392 393 private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme) 394 throws IOException { 395 MetricMetadata metadata = snapshot.getMetadata(); 396 writeMetadata(writer, null, "untyped", metadata, scheme); 397 String name = getMetadataName(metadata, scheme); 398 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 399 for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { 400 writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy); 401 writeDouble(writer, data.getValue()); 402 writeScrapeTimestampAndNewline(writer, data); 403 } 404 } 405 406 private void writeNameAndLabels( 407 Writer writer, 408 String name, 409 @Nullable String suffix, 410 Labels labels, 411 EscapingScheme escapingScheme, 412 boolean nameValidLegacy) 413 throws IOException { 414 writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0, nameValidLegacy); 415 } 416 417 private void writeNameAndLabels( 418 Writer writer, 419 String name, 420 @Nullable String suffix, 421 Labels labels, 422 EscapingScheme scheme, 423 @Nullable String additionalLabelName, 424 double additionalLabelValue, 425 boolean nameValidLegacy) 426 throws IOException { 427 boolean metricInsideBraces = false; 428 // If the name does not pass the legacy validity check, we must put the 429 // metric name inside the braces. Validity is scanned once per family by the caller (the metric 430 // name is constant across all data points), not re-scanned for every series here. Appending a 431 // suffix built from legacy characters (_total, _bucket, ...) does not change legacy validity, 432 // so the caller's check of the base name applies to the suffixed name too. 433 if (!nameValidLegacy) { 434 metricInsideBraces = true; 435 writer.write('{'); 436 } 437 writeName(writer, suffix != null ? name + suffix : name, NameType.Metric, nameValidLegacy); 438 if (!labels.isEmpty() || additionalLabelName != null) { 439 writeLabels( 440 writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces, scheme); 441 } else if (metricInsideBraces) { 442 writer.write('}'); 443 } 444 writer.write(' '); 445 } 446 447 private void writeMetadata( 448 Writer writer, 449 @Nullable String suffix, 450 String typeString, 451 MetricMetadata metadata, 452 EscapingScheme scheme) 453 throws IOException { 454 String baseName = getMetadataName(metadata, scheme); 455 String name = suffix != null ? baseName + suffix : baseName; 456 if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { 457 writer.write("# HELP "); 458 writeName(writer, name, NameType.Metric); 459 writer.write(' '); 460 writeEscapedHelp(writer, metadata.getHelp()); 461 writer.write('\n'); 462 } 463 writer.write("# TYPE "); 464 writeName(writer, name, NameType.Metric); 465 writer.write(' '); 466 writer.write(typeString); 467 writer.write('\n'); 468 } 469 470 private void writeMetadataWithFullName( 471 Writer writer, String fullName, String typeString, MetricMetadata metadata) 472 throws IOException { 473 if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { 474 writer.write("# HELP "); 475 writeName(writer, fullName, NameType.Metric); 476 writer.write(' '); 477 writeEscapedHelp(writer, metadata.getHelp()); 478 writer.write('\n'); 479 } 480 writer.write("# TYPE "); 481 writeName(writer, fullName, NameType.Metric); 482 writer.write(' '); 483 writer.write(typeString); 484 writer.write('\n'); 485 } 486 487 /** 488 * Returns the full exposition name for a metric. If the original name already ends with the given 489 * suffix (e.g. "_total" for counters), uses the original name directly. Otherwise, appends the 490 * suffix to the base name. 491 */ 492 private static String resolveExpositionName( 493 MetricMetadata metadata, String suffix, EscapingScheme scheme) { 494 String expositionBaseName = getExpositionBaseMetadataName(metadata, scheme); 495 if (expositionBaseName.endsWith(suffix)) { 496 return expositionBaseName; 497 } 498 return getMetadataName(metadata, scheme) + suffix; 499 } 500 501 private static String resolveBaseName(String fullName, String suffix) { 502 if (fullName.endsWith(suffix)) { 503 return fullName.substring(0, fullName.length() - suffix.length()); 504 } 505 return fullName; 506 } 507 508 private void writeEscapedHelp(Writer writer, String s) throws IOException { 509 for (int i = 0; i < s.length(); i++) { 510 char c = s.charAt(i); 511 switch (c) { 512 case '\\': 513 writer.append("\\\\"); 514 break; 515 case '\n': 516 writer.append("\\n"); 517 break; 518 default: 519 writer.append(c); 520 } 521 } 522 } 523 524 private void writeScrapeTimestampAndNewline(Writer writer, DataPointSnapshot data) 525 throws IOException { 526 if (data.hasScrapeTimestamp()) { 527 writer.write(' '); 528 writePrometheusTimestamp(writer, data.getScrapeTimestampMillis(), timestampsInMs); 529 } 530 writer.write('\n'); 531 } 532}