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.getSnapshotLabelName; 011 012import io.prometheus.metrics.config.EscapingScheme; 013import io.prometheus.metrics.config.OpenMetrics2Properties; 014import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets; 015import io.prometheus.metrics.model.snapshots.CounterSnapshot; 016import io.prometheus.metrics.model.snapshots.DataPointSnapshot; 017import io.prometheus.metrics.model.snapshots.DistributionDataPointSnapshot; 018import io.prometheus.metrics.model.snapshots.Exemplar; 019import io.prometheus.metrics.model.snapshots.Exemplars; 020import io.prometheus.metrics.model.snapshots.GaugeSnapshot; 021import io.prometheus.metrics.model.snapshots.HistogramSnapshot; 022import io.prometheus.metrics.model.snapshots.InfoSnapshot; 023import io.prometheus.metrics.model.snapshots.Labels; 024import io.prometheus.metrics.model.snapshots.MetricMetadata; 025import io.prometheus.metrics.model.snapshots.MetricSnapshot; 026import io.prometheus.metrics.model.snapshots.MetricSnapshots; 027import io.prometheus.metrics.model.snapshots.PrometheusNaming; 028import io.prometheus.metrics.model.snapshots.Quantile; 029import io.prometheus.metrics.model.snapshots.SnapshotEscaper; 030import io.prometheus.metrics.model.snapshots.StateSetSnapshot; 031import io.prometheus.metrics.model.snapshots.SummarySnapshot; 032import io.prometheus.metrics.model.snapshots.UnknownSnapshot; 033import java.io.BufferedWriter; 034import java.io.IOException; 035import java.io.OutputStream; 036import java.io.OutputStreamWriter; 037import java.io.Writer; 038import java.nio.charset.StandardCharsets; 039import java.util.List; 040import javax.annotation.Nullable; 041 042/** 043 * Write the OpenMetrics 2.0 text format. Unlike the OM1 writer, this writer outputs metric names as 044 * provided by the user — no {@code _total} or unit suffix appending. The {@code _info} suffix is 045 * enforced per the OM2 spec (MUST). This is experimental and subject to change as the <a 046 * href="https://github.com/prometheus/docs/blob/main/docs/specs/om/open_metrics_spec_2_0.md">OpenMetrics 047 * 2.0 specification</a> evolves. 048 */ 049public class OpenMetrics2TextFormatWriter implements ExpositionFormatWriter { 050 051 public static class Builder { 052 private OpenMetrics2Properties openMetrics2Properties = 053 OpenMetrics2Properties.builder().build(); 054 boolean createdTimestampsEnabled; 055 boolean exemplarsOnAllMetricTypesEnabled; 056 057 private Builder() {} 058 059 /** 060 * @param openMetrics2Properties OpenMetrics 2.0 feature flags 061 */ 062 public Builder setOpenMetrics2Properties(OpenMetrics2Properties openMetrics2Properties) { 063 this.openMetrics2Properties = openMetrics2Properties; 064 return this; 065 } 066 067 /** 068 * @param createdTimestampsEnabled whether to include the _created timestamp in the output 069 */ 070 public Builder setCreatedTimestampsEnabled(boolean createdTimestampsEnabled) { 071 this.createdTimestampsEnabled = createdTimestampsEnabled; 072 return this; 073 } 074 075 /** 076 * @param exemplarsOnAllMetricTypesEnabled whether to include exemplars in the output for all 077 * metric types 078 */ 079 public Builder setExemplarsOnAllMetricTypesEnabled(boolean exemplarsOnAllMetricTypesEnabled) { 080 this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled; 081 return this; 082 } 083 084 public OpenMetrics2TextFormatWriter build() { 085 return new OpenMetrics2TextFormatWriter( 086 openMetrics2Properties, createdTimestampsEnabled, exemplarsOnAllMetricTypesEnabled); 087 } 088 } 089 090 public static final String CONTENT_TYPE = 091 "application/openmetrics-text; version=2.0.0; charset=utf-8"; 092 private final OpenMetrics2Properties openMetrics2Properties; 093 private final boolean createdTimestampsEnabled; 094 private final boolean exemplarsOnAllMetricTypesEnabled; 095 096 /** 097 * @param openMetrics2Properties OpenMetrics 2.0 feature flags 098 * @param createdTimestampsEnabled whether to include the _created timestamp in the output - This 099 * will produce an invalid OpenMetrics output, but is kept for backwards compatibility. 100 * @param exemplarsOnAllMetricTypesEnabled whether to include exemplars on all metric types 101 */ 102 public OpenMetrics2TextFormatWriter( 103 OpenMetrics2Properties openMetrics2Properties, 104 boolean createdTimestampsEnabled, 105 boolean exemplarsOnAllMetricTypesEnabled) { 106 this.openMetrics2Properties = openMetrics2Properties; 107 this.createdTimestampsEnabled = createdTimestampsEnabled; 108 this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled; 109 } 110 111 public static Builder builder() { 112 return new Builder(); 113 } 114 115 public static OpenMetrics2TextFormatWriter create() { 116 return builder().build(); 117 } 118 119 @Override 120 public boolean accepts(@Nullable String acceptHeader) { 121 if (acceptHeader == null) { 122 return false; 123 } 124 return acceptHeader.contains("application/openmetrics-text"); 125 } 126 127 @Override 128 public String getContentType() { 129 // When contentNegotiation=false (default), masquerade as OM1 for compatibility 130 // When contentNegotiation=true, use proper OM2 version 131 if (openMetrics2Properties.getContentNegotiation()) { 132 return CONTENT_TYPE; 133 } else { 134 return OpenMetricsTextFormatWriter.CONTENT_TYPE; 135 } 136 } 137 138 public OpenMetrics2Properties getOpenMetrics2Properties() { 139 return openMetrics2Properties; 140 } 141 142 @Override 143 public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme scheme) 144 throws IOException { 145 Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); 146 MetricSnapshots merged = TextFormatUtil.mergeDuplicates(metricSnapshots); 147 for (MetricSnapshot s : merged) { 148 MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, scheme); 149 if (!snapshot.getDataPoints().isEmpty()) { 150 if (snapshot instanceof CounterSnapshot) { 151 writeCounter(writer, (CounterSnapshot) snapshot, scheme); 152 } else if (snapshot instanceof GaugeSnapshot) { 153 writeGauge(writer, (GaugeSnapshot) snapshot, scheme); 154 } else if (snapshot instanceof HistogramSnapshot) { 155 writeHistogram(writer, (HistogramSnapshot) snapshot, scheme); 156 } else if (snapshot instanceof SummarySnapshot) { 157 writeSummary(writer, (SummarySnapshot) snapshot, scheme); 158 } else if (snapshot instanceof InfoSnapshot) { 159 writeInfo(writer, (InfoSnapshot) snapshot, scheme); 160 } else if (snapshot instanceof StateSetSnapshot) { 161 writeStateSet(writer, (StateSetSnapshot) snapshot, scheme); 162 } else if (snapshot instanceof UnknownSnapshot) { 163 writeUnknown(writer, (UnknownSnapshot) snapshot, scheme); 164 } 165 } 166 } 167 writer.write("# EOF\n"); 168 writer.flush(); 169 } 170 171 private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme) 172 throws IOException { 173 MetricMetadata metadata = snapshot.getMetadata(); 174 // OM2: use the name as provided by the user, no _total appending 175 String counterName = getExpositionBaseMetadataName(metadata, scheme); 176 writeMetadataWithName(writer, counterName, "counter", metadata); 177 for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { 178 writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme); 179 writeDouble(writer, data.getValue()); 180 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 181 writeCreated(writer, counterName, data, scheme); 182 } 183 } 184 185 private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme) 186 throws IOException { 187 MetricMetadata metadata = snapshot.getMetadata(); 188 String name = getExpositionBaseMetadataName(metadata, scheme); 189 writeMetadataWithName(writer, name, "gauge", metadata); 190 for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { 191 writeNameAndLabels(writer, name, null, data.getLabels(), scheme); 192 writeDouble(writer, data.getValue()); 193 if (exemplarsOnAllMetricTypesEnabled) { 194 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 195 } else { 196 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 197 } 198 } 199 } 200 201 private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme) 202 throws IOException { 203 MetricMetadata metadata = snapshot.getMetadata(); 204 String name = getExpositionBaseMetadataName(metadata, scheme); 205 if (snapshot.isGaugeHistogram()) { 206 writeMetadataWithName(writer, name, "gaugehistogram", metadata); 207 writeClassicHistogramBuckets( 208 writer, name, "_gcount", "_gsum", snapshot.getDataPoints(), scheme); 209 } else { 210 writeMetadataWithName(writer, name, "histogram", metadata); 211 writeClassicHistogramBuckets( 212 writer, name, "_count", "_sum", snapshot.getDataPoints(), scheme); 213 } 214 } 215 216 private void writeClassicHistogramBuckets( 217 Writer writer, 218 String name, 219 String countSuffix, 220 String sumSuffix, 221 List<HistogramSnapshot.HistogramDataPointSnapshot> dataList, 222 EscapingScheme scheme) 223 throws IOException { 224 for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) { 225 ClassicHistogramBuckets buckets = getClassicBuckets(data); 226 Exemplars exemplars = data.getExemplars(); 227 long cumulativeCount = 0; 228 for (int i = 0; i < buckets.size(); i++) { 229 cumulativeCount += buckets.getCount(i); 230 writeNameAndLabels( 231 writer, name, "_bucket", data.getLabels(), scheme, "le", buckets.getUpperBound(i)); 232 writeLong(writer, cumulativeCount); 233 Exemplar exemplar; 234 if (i == 0) { 235 exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i)); 236 } else { 237 exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i)); 238 } 239 writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme); 240 } 241 // In OpenMetrics format, histogram _count and _sum are either both present or both absent. 242 if (data.hasCount() && data.hasSum()) { 243 writeCountAndSum(writer, name, data, countSuffix, sumSuffix, exemplars, scheme); 244 } 245 writeCreated(writer, name, data, scheme); 246 } 247 } 248 249 private ClassicHistogramBuckets getClassicBuckets( 250 HistogramSnapshot.HistogramDataPointSnapshot data) { 251 if (data.getClassicBuckets().isEmpty()) { 252 return ClassicHistogramBuckets.of( 253 new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()}); 254 } else { 255 return data.getClassicBuckets(); 256 } 257 } 258 259 private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme) 260 throws IOException { 261 boolean metadataWritten = false; 262 MetricMetadata metadata = snapshot.getMetadata(); 263 String name = getExpositionBaseMetadataName(metadata, scheme); 264 for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { 265 if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { 266 continue; 267 } 268 if (!metadataWritten) { 269 writeMetadataWithName(writer, name, "summary", metadata); 270 metadataWritten = true; 271 } 272 Exemplars exemplars = data.getExemplars(); 273 // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose 274 // for which 275 // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...] 276 // for the 277 // quantiles, all indexes modulo exemplars.length. 278 int exemplarIndex = 1; 279 for (Quantile quantile : data.getQuantiles()) { 280 writeNameAndLabels( 281 writer, name, null, data.getLabels(), scheme, "quantile", quantile.getQuantile()); 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, name, data, "_count", "_sum", exemplars, scheme); 292 writeCreated(writer, name, data, scheme); 293 } 294 } 295 296 private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) 297 throws IOException { 298 MetricMetadata metadata = snapshot.getMetadata(); 299 // OM2 spec: Info MetricFamily name MUST end in _info. 300 // In OM2, TYPE/HELP use the same name as the data lines. 301 String infoName = ensureSuffix(getExpositionBaseMetadataName(metadata, scheme), "_info"); 302 writeMetadataWithName(writer, infoName, "info", metadata); 303 for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { 304 writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme); 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 String name = getExpositionBaseMetadataName(metadata, scheme); 314 writeMetadataWithName(writer, name, "stateset", metadata); 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 String name = getExpositionBaseMetadataName(metadata, scheme); 350 writeMetadataWithName(writer, name, "unknown", metadata); 351 for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { 352 writeNameAndLabels(writer, name, null, data.getLabels(), scheme); 353 writeDouble(writer, data.getValue()); 354 if (exemplarsOnAllMetricTypesEnabled) { 355 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 356 } else { 357 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 358 } 359 } 360 } 361 362 private void writeCountAndSum( 363 Writer writer, 364 String name, 365 DistributionDataPointSnapshot data, 366 String countSuffix, 367 String sumSuffix, 368 Exemplars exemplars, 369 EscapingScheme scheme) 370 throws IOException { 371 if (data.hasCount()) { 372 writeNameAndLabels(writer, name, countSuffix, data.getLabels(), scheme); 373 writeLong(writer, data.getCount()); 374 if (exemplarsOnAllMetricTypesEnabled) { 375 writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme); 376 } else { 377 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 378 } 379 } 380 if (data.hasSum()) { 381 writeNameAndLabels(writer, name, sumSuffix, data.getLabels(), scheme); 382 writeDouble(writer, data.getSum()); 383 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 384 } 385 } 386 387 private void writeCreated( 388 Writer writer, String name, DataPointSnapshot data, EscapingScheme scheme) 389 throws IOException { 390 if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { 391 writeNameAndLabels(writer, name, "_created", data.getLabels(), scheme); 392 writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); 393 if (data.hasScrapeTimestamp()) { 394 writer.write(' '); 395 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 396 } 397 writer.write('\n'); 398 } 399 } 400 401 private void writeNameAndLabels( 402 Writer writer, 403 String name, 404 @Nullable String suffix, 405 Labels labels, 406 EscapingScheme escapingScheme) 407 throws IOException { 408 writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0); 409 } 410 411 private void writeNameAndLabels( 412 Writer writer, 413 String name, 414 @Nullable String suffix, 415 Labels labels, 416 EscapingScheme escapingScheme, 417 @Nullable String additionalLabelName, 418 double additionalLabelValue) 419 throws IOException { 420 boolean metricInsideBraces = false; 421 // If the name does not pass the legacy validity check, we must put the 422 // metric name inside the braces. 423 if (!PrometheusNaming.isValidLegacyMetricName(name)) { 424 metricInsideBraces = true; 425 writer.write('{'); 426 } 427 writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric); 428 if (!labels.isEmpty() || additionalLabelName != null) { 429 writeLabels( 430 writer, 431 labels, 432 additionalLabelName, 433 additionalLabelValue, 434 metricInsideBraces, 435 escapingScheme); 436 } else if (metricInsideBraces) { 437 writer.write('}'); 438 } 439 writer.write(' '); 440 } 441 442 private void writeScrapeTimestampAndExemplar( 443 Writer writer, DataPointSnapshot data, @Nullable Exemplar exemplar, EscapingScheme scheme) 444 throws IOException { 445 if (data.hasScrapeTimestamp()) { 446 writer.write(' '); 447 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 448 } 449 if (exemplar != null) { 450 writer.write(" # "); 451 writeLabels(writer, exemplar.getLabels(), null, 0, false, scheme); 452 writer.write(' '); 453 writeDouble(writer, exemplar.getValue()); 454 if (exemplar.hasTimestamp()) { 455 writer.write(' '); 456 writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis()); 457 } 458 } 459 writer.write('\n'); 460 } 461 462 private void writeMetadataWithName( 463 Writer writer, String name, String typeName, MetricMetadata metadata) throws IOException { 464 writer.write("# TYPE "); 465 writeName(writer, name, NameType.Metric); 466 writer.write(' '); 467 writer.write(typeName); 468 writer.write('\n'); 469 if (metadata.getUnit() != null) { 470 writer.write("# UNIT "); 471 writeName(writer, name, NameType.Metric); 472 writer.write(' '); 473 writeEscapedString(writer, metadata.getUnit().toString()); 474 writer.write('\n'); 475 } 476 if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { 477 writer.write("# HELP "); 478 writeName(writer, name, NameType.Metric); 479 writer.write(' '); 480 writeEscapedString(writer, metadata.getHelp()); 481 writer.write('\n'); 482 } 483 } 484 485 private static String ensureSuffix(String name, String suffix) { 486 if (name.endsWith(suffix)) { 487 return name; 488 } 489 return name + suffix; 490 } 491}