Skip to content

Commit 48d2e18

Browse files
author
Arthur Silva Sens
authored
Write created lines when negotiating OpenMetrics (#504)
* expfmt/openmetrics: Write created timestamps for counters, summaries and histograms * expfmt/encoder: Allow opt-in for OM created lines --------- Signed-off-by: Arthur Silva Sens <[email protected]>
1 parent b27d4bf commit 48d2e18

File tree

3 files changed

+133
-10
lines changed

3 files changed

+133
-10
lines changed

expfmt/encode.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,13 @@ func NegotiateIncludingOpenMetrics(h http.Header) Format {
139139
// interface is kept for backwards compatibility.
140140
// In cases where the Format does not allow for UTF-8 names, the global
141141
// NameEscapingScheme will be applied.
142-
func NewEncoder(w io.Writer, format Format) Encoder {
142+
//
143+
// NewEncoder can be called with additional options to customize the OpenMetrics text output.
144+
// For example:
145+
// NewEncoder(w, FmtOpenMetrics_1_0_0, WithCreatedLines())
146+
//
147+
// Extra options are ignored for all other formats.
148+
func NewEncoder(w io.Writer, format Format, options ...EncoderOption) Encoder {
143149
escapingScheme := format.ToEscapingScheme()
144150

145151
switch format.FormatType() {
@@ -178,7 +184,7 @@ func NewEncoder(w io.Writer, format Format) Encoder {
178184
case TypeOpenMetrics:
179185
return encoderCloser{
180186
encode: func(v *dto.MetricFamily) error {
181-
_, err := MetricFamilyToOpenMetrics(w, model.EscapeMetricFamily(v, escapingScheme))
187+
_, err := MetricFamilyToOpenMetrics(w, model.EscapeMetricFamily(v, escapingScheme), options...)
182188
return err
183189
},
184190
close: func() error {

expfmt/openmetrics_create.go

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,35 @@ import (
2222
"strconv"
2323
"strings"
2424

25+
"google.golang.org/protobuf/types/known/timestamppb"
26+
2527
"github.com/prometheus/common/model"
2628

2729
dto "github.com/prometheus/client_model/go"
2830
)
2931

32+
type encoderOption struct {
33+
withCreatedLines bool
34+
}
35+
36+
type EncoderOption func(*encoderOption)
37+
38+
// WithCreatedLines is an EncoderOption that configures the OpenMetrics encoder
39+
// to include _created lines (See
40+
// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#counter-1).
41+
// Created timestamps can improve the accuracy of series reset detection, but
42+
// come with a bandwidth cost.
43+
//
44+
// At the time of writing, created timestamp ingestion is still experimental in
45+
// Prometheus and need to be enabled with the feature-flag
46+
// `--feature-flag=created-timestamp-zero-ingestion`, and breaking changes are
47+
// still possible. Therefore, it is recommended to use this feature with caution.
48+
func WithCreatedLines() EncoderOption {
49+
return func(t *encoderOption) {
50+
t.withCreatedLines = true
51+
}
52+
}
53+
3054
// MetricFamilyToOpenMetrics converts a MetricFamily proto message into the
3155
// OpenMetrics text format and writes the resulting lines to 'out'. It returns
3256
// the number of bytes written and any error encountered. The output will have
@@ -64,15 +88,20 @@ import (
6488
// its type will be set to `unknown` in that case to avoid invalid OpenMetrics
6589
// output.
6690
//
67-
// - No support for the following (optional) features: `# UNIT` line, `_created`
68-
// line, info type, stateset type, gaugehistogram type.
91+
// - No support for the following (optional) features: `# UNIT` line, info type,
92+
// stateset type, gaugehistogram type.
6993
//
7094
// - The size of exemplar labels is not checked (i.e. it's possible to create
7195
// exemplars that are larger than allowed by the OpenMetrics specification).
7296
//
7397
// - The value of Counters is not checked. (OpenMetrics doesn't allow counters
7498
// with a `NaN` value.)
75-
func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int, err error) {
99+
func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...EncoderOption) (written int, err error) {
100+
toOM := encoderOption{}
101+
for _, option := range options {
102+
option(&toOM)
103+
}
104+
76105
name := in.GetName()
77106
if name == "" {
78107
return 0, fmt.Errorf("MetricFamily has no name: %s", in)
@@ -164,6 +193,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
164193
return
165194
}
166195

196+
var createdTsBytesWritten int
167197
// Finally the samples, one line for each.
168198
for _, metric := range in.Metric {
169199
switch metricType {
@@ -181,6 +211,10 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
181211
metric.Counter.GetValue(), 0, false,
182212
metric.Counter.Exemplar,
183213
)
214+
if toOM.withCreatedLines && metric.Counter.CreatedTimestamp != nil {
215+
createdTsBytesWritten, err = writeOpenMetricsCreated(w, name, "_total", metric, "", 0, metric.Counter.GetCreatedTimestamp())
216+
n += createdTsBytesWritten
217+
}
184218
case dto.MetricType_GAUGE:
185219
if metric.Gauge == nil {
186220
return written, fmt.Errorf(
@@ -235,6 +269,10 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
235269
0, metric.Summary.GetSampleCount(), true,
236270
nil,
237271
)
272+
if toOM.withCreatedLines && metric.Summary.CreatedTimestamp != nil {
273+
createdTsBytesWritten, err = writeOpenMetricsCreated(w, name, "", metric, "", 0, metric.Summary.GetCreatedTimestamp())
274+
n += createdTsBytesWritten
275+
}
238276
case dto.MetricType_HISTOGRAM:
239277
if metric.Histogram == nil {
240278
return written, fmt.Errorf(
@@ -283,6 +321,10 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
283321
0, metric.Histogram.GetSampleCount(), true,
284322
nil,
285323
)
324+
if toOM.withCreatedLines && metric.Histogram.CreatedTimestamp != nil {
325+
createdTsBytesWritten, err = writeOpenMetricsCreated(w, name, "", metric, "", 0, metric.Histogram.GetCreatedTimestamp())
326+
n += createdTsBytesWritten
327+
}
286328
default:
287329
return written, fmt.Errorf(
288330
"unexpected type in metric %s %s", name, metric,
@@ -473,6 +515,49 @@ func writeOpenMetricsNameAndLabelPairs(
473515
return written, nil
474516
}
475517

518+
// writeOpenMetricsCreated writes the created timestamp for a single time series
519+
// following OpenMetrics text format to w, given the metric name, the metric proto
520+
// message itself, optionally a suffix to be removed, e.g. '_total' for counters,
521+
// an additional label name with a float64 value (use empty string as label name if
522+
// not required) and the timestamp that represents the created timestamp.
523+
// The function returns the number of bytes written and any error encountered.
524+
func writeOpenMetricsCreated(w enhancedWriter,
525+
name, suffixToTrim string, metric *dto.Metric,
526+
additionalLabelName string, additionalLabelValue float64,
527+
createdTimestamp *timestamppb.Timestamp,
528+
) (int, error) {
529+
written := 0
530+
n, err := writeOpenMetricsNameAndLabelPairs(
531+
w, strings.TrimSuffix(name, suffixToTrim)+"_created", metric.Label, additionalLabelName, additionalLabelValue,
532+
)
533+
written += n
534+
if err != nil {
535+
return written, err
536+
}
537+
538+
err = w.WriteByte(' ')
539+
written++
540+
if err != nil {
541+
return written, err
542+
}
543+
544+
// TODO(beorn7): Format this directly from components of ts to
545+
// avoid overflow/underflow and precision issues of the float
546+
// conversion.
547+
n, err = writeOpenMetricsFloat(w, float64(createdTimestamp.AsTime().UnixNano())/1e9)
548+
written += n
549+
if err != nil {
550+
return written, err
551+
}
552+
553+
err = w.WriteByte('\n')
554+
written++
555+
if err != nil {
556+
return written, err
557+
}
558+
return written, nil
559+
}
560+
476561
// writeExemplar writes the provided exemplar in OpenMetrics format to w. The
477562
// function returns the number of bytes written and any error encountered.
478563
func writeExemplar(w enhancedWriter, e *dto.Exemplar) (int, error) {

expfmt/openmetrics_create_test.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ func TestCreateOpenMetrics(t *testing.T) {
4141
}()
4242

4343
scenarios := []struct {
44-
in *dto.MetricFamily
45-
out string
44+
in *dto.MetricFamily
45+
options []EncoderOption
46+
out string
4647
}{
4748
// 0: Counter, timestamp given, no _total suffix.
4849
{
@@ -306,6 +307,7 @@ unknown_name{name_1="value 1"} -1.23e-45
306307
Value: proto.Float64(0),
307308
},
308309
},
310+
CreatedTimestamp: openMetricsTimestamp,
309311
},
310312
},
311313
{
@@ -336,22 +338,26 @@ unknown_name{name_1="value 1"} -1.23e-45
336338
Value: proto.Float64(3),
337339
},
338340
},
341+
CreatedTimestamp: openMetricsTimestamp,
339342
},
340343
},
341344
},
342345
},
346+
options: []EncoderOption{WithCreatedLines()},
343347
out: `# HELP summary_name summary docstring
344348
# TYPE summary_name summary
345349
summary_name{quantile="0.5"} -1.23
346350
summary_name{quantile="0.9"} 0.2342354
347351
summary_name{quantile="0.99"} 0.0
348352
summary_name_sum -3.4567
349353
summary_name_count 42
354+
summary_name_created 12345.6
350355
summary_name{name_1="value 1",name_2="value 2",quantile="0.5"} 1.0
351356
summary_name{name_1="value 1",name_2="value 2",quantile="0.9"} 2.0
352357
summary_name{name_1="value 1",name_2="value 2",quantile="0.99"} 3.0
353358
summary_name_sum{name_1="value 1",name_2="value 2"} 2010.1971
354359
summary_name_count{name_1="value 1",name_2="value 2"} 4711
360+
summary_name_created{name_1="value 1",name_2="value 2"} 12345.6
355361
`,
356362
},
357363
// 7: Histogram
@@ -387,10 +393,12 @@ summary_name_count{name_1="value 1",name_2="value 2"} 4711
387393
CumulativeCount: proto.Uint64(2693),
388394
},
389395
},
396+
CreatedTimestamp: openMetricsTimestamp,
390397
},
391398
},
392399
},
393400
},
401+
options: []EncoderOption{WithCreatedLines()},
394402
out: `# HELP request_duration_microseconds The response latency.
395403
# TYPE request_duration_microseconds histogram
396404
request_duration_microseconds_bucket{le="100.0"} 123
@@ -400,6 +408,7 @@ request_duration_microseconds_bucket{le="172.8"} 1524
400408
request_duration_microseconds_bucket{le="+Inf"} 2693
401409
request_duration_microseconds_sum 1.7560473e+06
402410
request_duration_microseconds_count 2693
411+
request_duration_microseconds_created 12345.6
403412
`,
404413
},
405414
// 8: Histogram with missing +Inf bucket.
@@ -522,7 +531,30 @@ request_duration_microseconds_count 2693
522531
Metric: []*dto.Metric{
523532
{
524533
Counter: &dto.Counter{
525-
Value: proto.Float64(42),
534+
Value: proto.Float64(42),
535+
CreatedTimestamp: openMetricsTimestamp,
536+
},
537+
},
538+
},
539+
},
540+
options: []EncoderOption{WithCreatedLines()},
541+
out: `# HELP foos Number of foos.
542+
# TYPE foos counter
543+
foos_total 42.0
544+
foos_created 12345.6
545+
`,
546+
},
547+
// 11: Simple Counter without created line.
548+
{
549+
in: &dto.MetricFamily{
550+
Name: proto.String("foos_total"),
551+
Help: proto.String("Number of foos."),
552+
Type: dto.MetricType_COUNTER.Enum(),
553+
Metric: []*dto.Metric{
554+
{
555+
Counter: &dto.Counter{
556+
Value: proto.Float64(42),
557+
CreatedTimestamp: openMetricsTimestamp,
526558
},
527559
},
528560
},
@@ -532,7 +564,7 @@ request_duration_microseconds_count 2693
532564
foos_total 42.0
533565
`,
534566
},
535-
// 11: No metric.
567+
// 12: No metric.
536568
{
537569
in: &dto.MetricFamily{
538570
Name: proto.String("name_total"),
@@ -573,7 +605,7 @@ foos_total 42.0
573605

574606
for i, scenario := range scenarios {
575607
out := bytes.NewBuffer(make([]byte, 0, len(scenario.out)))
576-
n, err := MetricFamilyToOpenMetrics(out, scenario.in)
608+
n, err := MetricFamilyToOpenMetrics(out, scenario.in, scenario.options...)
577609
if err != nil {
578610
t.Errorf("%d. error: %s", i, err)
579611
continue

0 commit comments

Comments
 (0)