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