From ad7021c146dc96e89e6b9b365e016dcc72b2a62c Mon Sep 17 00:00:00 2001 From: David Ashpole Date: Mon, 24 Aug 2026 16:22:47 +0000 Subject: [PATCH] expfmt: format OpenMetrics 2.0 float values and validate units - Use writeOpenMetricsFloat in writeOpenMetrics20Sample and writeExemplar20 so integral floats render with .0. - Validate in.Unit against newlines and carriage returns. - Update test cases to reflect float formatting and add unit newline error tests. Signed-off-by: David Ashpole --- expfmt/openmetrics_2_0_create.go | 7 +++-- expfmt/openmetrics_2_0_create_test.go | 42 +++++++++++++++++++++------ 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/expfmt/openmetrics_2_0_create.go b/expfmt/openmetrics_2_0_create.go index 96990cf8..a218226b 100644 --- a/expfmt/openmetrics_2_0_create.go +++ b/expfmt/openmetrics_2_0_create.go @@ -42,6 +42,9 @@ func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options .. if containsRawNewline(name) { return 0, fmt.Errorf("MetricFamily name %q contains raw newlines", name) } + if in.Unit != nil && containsRawNewline(*in.Unit) { + return 0, fmt.Errorf("MetricFamily unit %q contains raw newlines", *in.Unit) + } // Try the interface upgrade. If it doesn't work, we'll use a // bufio.Writer from the sync.Pool. @@ -222,7 +225,7 @@ func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, if useIntValue { n, err = writeUint(w, intValue) } else { - n, err = writeFloat(w, floatValue) + n, err = writeOpenMetricsFloat(w, floatValue) } written += n if err != nil { @@ -305,7 +308,7 @@ func writeExemplar20(w enhancedWriter, e *dto.Exemplar) (int, error) { if err != nil { return written, err } - n, err = writeFloat(w, e.GetValue()) + n, err = writeOpenMetricsFloat(w, e.GetValue()) written += n if err != nil { return written, err diff --git a/expfmt/openmetrics_2_0_create_test.go b/expfmt/openmetrics_2_0_create_test.go index c5ee03dd..747bff4c 100644 --- a/expfmt/openmetrics_2_0_create_test.go +++ b/expfmt/openmetrics_2_0_create_test.go @@ -52,7 +52,7 @@ func TestCreateOpenMetrics20(t *testing.T) { }, out: `# HELP http_requests_total Total number of HTTP requests. # TYPE http_requests_total counter -http_requests_total{method="GET",code="200"} 1027 st@1234567890 +http_requests_total{method="GET",code="200"} 1027.0 st@1234567890 `, }, { @@ -76,7 +76,7 @@ http_requests_total{method="GET",code="200"} 1027 st@1234567890 }, out: `# HELP http_requests_total Total number of HTTP requests. # TYPE http_requests_total counter -http_requests_total{method="GET",code="200"} 1027 st@1234567890.987654321 +http_requests_total{method="GET",code="200"} 1027.0 st@1234567890.987654321 `, }, { @@ -160,7 +160,7 @@ node_memory_active_bytes 1.2345e+09 1234567890 }, }, out: `# TYPE http_requests_total counter -http_requests_total 1027 1234567891 st@1234567890 # {trace_id="1234"} 1 1234567890.5 +http_requests_total 1027.0 1234567891 st@1234567890 # {trace_id="1234"} 1.0 1234567890.5 `, }, { @@ -181,7 +181,7 @@ http_requests_total 1027 1234567891 st@1234567890 # {trace_id="1234"} 1 12345678 }, }, out: `# TYPE http_requests_total counter -http_requests_total 1027 # {} 1 1234567890.5 +http_requests_total 1027.0 # {} 1.0 1234567890.5 `, }, { @@ -204,7 +204,7 @@ http_requests_total 1027 # {} 1 1234567890.5 }, }, out: `# TYPE http_requests_total counter -http_requests_total 1027 +http_requests_total 1027.0 `, }, { @@ -228,7 +228,7 @@ http_requests_total 1027 }, }, out: `# TYPE http_requests_total counter -http_requests_total 1027 # {trace_id="1234"} NaN 1234567890 +http_requests_total 1027.0 # {trace_id="1234"} NaN 1234567890 `, }, { @@ -262,7 +262,7 @@ test_metric 1.23 }, }, out: `# TYPE http_requests_total counter -http_requests_total 1027 +http_requests_total 1027.0 `, }, { @@ -282,7 +282,7 @@ http_requests_total 1027 }, }, out: `# TYPE "你好_total" counter -{"你好_total","🌎"="🌍"} 1027 +{"你好_total","🌎"="🌍"} 1027.0 `, }, } @@ -491,6 +491,30 @@ func TestCreateOpenMetrics20_Errors(t *testing.T) { }, expectedErr: "contains raw newlines", }, + { + name: "NewlineInUnit", + in: &dto.MetricFamily{ + Name: proto.String("test_counter_total"), + Type: dto.MetricType_COUNTER.Enum(), + Unit: proto.String("seconds\n"), + Metric: []*dto.Metric{ + {Counter: &dto.Counter{Value: proto.Float64(1.0)}}, + }, + }, + expectedErr: "contains raw newlines", + }, + { + name: "CarriageReturnInUnit", + in: &dto.MetricFamily{ + Name: proto.String("test_counter_total"), + Type: dto.MetricType_COUNTER.Enum(), + Unit: proto.String("seconds\r"), + Metric: []*dto.Metric{ + {Counter: &dto.Counter{Value: proto.Float64(1.0)}}, + }, + }, + expectedErr: "contains raw newlines", + }, { name: "NilMetric", in: &dto.MetricFamily{ @@ -614,7 +638,7 @@ func TestCreateOpenMetrics20_SimpleWriter(t *testing.T) { } expected := `# TYPE http_requests_total counter -http_requests_total 1027 +http_requests_total 1027.0 ` if buf.String() != expected { t.Errorf("expected %q, got %q", expected, buf.String())