diff --git a/expfmt/encode.go b/expfmt/encode.go index 64dc0eb40..2d4f61104 100644 --- a/expfmt/encode.go +++ b/expfmt/encode.go @@ -72,6 +72,9 @@ func Negotiate(h http.Header) Format { return FmtProtoCompact } } + if ac.Type == "text" && ac.SubType == "html" { + return FmtHTML + } if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") { return FmtText } @@ -96,6 +99,9 @@ func NegotiateIncludingOpenMetrics(h http.Header) Format { return FmtProtoCompact } } + if ac.Type == "text" && ac.SubType == "html" { + return FmtHTML + } if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") { return FmtText } @@ -157,6 +163,8 @@ func NewEncoder(w io.Writer, format Format) Encoder { return err }, } + case FmtHTML: + return NewHTMLEncoder(w) } panic(fmt.Errorf("expfmt.NewEncoder: unknown format %q", format)) } diff --git a/expfmt/expfmt.go b/expfmt/expfmt.go index 0f176fa64..a1e56939c 100644 --- a/expfmt/expfmt.go +++ b/expfmt/expfmt.go @@ -33,6 +33,7 @@ const ( FmtProtoText Format = ProtoFmt + ` encoding=text` FmtProtoCompact Format = ProtoFmt + ` encoding=compact-text` FmtOpenMetrics Format = OpenMetricsType + `; version=` + OpenMetricsVersion + `; charset=utf-8` + FmtHTML Format = "text/html; charset=utf-8" ) const ( diff --git a/expfmt/html_create.go b/expfmt/html_create.go new file mode 100644 index 000000000..97ad529f9 --- /dev/null +++ b/expfmt/html_create.go @@ -0,0 +1,72 @@ +// Copyright 2022 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. + +// PROOF OF CONCEPT ONLY +// +// This is an experiment in providing a nicer experience for the "look at the +// metrics page in a browser" experience. It provides a path forward should we +// ever want to make a binary format the default for actual scraping. When +// negotiating a format with the client, we can take into account that the +// client may be a browser, and is asking for text/html. + +package expfmt + +import ( + "bytes" + "html/template" + "io" + + dto "github.com/prometheus/client_model/go" +) + +// TODO: make this prettier +var preamble = []byte("Metrics

Metrics

") +var metricFamilyTemplate = template.Must(template.New("metrics-page").Parse(`
{{.}}
`)) +var postamble = []byte("") + +// MetricFamilyToHTML writes the HTML for a single MetricFamily. +func MetricFamilyToHTML(out io.Writer, in *dto.MetricFamily) error { + buf := &bytes.Buffer{} + + _, err := MetricFamilyToText(buf, in) + if err != nil { + return nil + } + + return metricFamilyTemplate.Execute(out, buf) +} + +// HTMLPreamble writes the header and general front matter for the HTML +// representation. +func HTMLPreamble(out io.Writer) error { + _, err := out.Write(preamble) + return err +} + +// HTMLPostamble writes the footer and closing tags for the HTML representation. +// It closes all tags opened in the preamble. +func HTMLPostamble(out io.Writer) error { + _, err := out.Write(postamble) + return err +} + +func NewHTMLEncoder(w io.Writer) encoderCloser { + // TODO: don't swallow errors. The current interfaces don't allow for a global preamble, so we're writing it right here 🤞 + _ = HTMLPreamble(w) + return encoderCloser{ + encode: func(v *dto.MetricFamily) error { + return MetricFamilyToHTML(w, v) + }, + close: func() error { return HTMLPostamble(w) }, + } +} diff --git a/expfmt/html_create_test.go b/expfmt/html_create_test.go new file mode 100644 index 000000000..91bcbad29 --- /dev/null +++ b/expfmt/html_create_test.go @@ -0,0 +1,93 @@ +// Copyright 2022 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 ( + "bytes" + "math" + "testing" + + "github.com/golang/protobuf/proto" //nolint:staticcheck // Ignore SA1019. Need to keep deprecated package for compatibility. + dto "github.com/prometheus/client_model/go" +) + +func TestCreateHTML(t *testing.T) { + var scenarios = []struct { + in *dto.MetricFamily + out string + }{ + // 0: Counter, NaN as value, timestamp given. + { + in: &dto.MetricFamily{ + Name: proto.String("name"), + Help: proto.String("two-line\n doc str\\ing"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + &dto.Metric{ + Label: []*dto.LabelPair{ + &dto.LabelPair{ + Name: proto.String("labelname"), + Value: proto.String("val1"), + }, + &dto.LabelPair{ + Name: proto.String("basename"), + Value: proto.String("basevalue"), + }, + }, + Counter: &dto.Counter{ + Value: proto.Float64(math.NaN()), + }, + }, + &dto.Metric{ + Label: []*dto.LabelPair{ + &dto.LabelPair{ + Name: proto.String("labelname"), + Value: proto.String("val2"), + }, + &dto.LabelPair{ + Name: proto.String("basename"), + Value: proto.String("basevalue"), + }, + }, + Counter: &dto.Counter{ + Value: proto.Float64(.23), + }, + TimestampMs: proto.Int64(1234567890), + }, + }, + }, + out: `
# HELP name two-line\n doc  str\\ing
+# TYPE name counter
+name{labelname="val1",basename="basevalue"} NaN
+name{labelname="val2",basename="basevalue"} 0.23 1234567890
+
`, + }, + } + + for i, scenario := range scenarios { + out := bytes.NewBuffer(make([]byte, 0, len(scenario.out))) + err := MetricFamilyToHTML(out, scenario.in) + if err != nil { + t.Errorf("%d. error: %s", i, err) + continue + } + if expected, got := scenario.out, out.String(); expected != got { + t.Errorf( + "%d. expected\nout: %q\ngot: %q", + i, expected, got, + ) + } + } + +}