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