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 if (data.hasCount() && data.hasSum()) { 142 // In OpenMetrics format, histogram _count and _sum are either both present or both absent. 143 // While Prometheus allows Exemplars for histogram's _count and _sum now, we don't 144 // use Exemplars here to be backwards compatible with previous behavior. 145 writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, Exemplars.EMPTY); 146 } 147 writeCreated(writer, metadata, data); 148 } 149 } 150 151 private ClassicHistogramBuckets getClassicBuckets(HistogramSnapshot.HistogramDataPointSnapshot data) { 152 if (data.getClassicBuckets().isEmpty()) { 153 return ClassicHistogramBuckets.of( 154 new double[]{Double.POSITIVE_INFINITY}, 155 new long[]{data.getCount()} 156 ); 157 } else { 158 return data.getClassicBuckets(); 159 } 160 } 161 162 private void writeSummary(OutputStreamWriter writer, SummarySnapshot snapshot) throws IOException { 163 boolean metadataWritten = false; 164 MetricMetadata metadata = snapshot.getMetadata(); 165 for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) { 166 if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) { 167 continue; 168 } 169 if (!metadataWritten) { 170 writeMetadata(writer, "summary", metadata); 171 metadataWritten = true; 172 } 173 Exemplars exemplars = data.getExemplars(); 174 // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose for which 175 // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...] for the 176 // quantiles, all indexes modulo exemplars.length. 177 int exemplarIndex = 1; 178 for (Quantile quantile : data.getQuantiles()) { 179 writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels(), "quantile", quantile.getQuantile()); 180 writeDouble(writer, quantile.getValue()); 181 if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) { 182 exemplarIndex = (exemplarIndex + 1) % exemplars.size(); 183 writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex)); 184 } else { 185 writeScrapeTimestampAndExemplar(writer, data, null); 186 } 187 } 188 // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics. 189 writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars); 190 writeCreated(writer, metadata, data); 191 } 192 } 193 194 private void writeInfo(OutputStreamWriter writer, InfoSnapshot snapshot) throws IOException { 195 MetricMetadata metadata = snapshot.getMetadata(); 196 writeMetadata(writer, "info", metadata); 197 for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) { 198 writeNameAndLabels(writer, metadata.getPrometheusName(), "_info", data.getLabels()); 199 writer.write("1"); 200 writeScrapeTimestampAndExemplar(writer, data, null); 201 } 202 } 203 204 private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot) throws IOException { 205 MetricMetadata metadata = snapshot.getMetadata(); 206 writeMetadata(writer, "stateset", metadata); 207 for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) { 208 for (int i = 0; i < data.size(); i++) { 209 writer.write(metadata.getPrometheusName()); 210 writer.write('{'); 211 for (int j = 0; j < data.getLabels().size(); j++) { 212 if (j > 0) { 213 writer.write(","); 214 } 215 writer.write(data.getLabels().getPrometheusName(j)); 216 writer.write("=\""); 217 writeEscapedLabelValue(writer, data.getLabels().getValue(j)); 218 writer.write("\""); 219 } 220 if (!data.getLabels().isEmpty()) { 221 writer.write(","); 222 } 223 writer.write(metadata.getPrometheusName()); 224 writer.write("=\""); 225 writeEscapedLabelValue(writer, data.getName(i)); 226 writer.write("\"} "); 227 if (data.isTrue(i)) { 228 writer.write("1"); 229 } else { 230 writer.write("0"); 231 } 232 writeScrapeTimestampAndExemplar(writer, data, null); 233 } 234 } 235 } 236 237 private void writeUnknown(OutputStreamWriter writer, UnknownSnapshot snapshot) throws IOException { 238 MetricMetadata metadata = snapshot.getMetadata(); 239 writeMetadata(writer, "unknown", metadata); 240 for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) { 241 writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels()); 242 writeDouble(writer, data.getValue()); 243 if (exemplarsOnAllMetricTypesEnabled) { 244 writeScrapeTimestampAndExemplar(writer, data, data.getExemplar()); 245 } else { 246 writeScrapeTimestampAndExemplar(writer, data, null); 247 } 248 } 249 } 250 251 private void writeCountAndSum(OutputStreamWriter writer, MetricMetadata metadata, DistributionDataPointSnapshot data, String countSuffix, String sumSuffix, Exemplars exemplars) throws IOException { 252 int exemplarIndex = 0; 253 if (data.hasCount()) { 254 writeNameAndLabels(writer, metadata.getPrometheusName(), countSuffix, data.getLabels()); 255 writeLong(writer, data.getCount()); 256 if (exemplars.size() > 0) { 257 writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex)); 258 exemplarIndex = exemplarIndex + 1 % exemplars.size(); 259 } else { 260 writeScrapeTimestampAndExemplar(writer, data, null); 261 } 262 } 263 if (data.hasSum()) { 264 writeNameAndLabels(writer, metadata.getPrometheusName(), sumSuffix, data.getLabels()); 265 writeDouble(writer, data.getSum()); 266 if (exemplars.size() > 0) { 267 writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex)); 268 } else { 269 writeScrapeTimestampAndExemplar(writer, data, null); 270 } 271 } 272 } 273 274 private void writeCreated(OutputStreamWriter writer, MetricMetadata metadata, DataPointSnapshot data) throws IOException { 275 if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { 276 writeNameAndLabels(writer, metadata.getPrometheusName(), "_created", data.getLabels()); 277 writeTimestamp(writer, data.getCreatedTimestampMillis()); 278 if (data.hasScrapeTimestamp()) { 279 writer.write(' '); 280 writeTimestamp(writer, data.getScrapeTimestampMillis()); 281 } 282 writer.write('\n'); 283 } 284 } 285 286 private void writeNameAndLabels(OutputStreamWriter writer, String name, String suffix, Labels labels) throws IOException { 287 writeNameAndLabels(writer, name, suffix, labels, null, 0.0); 288 } 289 290 private void writeNameAndLabels(OutputStreamWriter writer, String name, String suffix, Labels labels, 291 String additionalLabelName, double additionalLabelValue) throws IOException { 292 writer.write(name); 293 if (suffix != null) { 294 writer.write(suffix); 295 } 296 if (!labels.isEmpty() || additionalLabelName != null) { 297 writeLabels(writer, labels, additionalLabelName, additionalLabelValue); 298 } 299 writer.write(' '); 300 } 301 302 private void writeScrapeTimestampAndExemplar(OutputStreamWriter writer, DataPointSnapshot data, Exemplar exemplar) throws IOException { 303 if (data.hasScrapeTimestamp()) { 304 writer.write(' '); 305 writeTimestamp(writer, data.getScrapeTimestampMillis()); 306 } 307 if (exemplar != null) { 308 writer.write(" # "); 309 writeLabels(writer, exemplar.getLabels(), null, 0); 310 writer.write(' '); 311 writeDouble(writer, exemplar.getValue()); 312 if (exemplar.hasTimestamp()) { 313 writer.write(' '); 314 writeTimestamp(writer, exemplar.getTimestampMillis()); 315 } 316 } 317 writer.write('\n'); 318 } 319 320 private void writeMetadata(OutputStreamWriter writer, String typeName, MetricMetadata metadata) throws IOException { 321 writer.write("# TYPE "); 322 writer.write(metadata.getPrometheusName()); 323 writer.write(' '); 324 writer.write(typeName); 325 writer.write('\n'); 326 if (metadata.getUnit() != null) { 327 writer.write("# UNIT "); 328 writer.write(metadata.getPrometheusName()); 329 writer.write(' '); 330 writeEscapedLabelValue(writer, metadata.getUnit().toString()); 331 writer.write('\n'); 332 } 333 if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) { 334 writer.write("# HELP "); 335 writer.write(metadata.getPrometheusName()); 336 writer.write(' '); 337 writeEscapedLabelValue(writer, metadata.getHelp()); 338 writer.write('\n'); 339 } 340 } 341}