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