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