Skip to content

Commit 2128d87

Browse files
committed
feat: compositeValues and exemplarCompliance flags for OM2 writer
- compositeValues=true: write Histogram/GaugeHistogram/Summary as a single composite-value line per the OM2 spec, e.g. foo {count:17,sum:324789.3,bucket:[0.1:8,0.25:10,+Inf:17]} st@0.5 GaugeHistogram uses gcount/gsum per spec. Replaces separate _bucket, _count, _sum, _created lines; created timestamp moves inline as st@. Exemplar (latest) is appended inline when present. - exemplarCompliance=true: skip exemplars without a timestamp, as the OM2 spec mandates timestamps on all exemplars (MUST). Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
1 parent 0fa1ad7 commit 2128d87

2 files changed

Lines changed: 391 additions & 32 deletions

File tree

prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java

Lines changed: 158 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,67 @@ private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingS
204204
String name = getExpositionBaseMetadataName(metadata, scheme);
205205
if (snapshot.isGaugeHistogram()) {
206206
writeMetadataWithName(writer, name, "gaugehistogram", metadata);
207-
writeClassicHistogramBuckets(
208-
writer, name, "_gcount", "_gsum", snapshot.getDataPoints(), scheme);
207+
if (openMetrics2Properties.getCompositeValues()) {
208+
for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
209+
writeCompositeHistogramDataPoint(writer, name, "gcount", "gsum", data, scheme);
210+
}
211+
} else {
212+
writeClassicHistogramBuckets(
213+
writer, name, "_gcount", "_gsum", snapshot.getDataPoints(), scheme);
214+
}
209215
} else {
210216
writeMetadataWithName(writer, name, "histogram", metadata);
211-
writeClassicHistogramBuckets(
212-
writer, name, "_count", "_sum", snapshot.getDataPoints(), scheme);
217+
if (openMetrics2Properties.getCompositeValues()) {
218+
for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
219+
writeCompositeHistogramDataPoint(writer, name, "count", "sum", data, scheme);
220+
}
221+
} else {
222+
writeClassicHistogramBuckets(
223+
writer, name, "_count", "_sum", snapshot.getDataPoints(), scheme);
224+
}
225+
}
226+
}
227+
228+
private void writeCompositeHistogramDataPoint(
229+
Writer writer,
230+
String name,
231+
String countKey,
232+
String sumKey,
233+
HistogramSnapshot.HistogramDataPointSnapshot data,
234+
EscapingScheme scheme)
235+
throws IOException {
236+
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
237+
writer.write('{');
238+
writer.write(countKey);
239+
writer.write(':');
240+
writeLong(writer, data.getCount());
241+
writer.write(',');
242+
writer.write(sumKey);
243+
writer.write(':');
244+
writeDouble(writer, data.getSum());
245+
writer.write(",bucket:[");
246+
ClassicHistogramBuckets buckets = getClassicBuckets(data);
247+
long cumulativeCount = 0;
248+
for (int i = 0; i < buckets.size(); i++) {
249+
if (i > 0) {
250+
writer.write(',');
251+
}
252+
cumulativeCount += buckets.getCount(i);
253+
writeDouble(writer, buckets.getUpperBound(i));
254+
writer.write(':');
255+
writeLong(writer, cumulativeCount);
256+
}
257+
writer.write("]}");
258+
if (data.hasScrapeTimestamp()) {
259+
writer.write(' ');
260+
writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
261+
}
262+
if (data.hasCreatedTimestamp()) {
263+
writer.write(" st@");
264+
writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
213265
}
266+
writeExemplarIfAllowed(writer, data.getExemplars().getLatest(), scheme);
267+
writer.write('\n');
214268
}
215269

216270
private void writeClassicHistogramBuckets(
@@ -269,28 +323,90 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem
269323
writeMetadataWithName(writer, name, "summary", metadata);
270324
metadataWritten = true;
271325
}
272-
Exemplars exemplars = data.getExemplars();
273-
// Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose
274-
// for which
275-
// time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...]
276-
// for the
277-
// quantiles, all indexes modulo exemplars.length.
278-
int exemplarIndex = 1;
279-
for (Quantile quantile : data.getQuantiles()) {
280-
writeNameAndLabels(
281-
writer, name, null, data.getLabels(), scheme, "quantile", quantile.getQuantile());
282-
writeDouble(writer, quantile.getValue());
283-
if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) {
284-
exemplarIndex = (exemplarIndex + 1) % exemplars.size();
285-
writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex), scheme);
286-
} else {
287-
writeScrapeTimestampAndExemplar(writer, data, null, scheme);
326+
if (openMetrics2Properties.getCompositeValues()) {
327+
writeCompositeSummaryDataPoint(writer, name, data, scheme);
328+
} else {
329+
writeNonCompositeSummaryDataPoint(writer, name, data, scheme);
330+
}
331+
}
332+
}
333+
334+
private void writeCompositeSummaryDataPoint(
335+
Writer writer,
336+
String name,
337+
SummarySnapshot.SummaryDataPointSnapshot data,
338+
EscapingScheme scheme)
339+
throws IOException {
340+
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
341+
writer.write('{');
342+
boolean first = true;
343+
if (data.hasCount()) {
344+
writer.write("count:");
345+
writeLong(writer, data.getCount());
346+
first = false;
347+
}
348+
if (data.hasSum()) {
349+
if (!first) {
350+
writer.write(',');
351+
}
352+
writer.write("sum:");
353+
writeDouble(writer, data.getSum());
354+
first = false;
355+
}
356+
if (data.getQuantiles().size() > 0) {
357+
if (!first) {
358+
writer.write(',');
359+
}
360+
writer.write("quantile:[");
361+
for (int i = 0; i < data.getQuantiles().size(); i++) {
362+
if (i > 0) {
363+
writer.write(',');
288364
}
365+
Quantile q = data.getQuantiles().get(i);
366+
writeDouble(writer, q.getQuantile());
367+
writer.write(':');
368+
writeDouble(writer, q.getValue());
369+
}
370+
writer.write(']');
371+
}
372+
writer.write('}');
373+
if (data.hasScrapeTimestamp()) {
374+
writer.write(' ');
375+
writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
376+
}
377+
if (data.hasCreatedTimestamp()) {
378+
writer.write(" st@");
379+
writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
380+
}
381+
writeExemplarIfAllowed(writer, data.getExemplars().getLatest(), scheme);
382+
writer.write('\n');
383+
}
384+
385+
private void writeNonCompositeSummaryDataPoint(
386+
Writer writer,
387+
String name,
388+
SummarySnapshot.SummaryDataPointSnapshot data,
389+
EscapingScheme scheme)
390+
throws IOException {
391+
Exemplars exemplars = data.getExemplars();
392+
// Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose
393+
// for which time series. We select exemplars[0] for _count, exemplars[1] for _sum, and
394+
// exemplars[2...] for the quantiles, all indexes modulo exemplars.length.
395+
int exemplarIndex = 1;
396+
for (Quantile quantile : data.getQuantiles()) {
397+
writeNameAndLabels(
398+
writer, name, null, data.getLabels(), scheme, "quantile", quantile.getQuantile());
399+
writeDouble(writer, quantile.getValue());
400+
if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) {
401+
exemplarIndex = (exemplarIndex + 1) % exemplars.size();
402+
writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex), scheme);
403+
} else {
404+
writeScrapeTimestampAndExemplar(writer, data, null, scheme);
289405
}
290-
// Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics.
291-
writeCountAndSum(writer, name, data, "_count", "_sum", exemplars, scheme);
292-
writeCreated(writer, name, data, scheme);
293406
}
407+
// Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics.
408+
writeCountAndSum(writer, name, data, "_count", "_sum", exemplars, scheme);
409+
writeCreated(writer, name, data, scheme);
294410
}
295411

296412
private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme)
@@ -446,17 +562,27 @@ private void writeScrapeTimestampAndExemplar(
446562
writer.write(' ');
447563
writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
448564
}
449-
if (exemplar != null) {
450-
writer.write(" # ");
451-
writeLabels(writer, exemplar.getLabels(), null, 0, false, scheme);
565+
writeExemplarIfAllowed(writer, exemplar, scheme);
566+
writer.write('\n');
567+
}
568+
569+
private void writeExemplarIfAllowed(
570+
Writer writer, @Nullable Exemplar exemplar, EscapingScheme scheme) throws IOException {
571+
if (exemplar == null) {
572+
return;
573+
}
574+
// In exemplarCompliance mode, exemplars MUST have a timestamp per the OM2 spec.
575+
if (openMetrics2Properties.getExemplarCompliance() && !exemplar.hasTimestamp()) {
576+
return;
577+
}
578+
writer.write(" # ");
579+
writeLabels(writer, exemplar.getLabels(), null, 0, false, scheme);
580+
writer.write(' ');
581+
writeDouble(writer, exemplar.getValue());
582+
if (exemplar.hasTimestamp()) {
452583
writer.write(' ');
453-
writeDouble(writer, exemplar.getValue());
454-
if (exemplar.hasTimestamp()) {
455-
writer.write(' ');
456-
writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis());
457-
}
584+
writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis());
458585
}
459-
writer.write('\n');
460586
}
461587

462588
private void writeMetadataWithName(

0 commit comments

Comments
 (0)