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.getMetadataName; 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. This is currently a skeleton implementation that produces 044 * identical output to OpenMetrics 1.0, with infrastructure for future OM2 features. This is 045 * 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 writeMetadata(writer, "counter", metadata, scheme); 175 for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { 176 writeNameAndLabels( 177 writer, getMetadataName(metadata, scheme), "_total", data.getLabels(), scheme); 178 writeDouble(writer, data.getValue()); 179 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 180 writeCreated(writer, metadata, data, scheme); 181 } 182 } 183 184 private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme) 185 throws IOException { 186 MetricMetadata metadata = snapshot.getMetadata(); 187 writeMetadata(writer, "gauge", metadata, scheme); 188 for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { 189 writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); 190 writeDouble(writer, data.getValue()); 191 if (exemplarsOnAllMetricTypesEnabled) { 192 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 193 } else { 194 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 195 } 196 } 197 } 198 199 private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme) 200 throws IOException { 201 MetricMetadata metadata = snapshot.getMetadata(); 202 if (snapshot.isGaugeHistogram()) { 203 writeMetadata(writer, "gaugehistogram", metadata, scheme); 204 writeClassicHistogramBuckets( 205 writer, metadata, "_gcount", "_gsum", snapshot.getDataPoints(), scheme); 206 } else { 207 writeMetadata(writer, "histogram", metadata, scheme); 208 writeClassicHistogramBuckets( 209 writer, metadata, "_count", "_sum", snapshot.getDataPoints(), scheme); 210 } 211 } 212 213 private void writeClassicHistogramBuckets( 214 Writer writer, 215 MetricMetadata metadata, 216 String countSuffix, 217 String sumSuffix, 218 List<HistogramSnapshot.HistogramDataPointSnapshot> dataList, 219 EscapingScheme scheme) 220 throws IOException { 221 for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) { 222 ClassicHistogramBuckets buckets = getClassicBuckets(data); 223 Exemplars exemplars = data.getExemplars(); 224 long cumulativeCount = 0; 225 for (int i = 0; i < buckets.size(); i++) { 226 cumulativeCount += buckets.getCount(i); 227 writeNameAndLabels( 228 writer, 229 getMetadataName(metadata, scheme), 230 "_bucket", 231 data.getLabels(), 232 scheme, 233 "le", 234 buckets.getUpperBound(i)); 235 writeLong(writer, cumulativeCount); 236 Exemplar exemplar; 237 if (i == 0) { 238 exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i)); 239 } else { 240 exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i)); 241 } 242 writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme); 243 } 244 // In OpenMetrics format, histogram _count and _sum are either both present or both absent. 245 if (data.hasCount() && data.hasSum()) { 246 writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, exemplars, scheme); 247 } 248 writeCreated(writer, metadata, data, scheme); 249 } 250 } 251 252 private ClassicHistogramBuckets getClassicBuckets( 253 HistogramSnapshot.HistogramDataPointSnapshot data) { 254 if (data.getClassicBuckets().isEmpty()) { 255 return ClassicHistogramBuckets.of( 256 new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()}); 257 } else { 258 return data.getClassicBuckets(); 259 } 260 } 261 262 private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme) 263 throws IOException { 264 boolean metadataWritten = false; 265 MetricMetadata metadata = snapshot.getMetadata(); 266 for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { 267 if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { 268 continue; 269 } 270 if (!metadataWritten) { 271 writeMetadata(writer, "summary", metadata, scheme); 272 metadataWritten = true; 273 } 274 Exemplars exemplars = data.getExemplars(); 275 // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose 276 // for which 277 // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...] 278 // for the 279 // quantiles, all indexes modulo exemplars.length. 280 int exemplarIndex = 1; 281 for (Quantile quantile : data.getQuantiles()) { 282 writeNameAndLabels( 283 writer, 284 getMetadataName(metadata, scheme), 285 null, 286 data.getLabels(), 287 scheme, 288 "quantile", 289 quantile.getQuantile()); 290 writeDouble(writer, quantile.getValue()); 291 if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) { 292 exemplarIndex = (exemplarIndex + 1) % exemplars.size(); 293 writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex), scheme); 294 } else { 295 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 296 } 297 } 298 // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics. 299 writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars, scheme); 300 writeCreated(writer, metadata, data, scheme); 301 } 302 } 303 304 private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) 305 throws IOException { 306 MetricMetadata metadata = snapshot.getMetadata(); 307 writeMetadata(writer, "info", metadata, scheme); 308 for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { 309 writeNameAndLabels( 310 writer, getMetadataName(metadata, scheme), "_info", data.getLabels(), scheme); 311 writer.write("1"); 312 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 313 } 314 } 315 316 private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme) 317 throws IOException { 318 MetricMetadata metadata = snapshot.getMetadata(); 319 writeMetadata(writer, "stateset", metadata, scheme); 320 for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { 321 for (int i = 0; i < data.size(); i++) { 322 writer.write(getMetadataName(metadata, scheme)); 323 writer.write('{'); 324 Labels labels = data.getLabels(); 325 for (int j = 0; j < labels.size(); j++) { 326 if (j > 0) { 327 writer.write(","); 328 } 329 writer.write(getSnapshotLabelName(labels, j, scheme)); 330 writer.write("=\""); 331 writeEscapedString(writer, labels.getValue(j)); 332 writer.write("\""); 333 } 334 if (!labels.isEmpty()) { 335 writer.write(","); 336 } 337 writer.write(getMetadataName(metadata, scheme)); 338 writer.write("=\""); 339 writeEscapedString(writer, data.getName(i)); 340 writer.write("\"} "); 341 if (data.isTrue(i)) { 342 writer.write("1"); 343 } else { 344 writer.write("0"); 345 } 346 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 347 } 348 } 349 } 350 351 private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme) 352 throws IOException { 353 MetricMetadata metadata = snapshot.getMetadata(); 354 writeMetadata(writer, "unknown", metadata, scheme); 355 for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { 356 writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); 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 MetricMetadata metadata, 369 DistributionDataPointSnapshot data, 370 String countSuffix, 371 String sumSuffix, 372 Exemplars exemplars, 373 EscapingScheme scheme) 374 throws IOException { 375 if (data.hasCount()) { 376 writeNameAndLabels( 377 writer, getMetadataName(metadata, scheme), countSuffix, data.getLabels(), scheme); 378 writeLong(writer, data.getCount()); 379 if (exemplarsOnAllMetricTypesEnabled) { 380 writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme); 381 } else { 382 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 383 } 384 } 385 if (data.hasSum()) { 386 writeNameAndLabels( 387 writer, getMetadataName(metadata, scheme), sumSuffix, data.getLabels(), scheme); 388 writeDouble(writer, data.getSum()); 389 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 390 } 391 } 392 393 private void writeCreated( 394 Writer writer, MetricMetadata metadata, DataPointSnapshot data, EscapingScheme scheme) 395 throws IOException { 396 if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { 397 writeNameAndLabels( 398 writer, getMetadataName(metadata, scheme), "_created", data.getLabels(), scheme); 399 writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); 400 if (data.hasScrapeTimestamp()) { 401 writer.write(' '); 402 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 403 } 404 writer.write('\n'); 405 } 406 } 407 408 private void writeNameAndLabels( 409 Writer writer, 410 String name, 411 @Nullable String suffix, 412 Labels labels, 413 EscapingScheme escapingScheme) 414 throws IOException { 415 writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0); 416 } 417 418 private void writeNameAndLabels( 419 Writer writer, 420 String name, 421 @Nullable String suffix, 422 Labels labels, 423 EscapingScheme escapingScheme, 424 @Nullable String additionalLabelName, 425 double additionalLabelValue) 426 throws IOException { 427 boolean metricInsideBraces = false; 428 // If the name does not pass the legacy validity check, we must put the 429 // metric name inside the braces. 430 if (!PrometheusNaming.isValidLegacyMetricName(name)) { 431 metricInsideBraces = true; 432 writer.write('{'); 433 } 434 writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric); 435 if (!labels.isEmpty() || additionalLabelName != null) { 436 writeLabels( 437 writer, 438 labels, 439 additionalLabelName, 440 additionalLabelValue, 441 metricInsideBraces, 442 escapingScheme); 443 } else if (metricInsideBraces) { 444 writer.write('}'); 445 } 446 writer.write(' '); 447 } 448 449 private void writeScrapeTimestampAndExemplar( 450 Writer writer, DataPointSnapshot data, @Nullable Exemplar exemplar, EscapingScheme scheme) 451 throws IOException { 452 if (data.hasScrapeTimestamp()) { 453 writer.write(' '); 454 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 455 } 456 if (exemplar != null) { 457 writer.write(" # "); 458 writeLabels(writer, exemplar.getLabels(), null, 0, false, scheme); 459 writer.write(' '); 460 writeDouble(writer, exemplar.getValue()); 461 if (exemplar.hasTimestamp()) { 462 writer.write(' '); 463 writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis()); 464 } 465 } 466 writer.write('\n'); 467 } 468 469 private void writeMetadata( 470 Writer writer, String typeName, MetricMetadata metadata, EscapingScheme scheme) 471 throws IOException { 472 writer.write("# TYPE "); 473 writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); 474 writer.write(' '); 475 writer.write(typeName); 476 writer.write('\n'); 477 if (metadata.getUnit() != null) { 478 writer.write("# UNIT "); 479 writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); 480 writer.write(' '); 481 writeEscapedString(writer, metadata.getUnit().toString()); 482 writer.write('\n'); 483 } 484 if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { 485 writer.write("# HELP "); 486 writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); 487 writer.write(' '); 488 writeEscapedString(writer, metadata.getHelp()); 489 writer.write('\n'); 490 } 491 } 492}