Skip to content

Commit ee2f53a

Browse files
committed
fix: fallback to gauge for protofmt-based negotiations
Fallback to `gauge` metric type when the negotiated content-type is `protofmt`-based, since Prometheus' protobuf machinery does not recognize all OpenMetrics types (`info` and `statesets` in this context). Signed-off-by: Pranshu Srivastava <[email protected]>
1 parent 90bd24f commit ee2f53a

File tree

3 files changed

+66
-4
lines changed

3 files changed

+66
-4
lines changed

pkg/metrics_store/metrics_writer.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ package metricsstore
1919
import (
2020
"fmt"
2121
"io"
22+
"strings"
23+
24+
"github.com/prometheus/common/expfmt"
25+
26+
"k8s.io/kube-state-metrics/v2/pkg/metric"
2227
)
2328

2429
// MetricsWriterList represent a list of MetricsWriter
@@ -82,13 +87,22 @@ func (m MetricsWriter) WriteAll(w io.Writer) error {
8287
return nil
8388
}
8489

85-
// SanitizeHeaders removes duplicate headers from the given MetricsWriterList for the same family (generated through CRS).
86-
// These are expected to be consecutive since G** resolution generates groups of similar metrics with same headers before moving onto the next G** spec in the CRS configuration.
87-
func SanitizeHeaders(writers MetricsWriterList) MetricsWriterList {
90+
// SanitizeHeaders sanitizes the headers of the given MetricsWriterList.
91+
func SanitizeHeaders(contentType string, writers MetricsWriterList) MetricsWriterList {
8892
var lastHeader string
8993
for _, writer := range writers {
9094
if len(writer.stores) > 0 {
9195
for i, header := range writer.stores[0].headers {
96+
// If the requested content type was proto-based, replace the type with "gauge", as "info" and "statesets" are not recognized by Prometheus' protobuf machinery.
97+
if strings.HasPrefix(contentType, expfmt.ProtoType) &&
98+
strings.HasPrefix(header, "# HELP") &&
99+
(strings.HasSuffix(header, " "+string(metric.Info)) || strings.HasSuffix(header, " "+string(metric.StateSet))) {
100+
typeStringWithoutTypePaddedIndex := strings.LastIndex(header, " ")
101+
typeStringWithoutType := header[:typeStringWithoutTypePaddedIndex]
102+
writer.stores[0].headers[i] = typeStringWithoutType + " " + string(metric.Gauge)
103+
}
104+
// Removes duplicate headers from the given MetricsWriterList for the same family (generated through CRS).
105+
// These are expected to be consecutive since G** resolution generates groups of similar metrics with same headers before moving onto the next G** spec in the CRS configuration.
92106
if header == lastHeader {
93107
writer.stores[0].headers[i] = ""
94108
} else {

pkg/metrics_store/metrics_writer_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package metricsstore_test
1818

1919
import (
2020
"fmt"
21+
"github.com/prometheus/common/expfmt"
2122
"strings"
2223
"testing"
2324

@@ -263,3 +264,49 @@ func TestWriteAllWithEmptyStores(t *testing.T) {
263264
t.Fatalf("Unexpected output, got %q, want %q", result, "")
264265
}
265266
}
267+
268+
func BenchmarkSanitizeHeaders(b *testing.B) {
269+
benchmarks := []struct {
270+
name string
271+
contentType expfmt.Format
272+
writersContainsDuplicates bool
273+
}{
274+
{
275+
name: "text-format unique headers",
276+
contentType: expfmt.FmtText,
277+
writersContainsDuplicates: false,
278+
},
279+
{
280+
name: "text-format duplicate headers",
281+
contentType: expfmt.FmtText,
282+
writersContainsDuplicates: true,
283+
},
284+
{
285+
name: "proto-format unique headers",
286+
contentType: expfmt.ProtoFmt, // Prometheus ProtoFmt is the only proto-based format we check for.
287+
writersContainsDuplicates: false,
288+
},
289+
{
290+
name: "proto-format duplicate headers",
291+
contentType: expfmt.ProtoFmt, // Prometheus ProtoFmt is the only proto-based format we check for.
292+
writersContainsDuplicates: true,
293+
},
294+
}
295+
296+
for _, benchmark := range benchmarks {
297+
headers := []string{}
298+
for j := 0; j < 10e4; j++ {
299+
if benchmark.writersContainsDuplicates {
300+
headers = append(headers, fmt.Sprintf("# HELP foo foo_help\n# TYPE foo info"))
301+
} else {
302+
headers = append(headers, fmt.Sprintf("# HELP foo_%d foo_help\n# TYPE foo_%d info", j, j))
303+
}
304+
}
305+
writer := metricsstore.NewMetricsWriter(metricsstore.NewMetricsStore(headers, nil))
306+
b.Run(benchmark.name, func(b *testing.B) {
307+
for i := 0; i < b.N; i++ {
308+
metricsstore.SanitizeHeaders(string(benchmark.contentType), metricsstore.MetricsWriterList{writer})
309+
}
310+
})
311+
}
312+
}

pkg/metricshandler/metrics_handler.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ func (m *MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
187187
var writer io.Writer = w
188188

189189
contentType := expfmt.NegotiateIncludingOpenMetrics(r.Header)
190+
gotContentType := contentType
190191

191192
// We do not support protobuf at the moment. Fall back to FmtText if the negotiated exposition format is not FmtOpenMetrics See: https://github.com/kubernetes/kube-state-metrics/issues/2022
192193
if contentType != expfmt.FmtOpenMetrics_1_0_0 && contentType != expfmt.FmtOpenMetrics_0_0_1 {
@@ -208,7 +209,7 @@ func (m *MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
208209
}
209210
}
210211

211-
m.metricsWriters = metricsstore.SanitizeHeaders(m.metricsWriters)
212+
m.metricsWriters = metricsstore.SanitizeHeaders(string(gotContentType), m.metricsWriters)
212213
for _, w := range m.metricsWriters {
213214
err := w.WriteAll(writer)
214215
if err != nil {

0 commit comments

Comments
 (0)