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.getMetadataName; 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.model.snapshots.ClassicHistogramBuckets; 016import io.prometheus.metrics.model.snapshots.CounterSnapshot; 017import io.prometheus.metrics.model.snapshots.DataPointSnapshot; 018import io.prometheus.metrics.model.snapshots.DistributionDataPointSnapshot; 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.PrometheusNaming; 029import io.prometheus.metrics.model.snapshots.Quantile; 030import io.prometheus.metrics.model.snapshots.SnapshotEscaper; 031import io.prometheus.metrics.model.snapshots.StateSetSnapshot; 032import io.prometheus.metrics.model.snapshots.SummarySnapshot; 033import io.prometheus.metrics.model.snapshots.UnknownSnapshot; 034import java.io.BufferedWriter; 035import java.io.IOException; 036import java.io.OutputStream; 037import java.io.OutputStreamWriter; 038import java.io.Writer; 039import java.nio.charset.StandardCharsets; 040import java.util.List; 041import javax.annotation.Nullable; 042 043/** 044 * Write the OpenMetrics text format as defined on <a 045 * href="https://openmetrics.io/">https://openmetrics.io</a>. 046 */ 047@StableApi 048public class OpenMetricsTextFormatWriter implements ExpositionFormatWriter { 049 050 public static class Builder { 051 boolean createdTimestampsEnabled; 052 boolean exemplarsOnAllMetricTypesEnabled; 053 054 private Builder() {} 055 056 /** 057 * @param createdTimestampsEnabled whether to include the _created timestamp in the output 058 */ 059 public Builder setCreatedTimestampsEnabled(boolean createdTimestampsEnabled) { 060 this.createdTimestampsEnabled = createdTimestampsEnabled; 061 return this; 062 } 063 064 /** 065 * @param exemplarsOnAllMetricTypesEnabled whether to include exemplars in the output for all 066 * metric types 067 */ 068 public Builder setExemplarsOnAllMetricTypesEnabled(boolean exemplarsOnAllMetricTypesEnabled) { 069 this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled; 070 return this; 071 } 072 073 public OpenMetricsTextFormatWriter build() { 074 return new OpenMetricsTextFormatWriter( 075 createdTimestampsEnabled, exemplarsOnAllMetricTypesEnabled); 076 } 077 } 078 079 public static final String CONTENT_TYPE = 080 "application/openmetrics-text; version=1.0.0; charset=utf-8"; 081 private final boolean createdTimestampsEnabled; 082 private final boolean exemplarsOnAllMetricTypesEnabled; 083 084 /** 085 * @param createdTimestampsEnabled whether to include the _created timestamp in the output - This 086 * will produce an invalid OpenMetrics output, but is kept for backwards compatibility. 087 */ 088 public OpenMetricsTextFormatWriter( 089 boolean createdTimestampsEnabled, boolean exemplarsOnAllMetricTypesEnabled) { 090 this.createdTimestampsEnabled = createdTimestampsEnabled; 091 this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled; 092 } 093 094 public static Builder builder() { 095 return new Builder(); 096 } 097 098 public static OpenMetricsTextFormatWriter create() { 099 return builder().build(); 100 } 101 102 @Override 103 public boolean accepts(@Nullable String acceptHeader) { 104 if (acceptHeader == null) { 105 return false; 106 } 107 return acceptHeader.contains("application/openmetrics-text"); 108 } 109 110 @Override 111 public String getContentType() { 112 return CONTENT_TYPE; 113 } 114 115 @Override 116 public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme scheme) 117 throws IOException { 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 = SnapshotEscaper.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 writer.write("# EOF\n"); 141 writer.flush(); 142 } 143 144 private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme) 145 throws IOException { 146 MetricMetadata metadata = snapshot.getMetadata(); 147 String counterName = resolveExpositionName(metadata, "_total", scheme); 148 String baseName = resolveBaseName(counterName, "_total"); 149 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(counterName); 150 writeMetadataWithName(writer, baseName, "counter", metadata); 151 for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { 152 writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme, nameValidLegacy); 153 writeDouble(writer, data.getValue()); 154 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 155 writeCreated(writer, baseName, data, scheme, nameValidLegacy); 156 } 157 } 158 159 private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme) 160 throws IOException { 161 MetricMetadata metadata = snapshot.getMetadata(); 162 writeMetadata(writer, "gauge", metadata, scheme); 163 String name = getMetadataName(metadata, scheme); 164 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 165 for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { 166 writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy); 167 writeDouble(writer, data.getValue()); 168 if (exemplarsOnAllMetricTypesEnabled) { 169 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 170 } else { 171 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 172 } 173 } 174 } 175 176 void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme) 177 throws IOException { 178 MetricMetadata metadata = snapshot.getMetadata(); 179 if (snapshot.isGaugeHistogram()) { 180 writeMetadata(writer, "gaugehistogram", metadata, scheme); 181 writeClassicHistogramBuckets( 182 writer, metadata, "_gcount", "_gsum", true, snapshot.getDataPoints(), scheme); 183 } else { 184 writeMetadata(writer, "histogram", metadata, scheme); 185 writeClassicHistogramBuckets( 186 writer, metadata, "_count", "_sum", false, snapshot.getDataPoints(), scheme); 187 } 188 } 189 190 private void writeClassicHistogramBuckets( 191 Writer writer, 192 MetricMetadata metadata, 193 String countSuffix, 194 String sumSuffix, 195 boolean gaugeHistogram, 196 List<HistogramSnapshot.HistogramDataPointSnapshot> dataList, 197 EscapingScheme scheme) 198 throws IOException { 199 String name = getMetadataName(metadata, scheme); 200 String bucketName = name + "_bucket"; 201 String countName = name + countSuffix; 202 String sumName = name + sumSuffix; 203 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 204 for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) { 205 ClassicHistogramBuckets buckets = getClassicBuckets(data); 206 Exemplars exemplars = data.getExemplars(); 207 long cumulativeCount = 0; 208 for (int i = 0; i < buckets.size(); i++) { 209 cumulativeCount += buckets.getCount(i); 210 writeNameAndLabels( 211 writer, 212 bucketName, 213 null, 214 data.getLabels(), 215 scheme, 216 "le", 217 buckets.getUpperBound(i), 218 nameValidLegacy); 219 writeLong(writer, cumulativeCount); 220 Exemplar exemplar; 221 if (i == 0) { 222 exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i)); 223 } else { 224 exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i)); 225 } 226 writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme); 227 } 228 // In OpenMetrics format, histogram _count and _sum are either both present or both absent. 229 // Negative threshold buckets preclude a sum for histograms, but not gauge histograms. 230 boolean hasNegativeBuckets = buckets.getUpperBound(0) < 0; 231 if (data.hasCount() && data.hasSum() && (gaugeHistogram || !hasNegativeBuckets)) { 232 writeCountAndSum(writer, countName, sumName, data, exemplars, scheme, nameValidLegacy); 233 } 234 writeCreated(writer, name, data, scheme, nameValidLegacy); 235 } 236 } 237 238 private ClassicHistogramBuckets getClassicBuckets( 239 HistogramSnapshot.HistogramDataPointSnapshot data) { 240 if (data.getClassicBuckets().isEmpty()) { 241 return ClassicHistogramBuckets.of( 242 new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()}); 243 } else { 244 return data.getClassicBuckets(); 245 } 246 } 247 248 void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme) 249 throws IOException { 250 writeSummary(writer, snapshot, scheme, getMetadataName(snapshot.getMetadata(), scheme)); 251 } 252 253 void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme, String name) 254 throws IOException { 255 boolean metadataWritten = false; 256 MetricMetadata metadata = snapshot.getMetadata(); 257 String countName = name + "_count"; 258 String sumName = name + "_sum"; 259 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 260 for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { 261 if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { 262 continue; 263 } 264 if (!metadataWritten) { 265 writeMetadataWithName(writer, name, "summary", metadata); 266 metadataWritten = true; 267 } 268 Exemplars exemplars = data.getExemplars(); 269 // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose 270 // for which 271 // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...] 272 // for the 273 // quantiles, all indexes modulo exemplars.length. 274 int exemplarIndex = 1; 275 for (Quantile quantile : data.getQuantiles()) { 276 writeNameAndLabels( 277 writer, 278 name, 279 null, 280 data.getLabels(), 281 scheme, 282 "quantile", 283 quantile.getQuantile(), 284 nameValidLegacy); 285 writeDouble(writer, quantile.getValue()); 286 if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) { 287 exemplarIndex = (exemplarIndex + 1) % exemplars.size(); 288 writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex), scheme); 289 } else { 290 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 291 } 292 } 293 // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics. 294 writeCountAndSum(writer, countName, sumName, data, exemplars, scheme, nameValidLegacy); 295 writeCreated(writer, name, data, scheme, nameValidLegacy); 296 } 297 } 298 299 private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) 300 throws IOException { 301 MetricMetadata metadata = snapshot.getMetadata(); 302 String infoName = resolveExpositionName(metadata, "_info", scheme); 303 String baseName = resolveBaseName(infoName, "_info"); 304 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(infoName); 305 writeMetadataWithName(writer, baseName, "info", metadata); 306 for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { 307 writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme, nameValidLegacy); 308 writer.write("1"); 309 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 310 } 311 } 312 313 private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme) 314 throws IOException { 315 MetricMetadata metadata = snapshot.getMetadata(); 316 writeMetadata(writer, "stateset", metadata, scheme); 317 String name = getMetadataName(metadata, scheme); 318 for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { 319 for (int i = 0; i < data.size(); i++) { 320 writer.write(name); 321 writer.write('{'); 322 Labels labels = data.getLabels(); 323 for (int j = 0; j < labels.size(); j++) { 324 if (j > 0) { 325 writer.write(","); 326 } 327 writer.write(getSnapshotLabelName(labels, j, scheme)); 328 writer.write("=\""); 329 writeEscapedString(writer, labels.getValue(j)); 330 writer.write("\""); 331 } 332 if (!labels.isEmpty()) { 333 writer.write(","); 334 } 335 writer.write(name); 336 writer.write("=\""); 337 writeEscapedString(writer, data.getName(i)); 338 writer.write("\"} "); 339 if (data.isTrue(i)) { 340 writer.write("1"); 341 } else { 342 writer.write("0"); 343 } 344 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 345 } 346 } 347 } 348 349 private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme) 350 throws IOException { 351 MetricMetadata metadata = snapshot.getMetadata(); 352 writeMetadata(writer, "unknown", metadata, scheme); 353 String name = getMetadataName(metadata, scheme); 354 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 355 for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { 356 writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy); 357 writeDouble(writer, data.getValue()); 358 if (exemplarsOnAllMetricTypesEnabled) { 359 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 360 } else { 361 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 362 } 363 } 364 } 365 366 private void writeCountAndSum( 367 Writer writer, 368 String countName, 369 String sumName, 370 DistributionDataPointSnapshot data, 371 Exemplars exemplars, 372 EscapingScheme scheme, 373 boolean nameValidLegacy) 374 throws IOException { 375 if (data.hasCount()) { 376 writeNameAndLabels(writer, countName, null, data.getLabels(), scheme, nameValidLegacy); 377 writeLong(writer, data.getCount()); 378 if (exemplarsOnAllMetricTypesEnabled) { 379 writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme); 380 } else { 381 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 382 } 383 } 384 if (data.hasSum()) { 385 writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme, nameValidLegacy); 386 writeDouble(writer, data.getSum()); 387 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 388 } 389 } 390 391 private void writeCreated( 392 Writer writer, 393 String baseName, 394 DataPointSnapshot data, 395 EscapingScheme scheme, 396 boolean nameValidLegacy) 397 throws IOException { 398 if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { 399 writeNameAndLabels(writer, baseName, "_created", data.getLabels(), scheme, nameValidLegacy); 400 writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); 401 if (data.hasScrapeTimestamp()) { 402 writer.write(' '); 403 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 404 } 405 writer.write('\n'); 406 } 407 } 408 409 private void writeNameAndLabels( 410 Writer writer, 411 String name, 412 @Nullable String suffix, 413 Labels labels, 414 EscapingScheme escapingScheme, 415 boolean nameValidLegacy) 416 throws IOException { 417 writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0, nameValidLegacy); 418 } 419 420 private void writeNameAndLabels( 421 Writer writer, 422 String name, 423 @Nullable String suffix, 424 Labels labels, 425 EscapingScheme escapingScheme, 426 @Nullable String additionalLabelName, 427 double additionalLabelValue, 428 boolean nameValidLegacy) 429 throws IOException { 430 boolean metricInsideBraces = false; 431 // If the name does not pass the legacy validity check, we must put the metric name inside the 432 // braces. Validity is scanned once per family by the caller (the metric name is constant across 433 // all data points), not re-scanned for every series. A suffix built from legacy characters 434 // (_total, _bucket, ...) does not change legacy validity, so the base-name check applies here. 435 if (!nameValidLegacy) { 436 metricInsideBraces = true; 437 writer.write('{'); 438 } 439 writeName(writer, suffix != null ? name + suffix : name, NameType.Metric, nameValidLegacy); 440 if (!labels.isEmpty() || additionalLabelName != null) { 441 writeLabels( 442 writer, 443 labels, 444 additionalLabelName, 445 additionalLabelValue, 446 metricInsideBraces, 447 escapingScheme); 448 } else if (metricInsideBraces) { 449 writer.write('}'); 450 } 451 writer.write(' '); 452 } 453 454 void writeScrapeTimestampAndExemplar( 455 Writer writer, DataPointSnapshot data, @Nullable Exemplar exemplar, EscapingScheme scheme) 456 throws IOException { 457 if (data.hasScrapeTimestamp()) { 458 writer.write(' '); 459 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 460 } 461 if (exemplar != null) { 462 writeExemplar(writer, exemplar, scheme); 463 } 464 writer.write('\n'); 465 } 466 467 void writeExemplar(Writer writer, Exemplar exemplar, EscapingScheme scheme) throws IOException { 468 writer.write(" # "); 469 writeLabels(writer, exemplar.getLabels(), null, 0, false, scheme); 470 writer.write(' '); 471 writeDouble(writer, exemplar.getValue()); 472 if (exemplar.hasTimestamp()) { 473 writer.write(' '); 474 writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis()); 475 } 476 } 477 478 /** 479 * Returns the full exposition name for a metric. If the original name already ends with the given 480 * suffix (e.g. "_total" for counters), uses the original name directly. Otherwise, appends the 481 * suffix to the base name. 482 */ 483 private static String resolveExpositionName( 484 MetricMetadata metadata, String suffix, EscapingScheme scheme) { 485 String expositionBaseName = getExpositionBaseMetadataName(metadata, scheme); 486 if (expositionBaseName.endsWith(suffix)) { 487 return expositionBaseName; 488 } 489 return getMetadataName(metadata, scheme) + suffix; 490 } 491 492 private void writeMetadata( 493 Writer writer, String typeName, MetricMetadata metadata, EscapingScheme scheme) 494 throws IOException { 495 writeMetadataWithName(writer, getMetadataName(metadata, scheme), typeName, metadata); 496 } 497 498 private void writeMetadataWithName( 499 Writer writer, String name, String typeName, MetricMetadata metadata) throws IOException { 500 writer.write("# TYPE "); 501 writeName(writer, name, NameType.Metric); 502 writer.write(' '); 503 writer.write(typeName); 504 writer.write('\n'); 505 if (metadata.getUnit() != null) { 506 writer.write("# UNIT "); 507 writeName(writer, name, NameType.Metric); 508 writer.write(' '); 509 writeEscapedString(writer, metadata.getUnit().toString()); 510 writer.write('\n'); 511 } 512 if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { 513 writer.write("# HELP "); 514 writeName(writer, name, NameType.Metric); 515 writer.write(' '); 516 writeEscapedString(writer, metadata.getHelp()); 517 writer.write('\n'); 518 } 519 } 520 521 private static String resolveBaseName(String fullName, String suffix) { 522 if (fullName.endsWith(suffix)) { 523 return fullName.substring(0, fullName.length() - suffix.length()); 524 } 525 return fullName; 526 } 527}