diff --git a/expfmt/openmetrics_2_0_create.go b/expfmt/openmetrics_2_0_create.go index f9b2a3c0..600ac32f 100644 --- a/expfmt/openmetrics_2_0_create.go +++ b/expfmt/openmetrics_2_0_create.go @@ -21,6 +21,7 @@ import ( "math" "strconv" "strings" + "unicode/utf8" dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/types/known/timestamppb" @@ -51,8 +52,26 @@ 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) + if !utf8.ValidString(name) { + return 0, fmt.Errorf("MetricFamily name %q is not valid UTF-8", name) + } + if in.Help != nil { + if !utf8.ValidString(*in.Help) { + return 0, fmt.Errorf("MetricFamily help %q is not valid UTF-8", *in.Help) + } + // A carriage return may appear in HELP, but not at the end, where it + // would make the line end in "\r\n". + if strings.HasSuffix(*in.Help, "\r") { + return 0, fmt.Errorf("MetricFamily help %q ends with a carriage return", *in.Help) + } + } + if in.Unit != nil { + if containsRawNewline(*in.Unit) { + return 0, fmt.Errorf("MetricFamily unit %q contains raw newlines", *in.Unit) + } + if !utf8.ValidString(*in.Unit) { + return 0, fmt.Errorf("MetricFamily unit %q is not valid UTF-8", *in.Unit) + } } // Try the interface upgrade. If it doesn't work, we'll use a diff --git a/expfmt/openmetrics_2_0_create_test.go b/expfmt/openmetrics_2_0_create_test.go index ff08eee4..4c42f3b2 100644 --- a/expfmt/openmetrics_2_0_create_test.go +++ b/expfmt/openmetrics_2_0_create_test.go @@ -304,6 +304,18 @@ http_requests_total 1027.0 http_requests_total 1027.0 `, }, + { + name: "CarriageReturnInsideHelp", + in: &dto.MetricFamily{ + Name: proto.String("http_requests_total"), + Help: proto.String("a\rb"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + {Counter: &dto.Counter{Value: proto.Float64(1027)}}, + }, + }, + out: "# HELP http_requests_total a\rb\n# TYPE http_requests_total counter\nhttp_requests_total 1027.0\n", + }, { name: "CounterWithNaNExemplar", in: &dto.MetricFamily{ @@ -2155,6 +2167,53 @@ func TestCreateOpenMetrics20_Errors(t *testing.T) { }, expectedErr: "contains raw newlines", }, + { + name: "InvalidUTF8InMetricName", + in: &dto.MetricFamily{ + Name: proto.String("test_counter\xff"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + {Counter: &dto.Counter{Value: proto.Float64(1.0)}}, + }, + }, + expectedErr: "is not valid UTF-8", + }, + { + name: "InvalidUTF8InHelp", + in: &dto.MetricFamily{ + Name: proto.String("test_counter_total"), + Help: proto.String("help \xff"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + {Counter: &dto.Counter{Value: proto.Float64(1.0)}}, + }, + }, + expectedErr: "is not valid UTF-8", + }, + { + name: "CarriageReturnAtEndOfHelp", + in: &dto.MetricFamily{ + Name: proto.String("test_counter_total"), + Help: proto.String("help\r"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + {Counter: &dto.Counter{Value: proto.Float64(1.0)}}, + }, + }, + expectedErr: "ends with a carriage return", + }, + { + name: "InvalidUTF8InUnit", + in: &dto.MetricFamily{ + Name: proto.String("test_counter_total"), + Type: dto.MetricType_COUNTER.Enum(), + Unit: proto.String("seconds\xff"), + Metric: []*dto.Metric{ + {Counter: &dto.Counter{Value: proto.Float64(1.0)}}, + }, + }, + expectedErr: "is not valid UTF-8", + }, { name: "NilMetric", in: &dto.MetricFamily{