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", snapshot.getDataPoints(), scheme); 183 } else { 184 writeMetadata(writer, "histogram", metadata, scheme); 185 writeClassicHistogramBuckets( 186 writer, metadata, "_count", "_sum", snapshot.getDataPoints(), scheme); 187 } 188 } 189 190 private void writeClassicHistogramBuckets( 191 Writer writer, 192 MetricMetadata metadata, 193 String countSuffix, 194 String sumSuffix, 195 List<HistogramSnapshot.HistogramDataPointSnapshot> dataList, 196 EscapingScheme scheme) 197 throws IOException { 198 String name = getMetadataName(metadata, scheme); 199 String bucketName = name + "_bucket"; 200 String countName = name + countSuffix; 201 String sumName = name + sumSuffix; 202 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 203 for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) { 204 ClassicHistogramBuckets buckets = getClassicBuckets(data); 205 Exemplars exemplars = data.getExemplars(); 206 long cumulativeCount = 0; 207 for (int i = 0; i < buckets.size(); i++) { 208 cumulativeCount += buckets.getCount(i); 209 writeNameAndLabels( 210 writer, 211 bucketName, 212 null, 213 data.getLabels(), 214 scheme, 215 "le", 216 buckets.getUpperBound(i), 217 nameValidLegacy); 218 writeLong(writer, cumulativeCount); 219 Exemplar exemplar; 220 if (i == 0) { 221 exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i)); 222 } else { 223 exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i)); 224 } 225 writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme); 226 } 227 // In OpenMetrics format, histogram _count and _sum are either both present or both absent. 228 if (data.hasCount() && data.hasSum()) { 229 writeCountAndSum(writer, countName, sumName, data, exemplars, scheme, nameValidLegacy); 230 } 231 writeCreated(writer, name, data, scheme, nameValidLegacy); 232 } 233 } 234 235 private ClassicHistogramBuckets getClassicBuckets( 236 HistogramSnapshot.HistogramDataPointSnapshot data) { 237 if (data.getClassicBuckets().isEmpty()) { 238 return ClassicHistogramBuckets.of( 239 new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()}); 240 } else { 241 return data.getClassicBuckets(); 242 } 243 } 244 245 void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme) 246 throws IOException { 247 writeSummary(writer, snapshot, scheme, getMetadataName(snapshot.getMetadata(), scheme)); 248 } 249 250 void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme, String name) 251 throws IOException { 252 boolean metadataWritten = false; 253 MetricMetadata metadata = snapshot.getMetadata(); 254 String countName = name + "_count"; 255 String sumName = name + "_sum"; 256 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 257 for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { 258 if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { 259 continue; 260 } 261 if (!metadataWritten) { 262 writeMetadataWithName(writer, name, "summary", metadata); 263 metadataWritten = true; 264 } 265 Exemplars exemplars = data.getExemplars(); 266 // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose 267 // for which 268 // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...] 269 // for the 270 // quantiles, all indexes modulo exemplars.length. 271 int exemplarIndex = 1; 272 for (Quantile quantile : data.getQuantiles()) { 273 writeNameAndLabels( 274 writer, 275 name, 276 null, 277 data.getLabels(), 278 scheme, 279 "quantile", 280 quantile.getQuantile(), 281 nameValidLegacy); 282 writeDouble(writer, quantile.getValue()); 283 if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) { 284 exemplarIndex = (exemplarIndex + 1) % exemplars.size(); 285 writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex), scheme); 286 } else { 287 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 288 } 289 } 290 // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics. 291 writeCountAndSum(writer, countName, sumName, data, exemplars, scheme, nameValidLegacy); 292 writeCreated(writer, name, data, scheme, nameValidLegacy); 293 } 294 } 295 296 private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) 297 throws IOException { 298 MetricMetadata metadata = snapshot.getMetadata(); 299 String infoName = resolveExpositionName(metadata, "_info", scheme); 300 String baseName = resolveBaseName(infoName, "_info"); 301 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(infoName); 302 writeMetadataWithName(writer, baseName, "info", metadata); 303 for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { 304 writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme, nameValidLegacy); 305 writer.write("1"); 306 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 307 } 308 } 309 310 private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme) 311 throws IOException { 312 MetricMetadata metadata = snapshot.getMetadata(); 313 writeMetadata(writer, "stateset", metadata, scheme); 314 String name = getMetadataName(metadata, scheme); 315 for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { 316 for (int i = 0; i < data.size(); i++) { 317 writer.write(name); 318 writer.write('{'); 319 Labels labels = data.getLabels(); 320 for (int j = 0; j < labels.size(); j++) { 321 if (j > 0) { 322 writer.write(","); 323 } 324 writer.write(getSnapshotLabelName(labels, j, scheme)); 325 writer.write("=\""); 326 writeEscapedString(writer, labels.getValue(j)); 327 writer.write("\""); 328 } 329 if (!labels.isEmpty()) { 330 writer.write(","); 331 } 332 writer.write(name); 333 writer.write("=\""); 334 writeEscapedString(writer, data.getName(i)); 335 writer.write("\"} "); 336 if (data.isTrue(i)) { 337 writer.write("1"); 338 } else { 339 writer.write("0"); 340 } 341 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 342 } 343 } 344 } 345 346 private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme) 347 throws IOException { 348 MetricMetadata metadata = snapshot.getMetadata(); 349 writeMetadata(writer, "unknown", metadata, scheme); 350 String name = getMetadataName(metadata, scheme); 351 boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name); 352 for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { 353 writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy); 354 writeDouble(writer, data.getValue()); 355 if (exemplarsOnAllMetricTypesEnabled) { 356 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 357 } else { 358 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 359 } 360 } 361 } 362 363 private void writeCountAndSum( 364 Writer writer, 365 String countName, 366 String sumName, 367 DistributionDataPointSnapshot data, 368 Exemplars exemplars, 369 EscapingScheme scheme, 370 boolean nameValidLegacy) 371 throws IOException { 372 if (data.hasCount()) { 373 writeNameAndLabels(writer, countName, null, data.getLabels(), scheme, nameValidLegacy); 374 writeLong(writer, data.getCount()); 375 if (exemplarsOnAllMetricTypesEnabled) { 376 writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme); 377 } else { 378 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 379 } 380 } 381 if (data.hasSum()) { 382 writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme, nameValidLegacy); 383 writeDouble(writer, data.getSum()); 384 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 385 } 386 } 387 388 private void writeCreated( 389 Writer writer, 390 String baseName, 391 DataPointSnapshot data, 392 EscapingScheme scheme, 393 boolean nameValidLegacy) 394 throws IOException { 395 if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { 396 writeNameAndLabels(writer, baseName, "_created", data.getLabels(), scheme, nameValidLegacy); 397 writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); 398 if (data.hasScrapeTimestamp()) { 399 writer.write(' '); 400 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 401 } 402 writer.write('\n'); 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 escapingScheme, 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 metric name inside the 429 // braces. Validity is scanned once per family by the caller (the metric name is constant across 430 // all data points), not re-scanned for every series. A suffix built from legacy characters 431 // (_total, _bucket, ...) does not change legacy validity, so the base-name check applies here. 432 if (!nameValidLegacy) { 433 metricInsideBraces = true; 434 writer.write('{'); 435 } 436 writeName(writer, suffix != null ? name + suffix : name, NameType.Metric, nameValidLegacy); 437 if (!labels.isEmpty() || additionalLabelName != null) { 438 writeLabels( 439 writer, 440 labels, 441 additionalLabelName, 442 additionalLabelValue, 443 metricInsideBraces, 444 escapingScheme); 445 } else if (metricInsideBraces) { 446 writer.write('}'); 447 } 448 writer.write(' '); 449 } 450 451 void writeScrapeTimestampAndExemplar( 452 Writer writer, DataPointSnapshot data, @Nullable Exemplar exemplar, EscapingScheme scheme) 453 throws IOException { 454 if (data.hasScrapeTimestamp()) { 455 writer.write(' '); 456 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 457 } 458 if (exemplar != null) { 459 writeExemplar(writer, exemplar, scheme); 460 } 461 writer.write('\n'); 462 } 463 464 void writeExemplar(Writer writer, Exemplar exemplar, EscapingScheme scheme) throws IOException { 465 writer.write(" # "); 466 writeLabels(writer, exemplar.getLabels(), null, 0, false, scheme); 467 writer.write(' '); 468 writeDouble(writer, exemplar.getValue()); 469 if (exemplar.hasTimestamp()) { 470 writer.write(' '); 471 writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis()); 472 } 473 } 474 475 /** 476 * Returns the full exposition name for a metric. If the original name already ends with the given 477 * suffix (e.g. "_total" for counters), uses the original name directly. Otherwise, appends the 478 * suffix to the base name. 479 */ 480 private static String resolveExpositionName( 481 MetricMetadata metadata, String suffix, EscapingScheme scheme) { 482 String expositionBaseName = getExpositionBaseMetadataName(metadata, scheme); 483 if (expositionBaseName.endsWith(suffix)) { 484 return expositionBaseName; 485 } 486 return getMetadataName(metadata, scheme) + suffix; 487 } 488 489 private void writeMetadata( 490 Writer writer, String typeName, MetricMetadata metadata, EscapingScheme scheme) 491 throws IOException { 492 writeMetadataWithName(writer, getMetadataName(metadata, scheme), typeName, metadata); 493 } 494 495 private void writeMetadataWithName( 496 Writer writer, String name, String typeName, MetricMetadata metadata) throws IOException { 497 writer.write("# TYPE "); 498 writeName(writer, name, NameType.Metric); 499 writer.write(' '); 500 writer.write(typeName); 501 writer.write('\n'); 502 if (metadata.getUnit() != null) { 503 writer.write("# UNIT "); 504 writeName(writer, name, NameType.Metric); 505 writer.write(' '); 506 writeEscapedString(writer, metadata.getUnit().toString()); 507 writer.write('\n'); 508 } 509 if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { 510 writer.write("# HELP "); 511 writeName(writer, name, NameType.Metric); 512 writer.write(' '); 513 writeEscapedString(writer, metadata.getHelp()); 514 writer.write('\n'); 515 } 516 } 517 518 private static String resolveBaseName(String fullName, String suffix) { 519 if (fullName.endsWith(suffix)) { 520 return fullName.substring(0, fullName.length() - suffix.length()); 521 } 522 return fullName; 523 } 524}