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