Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 19 additions & 29 deletions expfmt/openmetrics_2_0_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,17 +173,17 @@ func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ..
if val < 0 {
return written, fmt.Errorf("counter value cannot be negative (%g) in metric %s", val, name)
}
n, err = writeOpenMetrics20Sample(w, name, metric, val, 0, false, metric.Counter.Exemplar)
n, err = writeOpenMetrics20Sample(w, name, metric, val, 0, false, metric.Counter.CreatedTimestamp, metric.Counter.Exemplar)
case dto.MetricType_GAUGE:
if metric.Gauge == nil {
return written, fmt.Errorf("expected gauge in metric %s %s", name, metric)
}
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Gauge.GetValue(), 0, false, nil)
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Gauge.GetValue(), 0, false, nil, nil)
case dto.MetricType_UNTYPED:
if metric.Untyped == nil {
return written, fmt.Errorf("expected untyped in metric %s %s", name, metric)
}
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Untyped.GetValue(), 0, false, nil)
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Untyped.GetValue(), 0, false, nil, nil)
case dto.MetricType_SUMMARY:
if metric.Summary == nil {
return written, fmt.Errorf("expected summary in metric %s %s", name, metric)
Expand All @@ -206,7 +206,7 @@ func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ..
}

// writeOpenMetrics20Sample writes a single sample for simple types (Counter, Gauge, Untyped).
func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, floatValue float64, intValue uint64, useIntValue bool, exemplar *dto.Exemplar) (int, error) {
func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, floatValue float64, intValue uint64, useIntValue bool, startTimestamp *timestamppb.Timestamp, exemplar *dto.Exemplar) (int, error) {
if err := validateLabels20(metric.Label); err != nil {
return 0, err
}
Expand Down Expand Up @@ -245,18 +245,17 @@ func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric,
}
}

// Start Timestamp for Counter
if metric.Counter != nil && metric.Counter.CreatedTimestamp != nil {
ts := metric.Counter.CreatedTimestamp
if err := ts.CheckValid(); err != nil {
// Start Timestamp
if startTimestamp != nil {
if err := startTimestamp.CheckValid(); err != nil {
return written, fmt.Errorf("invalid created timestamp in metric %s: %w", name, err)
}
n, err = w.WriteString(" st@")
written += n
if err != nil {
return written, err
}
n, err = writeProtoTimestamp(w, ts)
n, err = writeProtoTimestamp(w, startTimestamp)
written += n
if err != nil {
return written, err
Expand Down Expand Up @@ -333,20 +332,11 @@ func writeExemplar20(w enhancedWriter, e *dto.Exemplar) (int, error) {

// writeOpenMetrics20Timestamp writes a float64 as a timestamp without scientific notation.
func writeOpenMetrics20Timestamp(w enhancedWriter, f float64) (int, error) {
switch {
case math.IsNaN(f):
return w.WriteString("NaN")
case math.IsInf(f, +1):
return w.WriteString("+Inf")
case math.IsInf(f, -1):
return w.WriteString("-Inf")
default:
bp := numBufPool.Get().(*[]byte)
*bp = strconv.AppendFloat((*bp)[:0], f, 'f', -1, 64)
written, err := w.Write(*bp)
numBufPool.Put(bp)
return written, err
}
bp := numBufPool.Get().(*[]byte)
*bp = strconv.AppendFloat((*bp)[:0], f, 'f', -1, 64)
written, err := w.Write(*bp)
numBufPool.Put(bp)
return written, err
}

// Stubs for Summary and Histogram
Expand Down Expand Up @@ -405,27 +395,27 @@ func writeProtoTimestamp(w enhancedWriter, ts *timestamppb.Timestamp) (int, erro
return n, nil
}
err = w.WriteByte('.')
n++
written := n + 1
if err != nil {
return n, err
return written, err
}
bp := numBufPool.Get().(*[]byte)
*bp = strconv.AppendInt((*bp)[:0], int64(ts.Nanos), 10)
pad := 9 - len(*bp)
for range pad {
err = w.WriteByte('0')
n++
written++
if err != nil {
numBufPool.Put(bp)
return n, err
return written, err
}
}
val := *bp
for len(val) > 0 && val[len(val)-1] == '0' {
val = val[:len(val)-1]
}
n2, err := w.Write(val)
n += n2
written += n2
numBufPool.Put(bp)
return n, err
return written, err
}
95 changes: 93 additions & 2 deletions expfmt/openmetrics_2_0_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,48 @@ test_metric 1.23
},
out: `# TYPE http_requests_total counter
http_requests_total 1027.0
`,
},
{
name: "GaugeWithAccidentalCounterCreatedTimestamp",
in: &dto.MetricFamily{
Name: proto.String("node_memory_active_bytes"),
Help: proto.String("Active memory in bytes."),
Type: dto.MetricType_GAUGE.Enum(),
Metric: []*dto.Metric{
{
Gauge: &dto.Gauge{
Value: proto.Float64(1.2345e+09),
},
Counter: &dto.Counter{
CreatedTimestamp: &timestamppb.Timestamp{Seconds: 1234567890},
},
},
},
},
out: `# HELP node_memory_active_bytes Active memory in bytes.
# TYPE node_memory_active_bytes gauge
node_memory_active_bytes 1.2345e+09
`,
},
{
name: "UntypedWithAccidentalCounterCreatedTimestamp",
in: &dto.MetricFamily{
Name: proto.String("test_metric"),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
{
Untyped: &dto.Untyped{
Value: proto.Float64(1.23),
},
Counter: &dto.Counter{
CreatedTimestamp: &timestamppb.Timestamp{Seconds: 1234567890},
},
},
},
},
out: `# TYPE test_metric unknown
test_metric 1.23
`,
},
{
Expand Down Expand Up @@ -304,12 +346,15 @@ http_requests_total 1027.0
}
}

func TestWriteOpenMetrics20Timestamp_SpecialValues(t *testing.T) {
func TestWriteOpenMetrics20Timestamp(t *testing.T) {
tests := []struct {
name string
val float64
out string
}{
{"Integer", 1234567890, "1234567890"},
{"Subsecond", 1234567890.123, "1234567890.123"},
{"Zero", 0, "0"},
{"NaN", math.NaN(), "NaN"},
{"+Inf", math.Inf(+1), "+Inf"},
{"-Inf", math.Inf(-1), "-Inf"},
Expand Down Expand Up @@ -600,7 +645,7 @@ func TestWriteOpenMetrics20Sample_UseIntValue(t *testing.T) {
var buf bytes.Buffer
w := enhancedWriter(&buf)
metric := &dto.Metric{}
n, err := writeOpenMetrics20Sample(w, "test_metric", metric, 0, 123, true, nil)
n, err := writeOpenMetrics20Sample(w, "test_metric", metric, 0, 123, true, nil, nil)
if err != nil {
t.Fatal(err)
}
Expand All @@ -613,6 +658,52 @@ func TestWriteOpenMetrics20Sample_UseIntValue(t *testing.T) {
}
}

func TestWriteProtoTimestamp(t *testing.T) {
tests := []struct {
name string
ts *timestamppb.Timestamp
out string
}{
{
name: "WholeSecondsPositive",
ts: &timestamppb.Timestamp{Seconds: 1234567890},
out: "1234567890",
},
{
name: "SubsecondPositive",
ts: &timestamppb.Timestamp{Seconds: 1234567890, Nanos: 500000000},
out: "1234567890.5",
},
{
name: "SubsecondPositiveFullPrecision",
ts: &timestamppb.Timestamp{Seconds: 1234567890, Nanos: 987654321},
out: "1234567890.987654321",
},
{
name: "ZeroSecondsSubsecond",
ts: &timestamppb.Timestamp{Seconds: 0, Nanos: 500000000},
out: "0.5",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var buf bytes.Buffer
w := enhancedWriter(&buf)
n, err := writeProtoTimestamp(w, tc.ts)
if err != nil {
t.Fatal(err)
}
if buf.String() != tc.out {
t.Errorf("expected %q, got %q", tc.out, buf.String())
}
if n != len(tc.out) {
t.Errorf("expected %d bytes written, got %d", len(tc.out), n)
}
})
}
}

func TestCreateOpenMetrics20_SimpleWriter(t *testing.T) {
in := &dto.MetricFamily{
Name: proto.String("http_requests_total"),
Expand Down
Loading