diff --git a/expfmt/openmetrics_2_0_create.go b/expfmt/openmetrics_2_0_create.go index a218226b..8bf4a724 100644 --- a/expfmt/openmetrics_2_0_create.go +++ b/expfmt/openmetrics_2_0_create.go @@ -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) @@ -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 } @@ -245,10 +245,9 @@ 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@") @@ -256,7 +255,7 @@ func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, if err != nil { return written, err } - n, err = writeProtoTimestamp(w, ts) + n, err = writeProtoTimestamp(w, startTimestamp) written += n if err != nil { return written, err @@ -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 @@ -405,19 +395,19 @@ 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 @@ -425,7 +415,7 @@ func writeProtoTimestamp(w enhancedWriter, ts *timestamppb.Timestamp) (int, erro val = val[:len(val)-1] } n2, err := w.Write(val) - n += n2 + written += n2 numBufPool.Put(bp) - return n, err + return written, err } diff --git a/expfmt/openmetrics_2_0_create_test.go b/expfmt/openmetrics_2_0_create_test.go index 747bff4c..4ee8ac16 100644 --- a/expfmt/openmetrics_2_0_create_test.go +++ b/expfmt/openmetrics_2_0_create_test.go @@ -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: ×tamppb.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: ×tamppb.Timestamp{Seconds: 1234567890}, + }, + }, + }, + }, + out: `# TYPE test_metric unknown +test_metric 1.23 `, }, { @@ -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"}, @@ -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) } @@ -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: ×tamppb.Timestamp{Seconds: 1234567890}, + out: "1234567890", + }, + { + name: "SubsecondPositive", + ts: ×tamppb.Timestamp{Seconds: 1234567890, Nanos: 500000000}, + out: "1234567890.5", + }, + { + name: "SubsecondPositiveFullPrecision", + ts: ×tamppb.Timestamp{Seconds: 1234567890, Nanos: 987654321}, + out: "1234567890.987654321", + }, + { + name: "ZeroSecondsSubsecond", + ts: ×tamppb.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"),