Skip to content

Commit 3f2b35b

Browse files
author
Arthur Silva Sens
committed
expfmt/openmetrics: Write created timestamps for counters, summaries and histograms
Signed-off-by: Arthur Silva Sens <[email protected]>
1 parent a3bdb9e commit 3f2b35b

File tree

2 files changed

+128
-9
lines changed

2 files changed

+128
-9
lines changed

expfmt/openmetrics_create.go

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,23 @@ import (
2323
"strings"
2424

2525
"github.com/prometheus/common/model"
26+
"google.golang.org/protobuf/types/known/timestamppb"
2627

2728
dto "github.com/prometheus/client_model/go"
2829
)
2930

31+
type toOpenMetrics struct {
32+
withCreatedLines bool
33+
}
34+
35+
type ToOpenMetricsOption func(*toOpenMetrics)
36+
37+
func WithCreatedLines() ToOpenMetricsOption {
38+
return func(t *toOpenMetrics) {
39+
t.withCreatedLines = true
40+
}
41+
}
42+
3043
// MetricFamilyToOpenMetrics converts a MetricFamily proto message into the
3144
// OpenMetrics text format and writes the resulting lines to 'out'. It returns
3245
// the number of bytes written and any error encountered. The output will have
@@ -64,15 +77,20 @@ import (
6477
// its type will be set to `unknown` in that case to avoid invalid OpenMetrics
6578
// output.
6679
//
67-
// - No support for the following (optional) features: `# UNIT` line, `_created`
68-
// line, info type, stateset type, gaugehistogram type.
80+
// - No support for the following (optional) features: `# UNIT` line, info type,
81+
// stateset type, gaugehistogram type.
6982
//
7083
// - The size of exemplar labels is not checked (i.e. it's possible to create
7184
// exemplars that are larger than allowed by the OpenMetrics specification).
7285
//
7386
// - The value of Counters is not checked. (OpenMetrics doesn't allow counters
7487
// with a `NaN` value.)
75-
func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int, err error) {
88+
func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...ToOpenMetricsOption) (written int, err error) {
89+
toOM := toOpenMetrics{}
90+
for _, option := range options {
91+
option(&toOM)
92+
}
93+
7694
name := in.GetName()
7795
if name == "" {
7896
return 0, fmt.Errorf("MetricFamily has no name: %s", in)
@@ -164,6 +182,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
164182
return
165183
}
166184

185+
var createdTsBytesWritten int
167186
// Finally the samples, one line for each.
168187
for _, metric := range in.Metric {
169188
switch metricType {
@@ -181,6 +200,10 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
181200
metric.Counter.GetValue(), 0, false,
182201
metric.Counter.Exemplar,
183202
)
203+
if toOM.withCreatedLines && metric.Counter.CreatedTimestamp != nil {
204+
createdTsBytesWritten, err = writeOpenMetricsCreated(w, name, "_total", metric, "", 0, metric.Counter.GetCreatedTimestamp())
205+
n += createdTsBytesWritten
206+
}
184207
case dto.MetricType_GAUGE:
185208
if metric.Gauge == nil {
186209
return written, fmt.Errorf(
@@ -235,6 +258,10 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
235258
0, metric.Summary.GetSampleCount(), true,
236259
nil,
237260
)
261+
if metric.Summary.CreatedTimestamp != nil {
262+
createdTsBytesWritten, err = writeOpenMetricsCreated(w, name, "", metric, "", 0, metric.Summary.GetCreatedTimestamp())
263+
n += createdTsBytesWritten
264+
}
238265
case dto.MetricType_HISTOGRAM:
239266
if metric.Histogram == nil {
240267
return written, fmt.Errorf(
@@ -283,6 +310,10 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
283310
0, metric.Histogram.GetSampleCount(), true,
284311
nil,
285312
)
313+
if metric.Histogram.CreatedTimestamp != nil {
314+
createdTsBytesWritten, err = writeOpenMetricsCreated(w, name, "", metric, "", 0, metric.Histogram.GetCreatedTimestamp())
315+
n += createdTsBytesWritten
316+
}
286317
default:
287318
return written, fmt.Errorf(
288319
"unexpected type in metric %s %s", name, metric,
@@ -473,6 +504,62 @@ func writeOpenMetricsNameAndLabelPairs(
473504
return written, nil
474505
}
475506

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

expfmt/openmetrics_create_test.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ func TestCreateOpenMetrics(t *testing.T) {
3232
t.Error(err)
3333
}
3434

35-
scenarios := []struct {
36-
in *dto.MetricFamily
37-
out string
35+
var scenarios = []struct {
36+
in *dto.MetricFamily
37+
options []ToOpenMetricsOption
38+
out string
3839
}{
3940
// 0: Counter, timestamp given, no _total suffix.
4041
{
@@ -298,6 +299,7 @@ unknown_name{name_1="value 1"} -1.23e-45
298299
Value: proto.Float64(0),
299300
},
300301
},
302+
CreatedTimestamp: openMetricsTimestamp,
301303
},
302304
},
303305
{
@@ -328,22 +330,26 @@ unknown_name{name_1="value 1"} -1.23e-45
328330
Value: proto.Float64(3),
329331
},
330332
},
333+
CreatedTimestamp: openMetricsTimestamp,
331334
},
332335
},
333336
},
334337
},
338+
options: []ToOpenMetricsOption{WithCreatedLines()},
335339
out: `# HELP summary_name summary docstring
336340
# TYPE summary_name summary
337341
summary_name{quantile="0.5"} -1.23
338342
summary_name{quantile="0.9"} 0.2342354
339343
summary_name{quantile="0.99"} 0.0
340344
summary_name_sum -3.4567
341345
summary_name_count 42
346+
summary_name_created 12345.6
342347
summary_name{name_1="value 1",name_2="value 2",quantile="0.5"} 1.0
343348
summary_name{name_1="value 1",name_2="value 2",quantile="0.9"} 2.0
344349
summary_name{name_1="value 1",name_2="value 2",quantile="0.99"} 3.0
345350
summary_name_sum{name_1="value 1",name_2="value 2"} 2010.1971
346351
summary_name_count{name_1="value 1",name_2="value 2"} 4711
352+
summary_name_created{name_1="value 1",name_2="value 2"} 12345.6
347353
`,
348354
},
349355
// 7: Histogram
@@ -379,10 +385,12 @@ summary_name_count{name_1="value 1",name_2="value 2"} 4711
379385
CumulativeCount: proto.Uint64(2693),
380386
},
381387
},
388+
CreatedTimestamp: openMetricsTimestamp,
382389
},
383390
},
384391
},
385392
},
393+
options: []ToOpenMetricsOption{WithCreatedLines()},
386394
out: `# HELP request_duration_microseconds The response latency.
387395
# TYPE request_duration_microseconds histogram
388396
request_duration_microseconds_bucket{le="100.0"} 123
@@ -392,6 +400,7 @@ request_duration_microseconds_bucket{le="172.8"} 1524
392400
request_duration_microseconds_bucket{le="+Inf"} 2693
393401
request_duration_microseconds_sum 1.7560473e+06
394402
request_duration_microseconds_count 2693
403+
request_duration_microseconds_created 12345.6
395404
`,
396405
},
397406
// 8: Histogram with missing +Inf bucket.
@@ -514,7 +523,30 @@ request_duration_microseconds_count 2693
514523
Metric: []*dto.Metric{
515524
{
516525
Counter: &dto.Counter{
517-
Value: proto.Float64(42),
526+
Value: proto.Float64(42),
527+
CreatedTimestamp: openMetricsTimestamp,
528+
},
529+
},
530+
},
531+
},
532+
options: []ToOpenMetricsOption{WithCreatedLines()},
533+
out: `# HELP foos Number of foos.
534+
# TYPE foos counter
535+
foos_total 42.0
536+
foos_created 12345.6
537+
`,
538+
},
539+
// 11: Simple Counter without created line.
540+
{
541+
in: &dto.MetricFamily{
542+
Name: proto.String("foos_total"),
543+
Help: proto.String("Number of foos."),
544+
Type: dto.MetricType_COUNTER.Enum(),
545+
Metric: []*dto.Metric{
546+
&dto.Metric{
547+
Counter: &dto.Counter{
548+
Value: proto.Float64(42),
549+
CreatedTimestamp: openMetricsTimestamp,
518550
},
519551
},
520552
},
@@ -524,7 +556,7 @@ request_duration_microseconds_count 2693
524556
foos_total 42.0
525557
`,
526558
},
527-
// 11: No metric.
559+
// 12: No metric.
528560
{
529561
in: &dto.MetricFamily{
530562
Name: proto.String("name_total"),
@@ -540,7 +572,7 @@ foos_total 42.0
540572

541573
for i, scenario := range scenarios {
542574
out := bytes.NewBuffer(make([]byte, 0, len(scenario.out)))
543-
n, err := MetricFamilyToOpenMetrics(out, scenario.in)
575+
n, err := MetricFamilyToOpenMetrics(out, scenario.in, scenario.options...)
544576
if err != nil {
545577
t.Errorf("%d. error: %s", i, err)
546578
continue

0 commit comments

Comments
 (0)