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.writeOpenMetricsTimestamp; 009import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName; 010import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getOriginalMetadataName; 011import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName; 012 013import io.prometheus.metrics.annotations.StableApi; 014import io.prometheus.metrics.config.EscapingScheme; 015import io.prometheus.metrics.config.OpenMetrics2Properties; 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.Exemplar; 020import io.prometheus.metrics.model.snapshots.Exemplars; 021import io.prometheus.metrics.model.snapshots.GaugeSnapshot; 022import io.prometheus.metrics.model.snapshots.HistogramSnapshot; 023import io.prometheus.metrics.model.snapshots.InfoSnapshot; 024import io.prometheus.metrics.model.snapshots.Labels; 025import io.prometheus.metrics.model.snapshots.MetricMetadata; 026import io.prometheus.metrics.model.snapshots.MetricSnapshot; 027import io.prometheus.metrics.model.snapshots.MetricSnapshots; 028import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets; 029import io.prometheus.metrics.model.snapshots.PrometheusNaming; 030import io.prometheus.metrics.model.snapshots.Quantile; 031import io.prometheus.metrics.model.snapshots.SnapshotEscaper; 032import io.prometheus.metrics.model.snapshots.StateSetSnapshot; 033import io.prometheus.metrics.model.snapshots.SummarySnapshot; 034import io.prometheus.metrics.model.snapshots.UnknownSnapshot; 035import java.io.BufferedWriter; 036import java.io.IOException; 037import java.io.OutputStream; 038import java.io.OutputStreamWriter; 039import java.io.Writer; 040import java.nio.charset.StandardCharsets; 041import javax.annotation.Nullable; 042 043/** 044 * Write the OpenMetrics 2.0 text format. By default, this writer preserves metric names as provided 045 * by the user. Unit and type suffixes can be appended for OM1 compatibility. The {@code _info} 046 * suffix is always enforced per the OM2 spec (MUST). This is experimental and subject to change as 047 * the <a 048 * href="https://github.com/prometheus/docs/blob/main/docs/specs/om/open_metrics_spec_2_0.md">OpenMetrics 049 * 2.0 specification</a> evolves. 050 */ 051@StableApi 052public class OpenMetrics2TextFormatWriter implements ExpositionFormatWriter { 053 054 public static class Builder { 055 private OpenMetrics2Properties openMetrics2Properties = 056 OpenMetrics2Properties.builder().build(); 057 boolean createdTimestampsEnabled; 058 boolean exemplarsOnAllMetricTypesEnabled; 059 060 private Builder() {} 061 062 /** 063 * @param openMetrics2Properties OpenMetrics 2.0 feature flags 064 */ 065 public Builder setOpenMetrics2Properties(OpenMetrics2Properties openMetrics2Properties) { 066 this.openMetrics2Properties = openMetrics2Properties; 067 return this; 068 } 069 070 /** 071 * @param createdTimestampsEnabled whether delegated OM1 output includes _created metrics 072 */ 073 public Builder setCreatedTimestampsEnabled(boolean createdTimestampsEnabled) { 074 this.createdTimestampsEnabled = createdTimestampsEnabled; 075 return this; 076 } 077 078 /** 079 * @param exemplarsOnAllMetricTypesEnabled whether to include exemplars in the output for all 080 * metric types 081 */ 082 public Builder setExemplarsOnAllMetricTypesEnabled(boolean exemplarsOnAllMetricTypesEnabled) { 083 this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled; 084 return this; 085 } 086 087 public OpenMetrics2TextFormatWriter build() { 088 return new OpenMetrics2TextFormatWriter( 089 openMetrics2Properties, createdTimestampsEnabled, exemplarsOnAllMetricTypesEnabled); 090 } 091 } 092 093 public static final String CONTENT_TYPE = 094 "application/openmetrics-text; version=2.0.0; charset=utf-8"; 095 private final OpenMetrics2Properties openMetrics2Properties; 096 private final boolean createdTimestampsEnabled; 097 private final boolean exemplarsOnAllMetricTypesEnabled; 098 private final OpenMetricsTextFormatWriter om1Writer; 099 100 /** 101 * @param openMetrics2Properties OpenMetrics 2.0 feature flags 102 * @param createdTimestampsEnabled whether delegated OM1 output includes _created metrics 103 * @param exemplarsOnAllMetricTypesEnabled whether to include exemplars on all metric types 104 */ 105 public OpenMetrics2TextFormatWriter( 106 OpenMetrics2Properties openMetrics2Properties, 107 boolean createdTimestampsEnabled, 108 boolean exemplarsOnAllMetricTypesEnabled) { 109 this.openMetrics2Properties = openMetrics2Properties; 110 this.createdTimestampsEnabled = createdTimestampsEnabled; 111 this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled; 112 this.om1Writer = 113 new OpenMetricsTextFormatWriter(createdTimestampsEnabled, exemplarsOnAllMetricTypesEnabled); 114 } 115 116 public static Builder builder() { 117 return new Builder(); 118 } 119 120 public static OpenMetrics2TextFormatWriter create() { 121 return builder().build(); 122 } 123 124 @Override 125 public boolean accepts(@Nullable String acceptHeader) { 126 if (acceptHeader == null) { 127 return false; 128 } 129 return acceptHeader.contains("application/openmetrics-text"); 130 } 131 132 @Override 133 public String getContentType() { 134 // When contentNegotiation=false (default), masquerade as OM1 for compatibility. 135 // When contentNegotiation=true, use proper OM2 version. 136 if (openMetrics2Properties.getContentNegotiation()) { 137 return CONTENT_TYPE; 138 } else { 139 return OpenMetricsTextFormatWriter.CONTENT_TYPE; 140 } 141 } 142 143 public OpenMetrics2Properties getOpenMetrics2Properties() { 144 return openMetrics2Properties; 145 } 146 147 @Override 148 public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme scheme) 149 throws IOException { 150 Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); 151 MetricSnapshots merged = TextFormatUtil.mergeDuplicates(metricSnapshots); 152 for (MetricSnapshot s : merged) { 153 MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, scheme); 154 if (!snapshot.getDataPoints().isEmpty()) { 155 if (snapshot instanceof CounterSnapshot) { 156 writeCounter(writer, (CounterSnapshot) snapshot, scheme); 157 } else if (snapshot instanceof GaugeSnapshot) { 158 writeGauge(writer, (GaugeSnapshot) snapshot, scheme); 159 } else if (snapshot instanceof HistogramSnapshot) { 160 writeHistogram(writer, (HistogramSnapshot) snapshot, scheme); 161 } else if (snapshot instanceof SummarySnapshot) { 162 writeSummary(writer, (SummarySnapshot) snapshot, scheme); 163 } else if (snapshot instanceof InfoSnapshot) { 164 writeInfo(writer, (InfoSnapshot) snapshot, scheme); 165 } else if (snapshot instanceof StateSetSnapshot) { 166 writeStateSet(writer, (StateSetSnapshot) snapshot, scheme); 167 } else if (snapshot instanceof UnknownSnapshot) { 168 writeUnknown(writer, (UnknownSnapshot) snapshot, scheme); 169 } 170 } 171 } 172 writer.write("# EOF\n"); 173 writer.flush(); 174 } 175 176 private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme) 177 throws IOException { 178 MetricMetadata metadata = snapshot.getMetadata(); 179 String counterName = getMetricName(metadata, scheme); 180 if (openMetrics2Properties.getSuffixes()) { 181 counterName = ensureSuffix(counterName, "_total"); 182 } 183 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(counterName); 184 writeMetadataWithName(writer, counterName, "counter", metadata); 185 for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { 186 writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme, nameValidLegacy); 187 writeDouble(writer, data.getValue()); 188 if (data.hasScrapeTimestamp()) { 189 writer.write(' '); 190 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 191 } 192 if (data.hasCreatedTimestamp()) { 193 writer.write(" st@"); 194 writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); 195 } 196 writeExemplar(writer, data.getExemplar(), scheme); 197 writer.write('\n'); 198 } 199 } 200 201 private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme) 202 throws IOException { 203 MetricMetadata metadata = snapshot.getMetadata(); 204 String name = getMetricName(metadata, scheme); 205 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 206 writeMetadataWithName(writer, name, "gauge", metadata); 207 for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { 208 writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy); 209 writeDouble(writer, data.getValue()); 210 if (exemplarsOnAllMetricTypesEnabled) { 211 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 212 } else { 213 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 214 } 215 } 216 } 217 218 private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme) 219 throws IOException { 220 boolean compositeHistogram = 221 openMetrics2Properties.getCompositeValues() || openMetrics2Properties.getNativeHistograms(); 222 MetricMetadata metadata = snapshot.getMetadata(); 223 String name = getMetricName(metadata, scheme); 224 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 225 if (!compositeHistogram && !openMetrics2Properties.getExemplarCompliance()) { 226 writeClassicHistogram(writer, name, snapshot, scheme, nameValidLegacy); 227 return; 228 } 229 if (snapshot.isGaugeHistogram()) { 230 writeMetadataWithName(writer, name, "gaugehistogram", metadata); 231 for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { 232 if (openMetrics2Properties.getNativeHistograms() && data.hasNativeHistogramData()) { 233 writeNativeHistogramDataPoint( 234 writer, name, "gcount", "gsum", data, scheme, false, nameValidLegacy); 235 } else { 236 writeCompositeHistogramDataPoint( 237 writer, name, "gcount", "gsum", data, scheme, false, nameValidLegacy); 238 } 239 } 240 } else { 241 writeMetadataWithName(writer, name, "histogram", metadata); 242 for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { 243 if (openMetrics2Properties.getNativeHistograms() && data.hasNativeHistogramData()) { 244 writeNativeHistogramDataPoint( 245 writer, name, "count", "sum", data, scheme, true, nameValidLegacy); 246 } else { 247 writeCompositeHistogramDataPoint( 248 writer, name, "count", "sum", data, scheme, true, nameValidLegacy); 249 } 250 } 251 } 252 } 253 254 private void writeClassicHistogram( 255 Writer writer, 256 String name, 257 HistogramSnapshot snapshot, 258 EscapingScheme scheme, 259 boolean nameValidLegacy) 260 throws IOException { 261 if (snapshot.isGaugeHistogram()) { 262 writeMetadataWithName(writer, name, "gaugehistogram", snapshot.getMetadata()); 263 writeClassicHistogramDataPoints( 264 writer, name, "_gcount", "_gsum", snapshot, scheme, nameValidLegacy); 265 } else { 266 writeMetadataWithName(writer, name, "histogram", snapshot.getMetadata()); 267 writeClassicHistogramDataPoints( 268 writer, name, "_count", "_sum", snapshot, scheme, nameValidLegacy); 269 } 270 } 271 272 private void writeClassicHistogramDataPoints( 273 Writer writer, 274 String name, 275 String countSuffix, 276 String sumSuffix, 277 HistogramSnapshot snapshot, 278 EscapingScheme scheme, 279 boolean nameValidLegacy) 280 throws IOException { 281 String bucketName = name + "_bucket"; 282 for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { 283 ClassicHistogramBuckets buckets = getClassicBuckets(data); 284 Exemplars exemplars = data.getExemplars(); 285 long cumulativeCount = 0; 286 for (int i = 0; i < buckets.size(); i++) { 287 cumulativeCount += buckets.getCount(i); 288 writeNameAndLabels( 289 writer, 290 bucketName, 291 null, 292 data.getLabels(), 293 scheme, 294 "le", 295 buckets.getUpperBound(i), 296 nameValidLegacy); 297 writeLong(writer, cumulativeCount); 298 Exemplar exemplar; 299 if (i == 0) { 300 exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i)); 301 } else { 302 exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i)); 303 } 304 writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme); 305 } 306 if (data.hasCount() && data.hasSum()) { 307 writeClassicCountAndSum( 308 writer, name, data, countSuffix, sumSuffix, exemplars, scheme, nameValidLegacy); 309 } 310 writeClassicCreated(writer, name, data, scheme, nameValidLegacy); 311 } 312 } 313 314 private void writeClassicCountAndSum( 315 Writer writer, 316 String name, 317 HistogramSnapshot.HistogramDataPointSnapshot data, 318 String countSuffix, 319 String sumSuffix, 320 Exemplars exemplars, 321 EscapingScheme scheme, 322 boolean nameValidLegacy) 323 throws IOException { 324 writeNameAndLabels(writer, name, countSuffix, data.getLabels(), scheme, nameValidLegacy); 325 writeLong(writer, data.getCount()); 326 if (exemplarsOnAllMetricTypesEnabled) { 327 writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme); 328 } else { 329 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 330 } 331 writeNameAndLabels(writer, name, sumSuffix, data.getLabels(), scheme, nameValidLegacy); 332 writeDouble(writer, data.getSum()); 333 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 334 } 335 336 private void writeClassicCreated( 337 Writer writer, 338 String name, 339 HistogramSnapshot.HistogramDataPointSnapshot data, 340 EscapingScheme scheme, 341 boolean nameValidLegacy) 342 throws IOException { 343 if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { 344 writeNameAndLabels(writer, name, "_created", data.getLabels(), scheme, nameValidLegacy); 345 writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); 346 if (data.hasScrapeTimestamp()) { 347 writer.write(' '); 348 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 349 } 350 writer.write('\n'); 351 } 352 } 353 354 private void writeCompositeHistogramDataPoint( 355 Writer writer, 356 String name, 357 String countKey, 358 String sumKey, 359 HistogramSnapshot.HistogramDataPointSnapshot data, 360 EscapingScheme scheme, 361 boolean includeStartTimestamp, 362 boolean nameValidLegacy) 363 throws IOException { 364 writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy); 365 writer.write('{'); 366 writer.write(countKey); 367 writer.write(':'); 368 writeLong(writer, data.getCount()); 369 writer.write(','); 370 writer.write(sumKey); 371 writer.write(':'); 372 writeDouble(writer, data.getSum()); 373 writeClassicBucketsField(writer, data); 374 writer.write('}'); 375 if (data.hasScrapeTimestamp()) { 376 writer.write(' '); 377 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 378 } 379 if (includeStartTimestamp && data.hasCreatedTimestamp()) { 380 writer.write(" st@"); 381 writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); 382 } 383 writeExemplars(writer, data.getExemplars(), scheme); 384 writer.write('\n'); 385 } 386 387 private void writeNativeHistogramDataPoint( 388 Writer writer, 389 String name, 390 String countKey, 391 String sumKey, 392 HistogramSnapshot.HistogramDataPointSnapshot data, 393 EscapingScheme scheme, 394 boolean includeStartTimestamp, 395 boolean nameValidLegacy) 396 throws IOException { 397 writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy); 398 writer.write('{'); 399 writer.write(countKey); 400 writer.write(':'); 401 writeLong(writer, data.getCount()); 402 writer.write(','); 403 writer.write(sumKey); 404 writer.write(':'); 405 writeDouble(writer, data.getSum()); 406 writer.write(",schema:"); 407 writer.write(Integer.toString(data.getNativeSchema())); 408 writer.write(",zero_threshold:"); 409 writeDouble(writer, data.getNativeZeroThreshold()); 410 writer.write(",zero_count:"); 411 writeLong(writer, data.getNativeZeroCount()); 412 writeNativeBucketFields(writer, "negative", data.getNativeBucketsForNegativeValues()); 413 writeNativeBucketFields(writer, "positive", data.getNativeBucketsForPositiveValues()); 414 if (data.hasClassicHistogramData()) { 415 writeClassicBucketsField(writer, data); 416 } 417 writer.write('}'); 418 if (data.hasScrapeTimestamp()) { 419 writer.write(' '); 420 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 421 } 422 if (includeStartTimestamp && data.hasCreatedTimestamp()) { 423 writer.write(" st@"); 424 writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); 425 } 426 writeExemplars(writer, data.getExemplars(), scheme); 427 writer.write('\n'); 428 } 429 430 private ClassicHistogramBuckets getClassicBuckets( 431 HistogramSnapshot.HistogramDataPointSnapshot data) { 432 if (data.getClassicBuckets().isEmpty()) { 433 return ClassicHistogramBuckets.of( 434 new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()}); 435 } else { 436 return data.getClassicBuckets(); 437 } 438 } 439 440 private void writeClassicBucketsField( 441 Writer writer, HistogramSnapshot.HistogramDataPointSnapshot data) throws IOException { 442 writer.write(",bucket:["); 443 ClassicHistogramBuckets buckets = getClassicBuckets(data); 444 long cumulativeCount = 0; 445 for (int i = 0; i < buckets.size(); i++) { 446 if (i > 0) { 447 writer.write(','); 448 } 449 cumulativeCount += buckets.getCount(i); 450 writeDouble(writer, buckets.getUpperBound(i)); 451 writer.write(':'); 452 writeLong(writer, cumulativeCount); 453 } 454 writer.write(']'); 455 } 456 457 private void writeNativeBucketFields(Writer writer, String prefix, NativeHistogramBuckets buckets) 458 throws IOException { 459 if (buckets.size() == 0) { 460 return; 461 } 462 writer.write(','); 463 writer.write(prefix); 464 writer.write("_spans:["); 465 writeNativeBucketSpans(writer, buckets); 466 writer.write("],"); 467 writer.write(prefix); 468 writer.write("_buckets:["); 469 for (int i = 0; i < buckets.size(); i++) { 470 if (i > 0) { 471 writer.write(','); 472 } 473 writeLong(writer, buckets.getCount(i)); 474 } 475 writer.write(']'); 476 } 477 478 private void writeNativeBucketSpans(Writer writer, NativeHistogramBuckets buckets) 479 throws IOException { 480 int spanOffset = buckets.getBucketIndex(0); 481 int spanLength = 1; 482 int previousIndex = buckets.getBucketIndex(0); 483 boolean firstSpan = true; 484 for (int i = 1; i < buckets.size(); i++) { 485 int bucketIndex = buckets.getBucketIndex(i); 486 if (bucketIndex == previousIndex + 1) { 487 spanLength++; 488 } else { 489 firstSpan = writeNativeBucketSpan(writer, spanOffset, spanLength, firstSpan); 490 spanOffset = bucketIndex - previousIndex - 1; 491 spanLength = 1; 492 } 493 previousIndex = bucketIndex; 494 } 495 writeNativeBucketSpan(writer, spanOffset, spanLength, firstSpan); 496 } 497 498 private boolean writeNativeBucketSpan(Writer writer, int offset, int length, boolean firstSpan) 499 throws IOException { 500 if (!firstSpan) { 501 writer.write(','); 502 } 503 writer.write(Integer.toString(offset)); 504 writer.write(':'); 505 writer.write(Integer.toString(length)); 506 return false; 507 } 508 509 private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme) 510 throws IOException { 511 MetricMetadata metadata = snapshot.getMetadata(); 512 String name = getMetricName(metadata, scheme); 513 if (!openMetrics2Properties.getCompositeValues() 514 && !openMetrics2Properties.getExemplarCompliance()) { 515 om1Writer.writeSummary(writer, snapshot, scheme, name); 516 return; 517 } 518 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 519 boolean metadataWritten = false; 520 for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { 521 if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { 522 continue; 523 } 524 if (!metadataWritten) { 525 writeMetadataWithName(writer, name, "summary", metadata); 526 metadataWritten = true; 527 } 528 writeCompositeSummaryDataPoint(writer, name, data, scheme, nameValidLegacy); 529 } 530 } 531 532 private void writeCompositeSummaryDataPoint( 533 Writer writer, 534 String name, 535 SummarySnapshot.SummaryDataPointSnapshot data, 536 EscapingScheme scheme, 537 boolean nameValidLegacy) 538 throws IOException { 539 writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy); 540 writer.write('{'); 541 boolean first = true; 542 if (data.hasCount()) { 543 writer.write("count:"); 544 writeLong(writer, data.getCount()); 545 first = false; 546 } 547 if (data.hasSum()) { 548 if (!first) { 549 writer.write(','); 550 } 551 writer.write("sum:"); 552 writeDouble(writer, data.getSum()); 553 first = false; 554 } 555 if (!first) { 556 writer.write(','); 557 } 558 writer.write("quantile:["); 559 for (int i = 0; i < data.getQuantiles().size(); i++) { 560 if (i > 0) { 561 writer.write(','); 562 } 563 Quantile q = data.getQuantiles().get(i); 564 writeDouble(writer, q.getQuantile()); 565 writer.write(':'); 566 writeDouble(writer, q.getValue()); 567 } 568 writer.write(']'); 569 writer.write('}'); 570 if (data.hasScrapeTimestamp()) { 571 writer.write(' '); 572 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 573 } 574 if (data.hasCreatedTimestamp()) { 575 writer.write(" st@"); 576 writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); 577 } 578 writeExemplars(writer, data.getExemplars(), scheme); 579 writer.write('\n'); 580 } 581 582 private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) 583 throws IOException { 584 MetricMetadata metadata = snapshot.getMetadata(); 585 // OM2 spec: Info MetricFamily name MUST end in _info. 586 // In OM2, TYPE/HELP use the same name as the data lines. 587 String infoName = ensureSuffix(getMetricName(metadata, scheme), "_info"); 588 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(infoName); 589 writeMetadataWithName(writer, infoName, "info", metadata); 590 for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { 591 writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme, nameValidLegacy); 592 writer.write("1"); 593 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 594 } 595 } 596 597 private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme) 598 throws IOException { 599 MetricMetadata metadata = snapshot.getMetadata(); 600 String name = getMetricName(metadata, scheme); 601 writeMetadataWithName(writer, name, "stateset", metadata); 602 for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { 603 for (int i = 0; i < data.size(); i++) { 604 writer.write(name); 605 writer.write('{'); 606 Labels labels = data.getLabels(); 607 for (int j = 0; j < labels.size(); j++) { 608 if (j > 0) { 609 writer.write(","); 610 } 611 writer.write(getSnapshotLabelName(labels, j, scheme)); 612 writer.write("=\""); 613 writeEscapedString(writer, labels.getValue(j)); 614 writer.write("\""); 615 } 616 if (!labels.isEmpty()) { 617 writer.write(","); 618 } 619 writer.write(name); 620 writer.write("=\""); 621 writeEscapedString(writer, data.getName(i)); 622 writer.write("\"} "); 623 if (data.isTrue(i)) { 624 writer.write("1"); 625 } else { 626 writer.write("0"); 627 } 628 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 629 } 630 } 631 } 632 633 private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme) 634 throws IOException { 635 MetricMetadata metadata = snapshot.getMetadata(); 636 String name = getMetricName(metadata, scheme); 637 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 638 writeMetadataWithName(writer, name, "unknown", metadata); 639 for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { 640 writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy); 641 writeDouble(writer, data.getValue()); 642 if (exemplarsOnAllMetricTypesEnabled) { 643 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 644 } else { 645 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 646 } 647 } 648 } 649 650 private void writeNameAndLabels( 651 Writer writer, 652 String name, 653 @Nullable String suffix, 654 Labels labels, 655 EscapingScheme escapingScheme, 656 boolean nameValidLegacy) 657 throws IOException { 658 writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0, nameValidLegacy); 659 } 660 661 private void writeNameAndLabels( 662 Writer writer, 663 String name, 664 @Nullable String suffix, 665 Labels labels, 666 EscapingScheme escapingScheme, 667 @Nullable String additionalLabelName, 668 double additionalLabelValue, 669 boolean nameValidLegacy) 670 throws IOException { 671 boolean metricInsideBraces = false; 672 // If the name does not pass the legacy validity check, we must put the metric name inside the 673 // braces. Validity is scanned once per family by the caller (the metric name is constant across 674 // all data points), not re-scanned for every series. A suffix built from legacy characters 675 // (_total, _bucket, ...) does not change legacy validity, so the base-name check applies here. 676 if (!nameValidLegacy) { 677 metricInsideBraces = true; 678 writer.write('{'); 679 } 680 writeName(writer, suffix != null ? name + suffix : name, NameType.Metric, nameValidLegacy); 681 if (!labels.isEmpty() || additionalLabelName != null) { 682 writeLabels( 683 writer, 684 labels, 685 additionalLabelName, 686 additionalLabelValue, 687 metricInsideBraces, 688 escapingScheme); 689 } else if (metricInsideBraces) { 690 writer.write('}'); 691 } 692 writer.write(' '); 693 } 694 695 private void writeScrapeTimestampAndExemplar( 696 Writer writer, DataPointSnapshot data, @Nullable Exemplar exemplar, EscapingScheme scheme) 697 throws IOException { 698 if (!openMetrics2Properties.getExemplarCompliance()) { 699 om1Writer.writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme); 700 return; 701 } 702 if (data.hasScrapeTimestamp()) { 703 writer.write(' '); 704 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 705 } 706 writeExemplar(writer, exemplar, scheme); 707 writer.write('\n'); 708 } 709 710 private void writeExemplar(Writer writer, @Nullable Exemplar exemplar, EscapingScheme scheme) 711 throws IOException { 712 if (exemplar == null) { 713 return; 714 } 715 if (!openMetrics2Properties.getExemplarCompliance()) { 716 om1Writer.writeExemplar(writer, exemplar, scheme); 717 return; 718 } 719 // exemplarCompliance=true: exemplars MUST have a timestamp per the OM2 spec. 720 if (exemplar.hasTimestamp()) { 721 om1Writer.writeExemplar(writer, exemplar, scheme); 722 } 723 } 724 725 private void writeExemplars(Writer writer, Exemplars exemplars, EscapingScheme scheme) 726 throws IOException { 727 for (Exemplar exemplar : exemplars) { 728 writeExemplar(writer, exemplar, scheme); 729 } 730 } 731 732 private void writeMetadataWithName( 733 Writer writer, String name, String typeName, MetricMetadata metadata) throws IOException { 734 writer.write("# TYPE "); 735 writeName(writer, name, NameType.Metric); 736 writer.write(' '); 737 writer.write(typeName); 738 writer.write('\n'); 739 if (metadata.getUnit() != null) { 740 writer.write("# UNIT "); 741 writeName(writer, name, NameType.Metric); 742 writer.write(' '); 743 writeEscapedString(writer, metadata.getUnit().toString()); 744 writer.write('\n'); 745 } 746 if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { 747 writer.write("# HELP "); 748 writeName(writer, name, NameType.Metric); 749 writer.write(' '); 750 writeEscapedString(writer, metadata.getHelp()); 751 writer.write('\n'); 752 } 753 } 754 755 private String getMetricName(MetricMetadata metadata, EscapingScheme scheme) { 756 if (openMetrics2Properties.getSuffixes()) { 757 return getExpositionBaseMetadataName(metadata, scheme); 758 } 759 return getOriginalMetadataName(metadata, scheme); 760 } 761 762 private static String ensureSuffix(String name, String suffix) { 763 if (name.endsWith(suffix)) { 764 return name; 765 } 766 return name + suffix; 767 } 768}