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