001package io.prometheus.metrics.expositionformats; 002 003import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeDouble; 004import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeEscapedLabelValue; 005import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLabels; 006import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong; 007import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeOpenMetricsTimestamp; 008 009import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets; 010import io.prometheus.metrics.model.snapshots.CounterSnapshot; 011import io.prometheus.metrics.model.snapshots.DataPointSnapshot; 012import io.prometheus.metrics.model.snapshots.DistributionDataPointSnapshot; 013import io.prometheus.metrics.model.snapshots.Exemplar; 014import io.prometheus.metrics.model.snapshots.Exemplars; 015import io.prometheus.metrics.model.snapshots.GaugeSnapshot; 016import io.prometheus.metrics.model.snapshots.HistogramSnapshot; 017import io.prometheus.metrics.model.snapshots.InfoSnapshot; 018import io.prometheus.metrics.model.snapshots.Labels; 019import io.prometheus.metrics.model.snapshots.MetricMetadata; 020import io.prometheus.metrics.model.snapshots.MetricSnapshot; 021import io.prometheus.metrics.model.snapshots.MetricSnapshots; 022import io.prometheus.metrics.model.snapshots.Quantile; 023import io.prometheus.metrics.model.snapshots.StateSetSnapshot; 024import io.prometheus.metrics.model.snapshots.SummarySnapshot; 025import io.prometheus.metrics.model.snapshots.UnknownSnapshot; 026import java.io.BufferedWriter; 027import java.io.IOException; 028import java.io.OutputStream; 029import java.io.OutputStreamWriter; 030import java.io.Writer; 031import java.nio.charset.StandardCharsets; 032import java.util.List; 033 034/** 035 * Write the OpenMetrics text format as defined on <a 036 * href="https://openmetrics.io/">https://openmetrics.io</a>. 037 */ 038public class OpenMetricsTextFormatWriter implements ExpositionFormatWriter { 039 040 public static class Builder { 041 boolean createdTimestampsEnabled; 042 boolean exemplarsOnAllMetricTypesEnabled; 043 044 private Builder() {} 045 046 /** 047 * @param createdTimestampsEnabled whether to include the _created timestamp in the output 048 */ 049 public Builder setCreatedTimestampsEnabled(boolean createdTimestampsEnabled) { 050 this.createdTimestampsEnabled = createdTimestampsEnabled; 051 return this; 052 } 053 054 /** 055 * @param exemplarsOnAllMetricTypesEnabled whether to include exemplars in the output for all 056 * metric types 057 */ 058 public Builder setExemplarsOnAllMetricTypesEnabled(boolean exemplarsOnAllMetricTypesEnabled) { 059 this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled; 060 return this; 061 } 062 063 public OpenMetricsTextFormatWriter build() { 064 return new OpenMetricsTextFormatWriter( 065 createdTimestampsEnabled, exemplarsOnAllMetricTypesEnabled); 066 } 067 } 068 069 public static final String CONTENT_TYPE = 070 "application/openmetrics-text; version=1.0.0; charset=utf-8"; 071 private final boolean createdTimestampsEnabled; 072 private final boolean exemplarsOnAllMetricTypesEnabled; 073 074 /** 075 * @param createdTimestampsEnabled whether to include the _created timestamp in the output - This 076 * will produce an invalid OpenMetrics output, but is kept for backwards compatibility. 077 */ 078 public OpenMetricsTextFormatWriter( 079 boolean createdTimestampsEnabled, boolean exemplarsOnAllMetricTypesEnabled) { 080 this.createdTimestampsEnabled = createdTimestampsEnabled; 081 this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled; 082 } 083 084 public static Builder builder() { 085 return new Builder(); 086 } 087 088 public static OpenMetricsTextFormatWriter create() { 089 return builder().build(); 090 } 091 092 @Override 093 public boolean accepts(String acceptHeader) { 094 if (acceptHeader == null) { 095 return false; 096 } 097 return acceptHeader.contains("application/openmetrics-text"); 098 } 099 100 @Override 101 public String getContentType() { 102 return CONTENT_TYPE; 103 } 104 105 @Override 106 public void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException { 107 Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); 108 for (MetricSnapshot snapshot : metricSnapshots) { 109 if (!snapshot.getDataPoints().isEmpty()) { 110 if (snapshot instanceof CounterSnapshot) { 111 writeCounter(writer, (CounterSnapshot) snapshot); 112 } else if (snapshot instanceof GaugeSnapshot) { 113 writeGauge(writer, (GaugeSnapshot) snapshot); 114 } else if (snapshot instanceof HistogramSnapshot) { 115 writeHistogram(writer, (HistogramSnapshot) snapshot); 116 } else if (snapshot instanceof SummarySnapshot) { 117 writeSummary(writer, (SummarySnapshot) snapshot); 118 } else if (snapshot instanceof InfoSnapshot) { 119 writeInfo(writer, (InfoSnapshot) snapshot); 120 } else if (snapshot instanceof StateSetSnapshot) { 121 writeStateSet(writer, (StateSetSnapshot) snapshot); 122 } else if (snapshot instanceof UnknownSnapshot) { 123 writeUnknown(writer, (UnknownSnapshot) snapshot); 124 } 125 } 126 } 127 writer.write("# EOF\n"); 128 writer.flush(); 129 } 130 131 private void writeCounter(Writer writer, CounterSnapshot snapshot) throws IOException { 132 MetricMetadata metadata = snapshot.getMetadata(); 133 writeMetadata(writer, "counter", metadata); 134 for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { 135 writeNameAndLabels(writer, metadata.getPrometheusName(), "_total", data.getLabels()); 136 writeDouble(writer, data.getValue()); 137 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar()); 138 writeCreated(writer, metadata, data); 139 } 140 } 141 142 private void writeGauge(Writer writer, GaugeSnapshot snapshot) throws IOException { 143 MetricMetadata metadata = snapshot.getMetadata(); 144 writeMetadata(writer, "gauge", metadata); 145 for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) { 146 writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels()); 147 writeDouble(writer, data.getValue()); 148 if (exemplarsOnAllMetricTypesEnabled) { 149 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar()); 150 } else { 151 writeScrapeTimestampAndExemplar(writer, data, null); 152 } 153 } 154 } 155 156 private void writeHistogram(Writer writer, HistogramSnapshot snapshot) throws IOException { 157 MetricMetadata metadata = snapshot.getMetadata(); 158 if (snapshot.isGaugeHistogram()) { 159 writeMetadata(writer, "gaugehistogram", metadata); 160 writeClassicHistogramBuckets(writer, metadata, "_gcount", "_gsum", snapshot.getDataPoints()); 161 } else { 162 writeMetadata(writer, "histogram", metadata); 163 writeClassicHistogramBuckets(writer, metadata, "_count", "_sum", snapshot.getDataPoints()); 164 } 165 } 166 167 private void writeClassicHistogramBuckets( 168 Writer writer, 169 MetricMetadata metadata, 170 String countSuffix, 171 String sumSuffix, 172 List<HistogramSnapshot.HistogramDataPointSnapshot> dataList) 173 throws IOException { 174 for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) { 175 ClassicHistogramBuckets buckets = getClassicBuckets(data); 176 Exemplars exemplars = data.getExemplars(); 177 long cumulativeCount = 0; 178 for (int i = 0; i < buckets.size(); i++) { 179 cumulativeCount += buckets.getCount(i); 180 writeNameAndLabels( 181 writer, 182 metadata.getPrometheusName(), 183 "_bucket", 184 data.getLabels(), 185 "le", 186 buckets.getUpperBound(i)); 187 writeLong(writer, cumulativeCount); 188 Exemplar exemplar; 189 if (i == 0) { 190 exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i)); 191 } else { 192 exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i)); 193 } 194 writeScrapeTimestampAndExemplar(writer, data, exemplar); 195 } 196 // In OpenMetrics format, histogram _count and _sum are either both present or both absent. 197 if (data.hasCount() && data.hasSum()) { 198 writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, exemplars); 199 } 200 writeCreated(writer, metadata, data); 201 } 202 } 203 204 private ClassicHistogramBuckets getClassicBuckets( 205 HistogramSnapshot.HistogramDataPointSnapshot data) { 206 if (data.getClassicBuckets().isEmpty()) { 207 return ClassicHistogramBuckets.of( 208 new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()}); 209 } else { 210 return data.getClassicBuckets(); 211 } 212 } 213 214 private void writeSummary(Writer writer, SummarySnapshot snapshot) throws IOException { 215 boolean metadataWritten = false; 216 MetricMetadata metadata = snapshot.getMetadata(); 217 for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { 218 if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { 219 continue; 220 } 221 if (!metadataWritten) { 222 writeMetadata(writer, "summary", metadata); 223 metadataWritten = true; 224 } 225 Exemplars exemplars = data.getExemplars(); 226 // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose 227 // for which 228 // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...] 229 // for the 230 // quantiles, all indexes modulo exemplars.length. 231 int exemplarIndex = 1; 232 for (Quantile quantile : data.getQuantiles()) { 233 writeNameAndLabels( 234 writer, 235 metadata.getPrometheusName(), 236 null, 237 data.getLabels(), 238 "quantile", 239 quantile.getQuantile()); 240 writeDouble(writer, quantile.getValue()); 241 if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) { 242 exemplarIndex = (exemplarIndex + 1) % exemplars.size(); 243 writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex)); 244 } else { 245 writeScrapeTimestampAndExemplar(writer, data, null); 246 } 247 } 248 // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics. 249 writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars); 250 writeCreated(writer, metadata, data); 251 } 252 } 253 254 private void writeInfo(Writer writer, InfoSnapshot snapshot) throws IOException { 255 MetricMetadata metadata = snapshot.getMetadata(); 256 writeMetadata(writer, "info", metadata); 257 for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { 258 writeNameAndLabels(writer, metadata.getPrometheusName(), "_info", data.getLabels()); 259 writer.write("1"); 260 writeScrapeTimestampAndExemplar(writer, data, null); 261 } 262 } 263 264 private void writeStateSet(Writer writer, StateSetSnapshot snapshot) throws IOException { 265 MetricMetadata metadata = snapshot.getMetadata(); 266 writeMetadata(writer, "stateset", metadata); 267 for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { 268 for (int i = 0; i < data.size(); i++) { 269 writer.write(metadata.getPrometheusName()); 270 writer.write('{'); 271 for (int j = 0; j < data.getLabels().size(); j++) { 272 if (j > 0) { 273 writer.write(","); 274 } 275 writer.write(data.getLabels().getPrometheusName(j)); 276 writer.write("=\""); 277 writeEscapedLabelValue(writer, data.getLabels().getValue(j)); 278 writer.write("\""); 279 } 280 if (!data.getLabels().isEmpty()) { 281 writer.write(","); 282 } 283 writer.write(metadata.getPrometheusName()); 284 writer.write("=\""); 285 writeEscapedLabelValue(writer, data.getName(i)); 286 writer.write("\"} "); 287 if (data.isTrue(i)) { 288 writer.write("1"); 289 } else { 290 writer.write("0"); 291 } 292 writeScrapeTimestampAndExemplar(writer, data, null); 293 } 294 } 295 } 296 297 private void writeUnknown(Writer writer, UnknownSnapshot snapshot) throws IOException { 298 MetricMetadata metadata = snapshot.getMetadata(); 299 writeMetadata(writer, "unknown", metadata); 300 for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { 301 writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels()); 302 writeDouble(writer, data.getValue()); 303 if (exemplarsOnAllMetricTypesEnabled) { 304 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar()); 305 } else { 306 writeScrapeTimestampAndExemplar(writer, data, null); 307 } 308 } 309 } 310 311 private void writeCountAndSum( 312 Writer writer, 313 MetricMetadata metadata, 314 DistributionDataPointSnapshot data, 315 String countSuffix, 316 String sumSuffix, 317 Exemplars exemplars) 318 throws IOException { 319 if (data.hasCount()) { 320 writeNameAndLabels(writer, metadata.getPrometheusName(), countSuffix, data.getLabels()); 321 writeLong(writer, data.getCount()); 322 if (exemplarsOnAllMetricTypesEnabled) { 323 writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest()); 324 } else { 325 writeScrapeTimestampAndExemplar(writer, data, null); 326 } 327 } 328 if (data.hasSum()) { 329 writeNameAndLabels(writer, metadata.getPrometheusName(), sumSuffix, data.getLabels()); 330 writeDouble(writer, data.getSum()); 331 writeScrapeTimestampAndExemplar(writer, data, null); 332 } 333 } 334 335 private void writeCreated(Writer writer, MetricMetadata metadata, DataPointSnapshot data) 336 throws IOException { 337 if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { 338 writeNameAndLabels(writer, metadata.getPrometheusName(), "_created", data.getLabels()); 339 writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); 340 if (data.hasScrapeTimestamp()) { 341 writer.write(' '); 342 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 343 } 344 writer.write('\n'); 345 } 346 } 347 348 private void writeNameAndLabels(Writer writer, String name, String suffix, Labels labels) 349 throws IOException { 350 writeNameAndLabels(writer, name, suffix, labels, null, 0.0); 351 } 352 353 private void writeNameAndLabels( 354 Writer writer, 355 String name, 356 String suffix, 357 Labels labels, 358 String additionalLabelName, 359 double additionalLabelValue) 360 throws IOException { 361 writer.write(name); 362 if (suffix != null) { 363 writer.write(suffix); 364 } 365 if (!labels.isEmpty() || additionalLabelName != null) { 366 writeLabels(writer, labels, additionalLabelName, additionalLabelValue); 367 } 368 writer.write(' '); 369 } 370 371 private void writeScrapeTimestampAndExemplar( 372 Writer writer, DataPointSnapshot data, Exemplar exemplar) throws IOException { 373 if (data.hasScrapeTimestamp()) { 374 writer.write(' '); 375 writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); 376 } 377 if (exemplar != null) { 378 writer.write(" # "); 379 writeLabels(writer, exemplar.getLabels(), null, 0); 380 writer.write(' '); 381 writeDouble(writer, exemplar.getValue()); 382 if (exemplar.hasTimestamp()) { 383 writer.write(' '); 384 writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis()); 385 } 386 } 387 writer.write('\n'); 388 } 389 390 private void writeMetadata(Writer writer, String typeName, MetricMetadata metadata) 391 throws IOException { 392 writer.write("# TYPE "); 393 writer.write(metadata.getPrometheusName()); 394 writer.write(' '); 395 writer.write(typeName); 396 writer.write('\n'); 397 if (metadata.getUnit() != null) { 398 writer.write("# UNIT "); 399 writer.write(metadata.getPrometheusName()); 400 writer.write(' '); 401 writeEscapedLabelValue(writer, metadata.getUnit().toString()); 402 writer.write('\n'); 403 } 404 if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { 405 writer.write("# HELP "); 406 writer.write(metadata.getPrometheusName()); 407 writer.write(' '); 408 writeEscapedLabelValue(writer, metadata.getHelp()); 409 writer.write('\n'); 410 } 411 } 412}