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