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 writeMetadataWithName(writer, baseName, "counter", metadata); 150 for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { 151 writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme); 152 writeDouble(writer, data.getValue()); 153 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 154 writeCreated(writer, baseName, data, scheme); 155 } 156 } 157 158 private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme) 159 throws IOException { 160 MetricMetadata metadata = snapshot.getMetadata(); 161 writeMetadata(writer, "gauge", metadata, scheme); 162 String name = getMetadataName(metadata, scheme); 163 for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { 164 writeNameAndLabels(writer, name, null, data.getLabels(), scheme); 165 writeDouble(writer, data.getValue()); 166 if (exemplarsOnAllMetricTypesEnabled) { 167 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 168 } else { 169 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 170 } 171 } 172 } 173 174 void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme) 175 throws IOException { 176 MetricMetadata metadata = snapshot.getMetadata(); 177 if (snapshot.isGaugeHistogram()) { 178 writeMetadata(writer, "gaugehistogram", metadata, scheme); 179 writeClassicHistogramBuckets( 180 writer, metadata, "_gcount", "_gsum", snapshot.getDataPoints(), scheme); 181 } else { 182 writeMetadata(writer, "histogram", metadata, scheme); 183 writeClassicHistogramBuckets( 184 writer, metadata, "_count", "_sum", snapshot.getDataPoints(), scheme); 185 } 186 } 187 188 private void writeClassicHistogramBuckets( 189 Writer writer, 190 MetricMetadata metadata, 191 String countSuffix, 192 String sumSuffix, 193 List<HistogramSnapshot.HistogramDataPointSnapshot> dataList, 194 EscapingScheme scheme) 195 throws IOException { 196 String name = getMetadataName(metadata, scheme); 197 String bucketName = name + "_bucket"; 198 String countName = name + countSuffix; 199 String sumName = name + sumSuffix; 200 for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) { 201 ClassicHistogramBuckets buckets = getClassicBuckets(data); 202 Exemplars exemplars = data.getExemplars(); 203 long cumulativeCount = 0; 204 for (int i = 0; i < buckets.size(); i++) { 205 cumulativeCount += buckets.getCount(i); 206 writeNameAndLabels( 207 writer, bucketName, null, data.getLabels(), scheme, "le", buckets.getUpperBound(i)); 208 writeLong(writer, cumulativeCount); 209 Exemplar exemplar; 210 if (i == 0) { 211 exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i)); 212 } else { 213 exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i)); 214 } 215 writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme); 216 } 217 // In OpenMetrics format, histogram _count and _sum are either both present or both absent. 218 if (data.hasCount() && data.hasSum()) { 219 writeCountAndSum(writer, countName, sumName, data, exemplars, scheme); 220 } 221 writeCreated(writer, name, data, scheme); 222 } 223 } 224 225 private ClassicHistogramBuckets getClassicBuckets( 226 HistogramSnapshot.HistogramDataPointSnapshot data) { 227 if (data.getClassicBuckets().isEmpty()) { 228 return ClassicHistogramBuckets.of( 229 new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()}); 230 } else { 231 return data.getClassicBuckets(); 232 } 233 } 234 235 void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme) 236 throws IOException { 237 boolean metadataWritten = false; 238 MetricMetadata metadata = snapshot.getMetadata(); 239 String name = getMetadataName(metadata, scheme); 240 String countName = name + "_count"; 241 String sumName = name + "_sum"; 242 for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { 243 if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { 244 continue; 245 } 246 if (!metadataWritten) { 247 writeMetadata(writer, "summary", metadata, scheme); 248 metadataWritten = true; 249 } 250 Exemplars exemplars = data.getExemplars(); 251 // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose 252 // for which 253 // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...] 254 // for the 255 // quantiles, all indexes modulo exemplars.length. 256 int exemplarIndex = 1; 257 for (Quantile quantile : data.getQuantiles()) { 258 writeNameAndLabels( 259 writer, name, null, data.getLabels(), scheme, "quantile", quantile.getQuantile()); 260 writeDouble(writer, quantile.getValue()); 261 if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) { 262 exemplarIndex = (exemplarIndex + 1) % exemplars.size(); 263 writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex), scheme); 264 } else { 265 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 266 } 267 } 268 // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics. 269 writeCountAndSum(writer, countName, sumName, data, exemplars, scheme); 270 writeCreated(writer, name, data, scheme); 271 } 272 } 273 274 private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) 275 throws IOException { 276 MetricMetadata metadata = snapshot.getMetadata(); 277 String infoName = resolveExpositionName(metadata, "_info", scheme); 278 String baseName = resolveBaseName(infoName, "_info"); 279 writeMetadataWithName(writer, baseName, "info", metadata); 280 for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { 281 writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme); 282 writer.write("1"); 283 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 284 } 285 } 286 287 private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme) 288 throws IOException { 289 MetricMetadata metadata = snapshot.getMetadata(); 290 writeMetadata(writer, "stateset", metadata, scheme); 291 String name = getMetadataName(metadata, scheme); 292 for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { 293 for (int i = 0; i < data.size(); i++) { 294 writer.write(name); 295 writer.write('{'); 296 Labels labels = data.getLabels(); 297 for (int j = 0; j < labels.size(); j++) { 298 if (j > 0) { 299 writer.write(","); 300 } 301 writer.write(getSnapshotLabelName(labels, j, scheme)); 302 writer.write("=\""); 303 writeEscapedString(writer, labels.getValue(j)); 304 writer.write("\""); 305 } 306 if (!labels.isEmpty()) { 307 writer.write(","); 308 } 309 writer.write(name); 310 writer.write("=\""); 311 writeEscapedString(writer, data.getName(i)); 312 writer.write("\"} "); 313 if (data.isTrue(i)) { 314 writer.write("1"); 315 } else { 316 writer.write("0"); 317 } 318 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 319 } 320 } 321 } 322 323 private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme) 324 throws IOException { 325 MetricMetadata metadata = snapshot.getMetadata(); 326 writeMetadata(writer, "unknown", metadata, scheme); 327 String name = getMetadataName(metadata, scheme); 328 for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { 329 writeNameAndLabels(writer, name, 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 String countName, 342 String sumName, 343 DistributionDataPointSnapshot data, 344 Exemplars exemplars, 345 EscapingScheme scheme) 346 throws IOException { 347 if (data.hasCount()) { 348 writeNameAndLabels(writer, countName, null, data.getLabels(), scheme); 349 writeLong(writer, data.getCount()); 350 if (exemplarsOnAllMetricTypesEnabled) { 351 writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme); 352 } else { 353 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 354 } 355 } 356 if (data.hasSum()) { 357 writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme); 358 writeDouble(writer, data.getSum()); 359 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 360 } 361 } 362 363 private void writeCreated( 364 Writer writer, String baseName, DataPointSnapshot data, EscapingScheme scheme) 365 throws IOException { 366 if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { 367 writeNameAndLabels(writer, baseName, "_created", data.getLabels(), scheme); 368 writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); 369 if (data.hasScrapeTimestamp()) { 370 writer.write(' '); 371 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 372 } 373 writer.write('\n'); 374 } 375 } 376 377 private void writeNameAndLabels( 378 Writer writer, 379 String name, 380 @Nullable String suffix, 381 Labels labels, 382 EscapingScheme escapingScheme) 383 throws IOException { 384 writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0); 385 } 386 387 private void writeNameAndLabels( 388 Writer writer, 389 String name, 390 @Nullable String suffix, 391 Labels labels, 392 EscapingScheme escapingScheme, 393 @Nullable String additionalLabelName, 394 double additionalLabelValue) 395 throws IOException { 396 boolean metricInsideBraces = false; 397 // If the name does not pass the legacy validity check, we must put the 398 // metric name inside the braces. 399 if (!PrometheusNaming.isValidLegacyMetricName(name)) { 400 metricInsideBraces = true; 401 writer.write('{'); 402 } 403 writeName(writer, suffix != null ? name + suffix : name, NameType.Metric); 404 if (!labels.isEmpty() || additionalLabelName != null) { 405 writeLabels( 406 writer, 407 labels, 408 additionalLabelName, 409 additionalLabelValue, 410 metricInsideBraces, 411 escapingScheme); 412 } else if (metricInsideBraces) { 413 writer.write('}'); 414 } 415 writer.write(' '); 416 } 417 418 void writeScrapeTimestampAndExemplar( 419 Writer writer, DataPointSnapshot data, @Nullable Exemplar exemplar, EscapingScheme scheme) 420 throws IOException { 421 if (data.hasScrapeTimestamp()) { 422 writer.write(' '); 423 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 424 } 425 if (exemplar != null) { 426 writeExemplar(writer, exemplar, scheme); 427 } 428 writer.write('\n'); 429 } 430 431 void writeExemplar(Writer writer, Exemplar exemplar, EscapingScheme scheme) throws IOException { 432 writer.write(" # "); 433 writeLabels(writer, exemplar.getLabels(), null, 0, false, scheme); 434 writer.write(' '); 435 writeDouble(writer, exemplar.getValue()); 436 if (exemplar.hasTimestamp()) { 437 writer.write(' '); 438 writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis()); 439 } 440 } 441 442 /** 443 * Returns the full exposition name for a metric. If the original name already ends with the given 444 * suffix (e.g. "_total" for counters), uses the original name directly. Otherwise, appends the 445 * suffix to the base name. 446 */ 447 private static String resolveExpositionName( 448 MetricMetadata metadata, String suffix, EscapingScheme scheme) { 449 String expositionBaseName = getExpositionBaseMetadataName(metadata, scheme); 450 if (expositionBaseName.endsWith(suffix)) { 451 return expositionBaseName; 452 } 453 return getMetadataName(metadata, scheme) + suffix; 454 } 455 456 private void writeMetadata( 457 Writer writer, String typeName, MetricMetadata metadata, EscapingScheme scheme) 458 throws IOException { 459 writeMetadataWithName(writer, getMetadataName(metadata, scheme), typeName, metadata); 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 resolveBaseName(String fullName, String suffix) { 486 if (fullName.endsWith(suffix)) { 487 return fullName.substring(0, fullName.length() - suffix.length()); 488 } 489 return fullName; 490 } 491}