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