Repository: prometheus/common
Pull request #596 (2024-03-05 15:55:12)

expfmt: Add a way to generate different OpenMetrics Formats
Also complete test coverage of expfmt.go

ywwg: not sure how to rerun the circleci tests, I added the license header so it should be ok now
beorn7: Thanks, this is on my review list (which is, unfortunately, very long once more).

Patch:
diff --git a/Makefile.common b/Makefile.common
index 9255815..49ed5f5 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -208,6 +208,10 @@ common-tarball: promu
 	@echo ">> building release tarball"
 	$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR)
 
+.PHONY: common-docker-repo-name
+common-docker-repo-name:
+	@echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)"
+
 .PHONY: common-docker $(BUILD_DOCKER_ARCHS)
 common-docker: $(BUILD_DOCKER_ARCHS)
 $(BUILD_DOCKER_ARCHS): common-docker-%:
diff --git a/expfmt/expfmt.go b/expfmt/expfmt.go
index 6fc9555..051b38c 100644
--- a/expfmt/expfmt.go
+++ b/expfmt/expfmt.go
@@ -15,6 +15,7 @@
 package expfmt
 
 import (
+	"fmt"
 	"strings"
 
 	"github.com/prometheus/common/model"
@@ -63,7 +64,7 @@ const (
 type FormatType int
 
 const (
-	TypeUnknown = iota
+	TypeUnknown FormatType = iota
 	TypeProtoCompact
 	TypeProtoDelim
 	TypeProtoText
@@ -73,7 +74,8 @@ const (
 
 // NewFormat generates a new Format from the type provided. Mostly used for
 // tests, most Formats should be generated as part of content negotiation in
-// encode.go.
+// encode.go. If a type has more than one version, the latest version will be
+// returned.
 func NewFormat(t FormatType) Format {
 	switch t {
 	case TypeProtoCompact:
@@ -91,13 +93,21 @@ func NewFormat(t FormatType) Format {
 	}
 }
 
+// NewOpenMetricsFormat generates a new OpenMetrics format matching the
+// specified version number.
+func NewOpenMetricsFormat(version string) (Format, error) {
+	if version == OpenMetricsVersion_0_0_1 {
+		return fmtOpenMetrics_0_0_1, nil
+	}
+	if version == OpenMetricsVersion_1_0_0 {
+		return fmtOpenMetrics_1_0_0, nil
+	}
+	return fmtUnknown, fmt.Errorf("unknown open metrics version string")
+}
+
 // FormatType deduces an overall FormatType for the given format.
 func (f Format) FormatType() FormatType {
 	toks := strings.Split(string(f), ";")
-	if len(toks) < 2 {
-		return TypeUnknown
-	}
-
 	params := make(map[string]string)
 	for i, t := range toks {
 		if i == 0 {


Test patch:
diff --git a/expfmt/expfmt_test.go b/expfmt/expfmt_test.go
new file mode 100644
index 0000000..8ec1652
--- /dev/null
+++ b/expfmt/expfmt_test.go
@@ -0,0 +1,129 @@
+// Copyright 2024 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package expfmt
+
+import (
+	"testing"
+
+	"github.com/prometheus/common/model"
+)
+
+// Test Format to Escapting Scheme conversion
+// Path: expfmt/expfmt_test.go
+// Compare this snippet from expfmt/expfmt.go:
+func TestToFormatType(t *testing.T) {
+	tests := []struct {
+		format   Format
+		expected FormatType
+	}{
+		{
+			format:   fmtProtoCompact,
+			expected: TypeProtoCompact,
+		},
+		{
+			format:   fmtProtoDelim,
+			expected: TypeProtoDelim,
+		},
+		{
+			format:   fmtProtoText,
+			expected: TypeProtoText,
+		},
+		{
+			format:   fmtOpenMetrics_1_0_0,
+			expected: TypeOpenMetrics,
+		},
+		{
+			format:   fmtText,
+			expected: TypeTextPlain,
+		},
+		{
+			format:   fmtOpenMetrics_0_0_1,
+			expected: TypeOpenMetrics,
+		},
+		{
+			format:   "application/vnd.google.protobuf; proto=BadProtocol; encoding=text",
+			expected: TypeUnknown,
+		},
+		{
+			format:   "application/vnd.google.protobuf",
+			expected: TypeUnknown,
+		},
+		{
+			format:   "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily=bad",
+			expected: TypeUnknown,
+		},
+		// encoding missing
+		{
+			format:   "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily",
+			expected: TypeUnknown,
+		},
+		// invalid encoding
+		{
+			format:   "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=textual",
+			expected: TypeUnknown,
+		},
+		// bad charset, must be utf-8
+		{
+			format:   "application/openmetrics-text; version=1.0.0; charset=ascii",
+			expected: TypeUnknown,
+		},
+		{
+			format:   "text/plain",
+			expected: TypeTextPlain,
+		},
+		{
+			format:   "text/plain; version=invalid",
+			expected: TypeUnknown,
+		},
+		{
+			format:   "gobbledygook",
+			expected: TypeUnknown,
+		},
+	}
+	for _, test := range tests {
+		if test.format.FormatType() != test.expected {
+			t.Errorf("expected %v got %v", test.expected, test.format.FormatType())
+		}
+	}
+}
+
+func TestToEscapingScheme(t *testing.T) {
+	tests := []struct {
+		format   Format
+		expected model.EscapingScheme
+	}{
+		{
+			format:   fmtProtoCompact,
+			expected: model.ValueEncodingEscaping,
+		},
+		{
+			format:   "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=underscores",
+			expected: model.UnderscoreEscaping,
+		},
+		{
+			format:   "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=allow-utf-8",
+			expected: model.NoEscaping,
+		},
+		// error returns default
+		{
+			format:   "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=invalid",
+			expected: model.NameEscapingScheme,
+		},
+	}
+	for _, test := range tests {
+		if test.format.ToEscapingScheme() != test.expected {
+			t.Errorf("expected %v got %v", test.expected, test.format.ToEscapingScheme())
+		}
+	}
+}