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 for (MetricSnapshot s : metricSnapshots) { 117 MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, scheme); 118 if (!snapshot.getDataPoints().isEmpty()) { 119 if (snapshot instanceof CounterSnapshot) { 120 writeCounter(writer, (CounterSnapshot) snapshot, scheme); 121 } else if (snapshot instanceof GaugeSnapshot) { 122 writeGauge(writer, (GaugeSnapshot) snapshot, scheme); 123 } else if (snapshot instanceof HistogramSnapshot) { 124 writeHistogram(writer, (HistogramSnapshot) snapshot, scheme); 125 } else if (snapshot instanceof SummarySnapshot) { 126 writeSummary(writer, (SummarySnapshot) snapshot, scheme); 127 } else if (snapshot instanceof InfoSnapshot) { 128 writeInfo(writer, (InfoSnapshot) snapshot, scheme); 129 } else if (snapshot instanceof StateSetSnapshot) { 130 writeStateSet(writer, (StateSetSnapshot) snapshot, scheme); 131 } else if (snapshot instanceof UnknownSnapshot) { 132 writeUnknown(writer, (UnknownSnapshot) snapshot, scheme); 133 } 134 } 135 } 136 writer.write("# EOF\n"); 137 writer.flush(); 138 } 139 140 private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme) 141 throws IOException { 142 MetricMetadata metadata = snapshot.getMetadata(); 143 writeMetadata(writer, "counter", metadata, scheme); 144 for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { 145 writeNameAndLabels( 146 writer, getMetadataName(metadata, scheme), "_total", data.getLabels(), scheme); 147 writeDouble(writer, data.getValue()); 148 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 149 writeCreated(writer, metadata, data, scheme); 150 } 151 } 152 153 private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme) 154 throws IOException { 155 MetricMetadata metadata = snapshot.getMetadata(); 156 writeMetadata(writer, "gauge", metadata, scheme); 157 for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { 158 writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); 159 writeDouble(writer, data.getValue()); 160 if (exemplarsOnAllMetricTypesEnabled) { 161 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 162 } else { 163 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 164 } 165 } 166 } 167 168 private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme) 169 throws IOException { 170 MetricMetadata metadata = snapshot.getMetadata(); 171 if (snapshot.isGaugeHistogram()) { 172 writeMetadata(writer, "gaugehistogram", metadata, scheme); 173 writeClassicHistogramBuckets( 174 writer, metadata, "_gcount", "_gsum", snapshot.getDataPoints(), scheme); 175 } else { 176 writeMetadata(writer, "histogram", metadata, scheme); 177 writeClassicHistogramBuckets( 178 writer, metadata, "_count", "_sum", snapshot.getDataPoints(), scheme); 179 } 180 } 181 182 private void writeClassicHistogramBuckets( 183 Writer writer, 184 MetricMetadata metadata, 185 String countSuffix, 186 String sumSuffix, 187 List<HistogramSnapshot.HistogramDataPointSnapshot> dataList, 188 EscapingScheme scheme) 189 throws IOException { 190 for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) { 191 ClassicHistogramBuckets buckets = getClassicBuckets(data); 192 Exemplars exemplars = data.getExemplars(); 193 long cumulativeCount = 0; 194 for (int i = 0; i < buckets.size(); i++) { 195 cumulativeCount += buckets.getCount(i); 196 writeNameAndLabels( 197 writer, 198 getMetadataName(metadata, scheme), 199 "_bucket", 200 data.getLabels(), 201 scheme, 202 "le", 203 buckets.getUpperBound(i)); 204 writeLong(writer, cumulativeCount); 205 Exemplar exemplar; 206 if (i == 0) { 207 exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i)); 208 } else { 209 exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i)); 210 } 211 writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme); 212 } 213 // In OpenMetrics format, histogram _count and _sum are either both present or both absent. 214 if (data.hasCount() && data.hasSum()) { 215 writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, exemplars, scheme); 216 } 217 writeCreated(writer, metadata, data, scheme); 218 } 219 } 220 221 private ClassicHistogramBuckets getClassicBuckets( 222 HistogramSnapshot.HistogramDataPointSnapshot data) { 223 if (data.getClassicBuckets().isEmpty()) { 224 return ClassicHistogramBuckets.of( 225 new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()}); 226 } else { 227 return data.getClassicBuckets(); 228 } 229 } 230 231 private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme) 232 throws IOException { 233 boolean metadataWritten = false; 234 MetricMetadata metadata = snapshot.getMetadata(); 235 for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { 236 if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { 237 continue; 238 } 239 if (!metadataWritten) { 240 writeMetadata(writer, "summary", metadata, scheme); 241 metadataWritten = true; 242 } 243 Exemplars exemplars = data.getExemplars(); 244 // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose 245 // for which 246 // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...] 247 // for the 248 // quantiles, all indexes modulo exemplars.length. 249 int exemplarIndex = 1; 250 for (Quantile quantile : data.getQuantiles()) { 251 writeNameAndLabels( 252 writer, 253 getMetadataName(metadata, scheme), 254 null, 255 data.getLabels(), 256 scheme, 257 "quantile", 258 quantile.getQuantile()); 259 writeDouble(writer, quantile.getValue()); 260 if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) { 261 exemplarIndex = (exemplarIndex + 1) % exemplars.size(); 262 writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex), scheme); 263 } else { 264 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 265 } 266 } 267 // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics. 268 writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars, scheme); 269 writeCreated(writer, metadata, data, scheme); 270 } 271 } 272 273 private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme) 274 throws IOException { 275 MetricMetadata metadata = snapshot.getMetadata(); 276 writeMetadata(writer, "info", metadata, scheme); 277 for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { 278 writeNameAndLabels( 279 writer, getMetadataName(metadata, scheme), "_info", 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 for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { 290 for (int i = 0; i < data.size(); i++) { 291 writer.write(getMetadataName(metadata, scheme)); 292 writer.write('{'); 293 Labels labels = data.getLabels(); 294 for (int j = 0; j < labels.size(); j++) { 295 if (j > 0) { 296 writer.write(","); 297 } 298 writer.write(getSnapshotLabelName(labels, j, scheme)); 299 writer.write("=\""); 300 writeEscapedString(writer, labels.getValue(j)); 301 writer.write("\""); 302 } 303 if (!labels.isEmpty()) { 304 writer.write(","); 305 } 306 writer.write(getMetadataName(metadata, scheme)); 307 writer.write("=\""); 308 writeEscapedString(writer, data.getName(i)); 309 writer.write("\"} "); 310 if (data.isTrue(i)) { 311 writer.write("1"); 312 } else { 313 writer.write("0"); 314 } 315 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 316 } 317 } 318 } 319 320 private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme) 321 throws IOException { 322 MetricMetadata metadata = snapshot.getMetadata(); 323 writeMetadata(writer, "unknown", metadata, scheme); 324 for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { 325 writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme); 326 writeDouble(writer, data.getValue()); 327 if (exemplarsOnAllMetricTypesEnabled) { 328 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); 329 } else { 330 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 331 } 332 } 333 } 334 335 private void writeCountAndSum( 336 Writer writer, 337 MetricMetadata metadata, 338 DistributionDataPointSnapshot data, 339 String countSuffix, 340 String sumSuffix, 341 Exemplars exemplars, 342 EscapingScheme scheme) 343 throws IOException { 344 if (data.hasCount()) { 345 writeNameAndLabels( 346 writer, getMetadataName(metadata, scheme), countSuffix, 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( 356 writer, getMetadataName(metadata, scheme), sumSuffix, data.getLabels(), scheme); 357 writeDouble(writer, data.getSum()); 358 writeScrapeTimestampAndExemplar(writer, data, null, scheme); 359 } 360 } 361 362 private void writeCreated( 363 Writer writer, MetricMetadata metadata, DataPointSnapshot data, EscapingScheme scheme) 364 throws IOException { 365 if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { 366 writeNameAndLabels( 367 writer, getMetadataName(metadata, scheme), "_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, name + (suffix != null ? suffix : ""), 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 private 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 writer.write(" # "); 427 writeLabels(writer, exemplar.getLabels(), null, 0, false, scheme); 428 writer.write(' '); 429 writeDouble(writer, exemplar.getValue()); 430 if (exemplar.hasTimestamp()) { 431 writer.write(' '); 432 writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis()); 433 } 434 } 435 writer.write('\n'); 436 } 437 438 private void writeMetadata( 439 Writer writer, String typeName, MetricMetadata metadata, EscapingScheme scheme) 440 throws IOException { 441 writer.write("# TYPE "); 442 writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); 443 writer.write(' '); 444 writer.write(typeName); 445 writer.write('\n'); 446 if (metadata.getUnit() != null) { 447 writer.write("# UNIT "); 448 writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); 449 writer.write(' '); 450 writeEscapedString(writer, metadata.getUnit().toString()); 451 writer.write('\n'); 452 } 453 if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { 454 writer.write("# HELP "); 455 writeName(writer, getMetadataName(metadata, scheme), NameType.Metric); 456 writer.write(' '); 457 writeEscapedString(writer, metadata.getHelp()); 458 writer.write('\n'); 459 } 460 } 461}