Repository: prometheus/common
Pull request #219 (2020-01-18 23:02:35)

Very preliminary OpenMetrics creation
This is mostly to enable exemplar rendering in client_golang.

The blacklisting feature is introduced here so that OpenMetrics can be configured as opt-in in client_golang.

brian-brazil: It occured to me that we're going to need to change the le/quantile values for the Prometheus format at this point too, so you get the same series no matter what is negotiated under the covers.
beorn7: > It occured to me that we're going to need to change the le/quantile values for the Prometheus format at this point too, so you get the same series no matter what is negotiated under the covers.

I'd say the opposite is required: We don't want to change the le/quantile labels as long as the Prometheus format is negotiated. It would be a breaking change of client_golang if we did. (We mitigated the impact by changing the `histogram_quantile` function, but it will still break many use cases.)

Only changing the le/quantile labels when negotiating OpenMetrics is at least a change that will only happen if a config is changed (in client_golang, OpenMetrics negatiation has to be activated).

It's still not ideal but better than changing the format by a minor version upgrade in client_golang.

Also, I'm still hopeful that https://github.com/OpenObservability/OpenMetrics/issues/129 makes it into the spec. Then we don't need to worry in OpenMetrics.

(And if https://github.com/OpenObservability/OpenMetrics/issues/129 didn't make it into the spec, we wouldn't know an algorithm to reproducibly create correct OM, so we don't have to worry, either.)
beorn7: > At most we'll make it a SHOULD rather than MUST to produce the right output

But nobody knows how to create the right output.

> I'd hope we'd be enabling it be default, what are your plans there?

At the moment, I cannot see a way how to do it without breaking users in more or less subtle ways. Given the widespread use of the library, I don't think we should make it the default without a major version bump. Thus, I'd like to keep it an opt-in in v1.

> Such algorithms are well known, and implemented in many languages including Go.

My understanding is, as [explained before](https://github.com/OpenObservability/OpenMetrics/issues/129#issuecomment-503154885), that all algorithms we know are concerned with reproducibly creating the same float64 from a given string rather then creating the same string from a given float64. But for OM we need the latter. Happy to be proven wrong, but that hasn't happened yet.
beorn7: To summarize: My goal here was to let us experiment with exemplars and OM without breaking existing users of this code (and neither client_golang). As the discussion above lead to the insight that an uninformed upgrade of prometheus/common could trigger OM negotiation without the user being aware that the `# EOF` line has to be added (one way or the other), that seems impossible to accomplish.

To avoid working from a branch, I'll add another commit to this PR that uses a separate `Negotiate` function for OM. We can then decide if we want to merge that or keep working from a branch for the experiments.
brian-brazil: Godoc is still unhappy, but poking around github there seems to be at least a handful of actively maintained projects using this code (though most would be better off using promhttp), so breakage isn't going to be free and this is a kinda-public API.

> We can then decide if we want to merge that or keep working from a branch for the experiments.

I'd prefer not to have two APIs for this. To what extent are you planning on doing experiments, just local or getting it into a Prometheus release?
beorn7: > I'd prefer not to have two APIs for this. To what extent are you planning on doing experiments, just local or getting it into a Prometheus release?

I want this to instrument targets with exemplars. prometheus/prometheus doesn't need to be released with any of these changes, but if we do this without breaking changes, there is also no harm in doing so.
beorn7: OK, PHAL.

Now we have a `Closer`, but Go stdlib style as an interface upgrade to not break anybody.

OM will only be an option for content negotiation with a dedicated function to negotiate. It is marked in the doc string as going away once the experiment is over.
brian-brazil: :+1: 
beorn7: Thanks. After all this careful consideration, I'll squash this into one commit as I don't think we will take those steps apart again.

Patch:
diff --git a/expfmt/encode.go b/expfmt/encode.go
index 11839ed..bd4e347 100644
--- a/expfmt/encode.go
+++ b/expfmt/encode.go
@@ -30,17 +30,38 @@ type Encoder interface {
 	Encode(*dto.MetricFamily) error
 }
 
-type encoder func(*dto.MetricFamily) error
+// Closer is implemented by Encoders that need to be closed to finalize
+// encoding. (For example, OpenMetrics needs a final `# EOF` line.)
+//
+// Note that all Encoder implementations returned from this package implement
+// Closer, too, even if the Close call is a no-op. This happens in preparation
+// for adding a Close method to the Encoder interface directly in a (mildly
+// breaking) release in the future.
+type Closer interface {
+	Close() error
+}
+
+type encoderCloser struct {
+	encode func(*dto.MetricFamily) error
+	close  func() error
+}
 
-func (e encoder) Encode(v *dto.MetricFamily) error {
-	return e(v)
+func (ec encoderCloser) Encode(v *dto.MetricFamily) error {
+	return ec.encode(v)
 }
 
-// Negotiate returns the Content-Type based on the given Accept header.
-// If no appropriate accepted type is found, FmtText is returned.
+func (ec encoderCloser) Close() error {
+	return ec.close()
+}
+
+// Negotiate returns the Content-Type based on the given Accept header. If no
+// appropriate accepted type is found, FmtText is returned (which is the
+// Prometheus text format). This function will never negotiate FmtOpenMetrics,
+// as the support is still experimental. To include the option to negotiate
+// FmtOpenMetrics, use NegotiateOpenMetrics.
 func Negotiate(h http.Header) Format {
 	for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) {
-		// Check for protocol buffer
+		ver := ac.Params["version"]
 		if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol {
 			switch ac.Params["encoding"] {
 			case "delimited":
@@ -51,38 +72,91 @@ func Negotiate(h http.Header) Format {
 				return FmtProtoCompact
 			}
 		}
-		// Check for text format.
+		if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
+			return FmtText
+		}
+	}
+	return FmtText
+}
+
+// NegotiateIncludingOpenMetrics works like Negotiate but includes
+// FmtOpenMetrics as an option for the result. Note that this function is
+// temporary and will disappear once FmtOpenMetrics is fully supported and as
+// such may be negotiated by the normal Negotiate function.
+func NegotiateIncludingOpenMetrics(h http.Header) Format {
+	for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) {
 		ver := ac.Params["version"]
+		if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol {
+			switch ac.Params["encoding"] {
+			case "delimited":
+				return FmtProtoDelim
+			case "text":
+				return FmtProtoText
+			case "compact-text":
+				return FmtProtoCompact
+			}
+		}
 		if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
 			return FmtText
 		}
+		if ac.Type+"/"+ac.SubType == OpenMetricsType && (ver == OpenMetricsVersion || ver == "") {
+			return FmtOpenMetrics
+		}
 	}
 	return FmtText
 }
 
-// NewEncoder returns a new encoder based on content type negotiation.
+// NewEncoder returns a new encoder based on content type negotiation. All
+// Encoder implementations returned by NewEncoder also implement Closer, and
+// callers should always call the Close method. It is currently only required
+// for FmtOpenMetrics, but a future (breaking) release will add the Close method
+// to the Encoder interface directly. The current version of the Encoder
+// interface is kept for backwards compatibility.
 func NewEncoder(w io.Writer, format Format) Encoder {
 	switch format {
 	case FmtProtoDelim:
-		return encoder(func(v *dto.MetricFamily) error {
-			_, err := pbutil.WriteDelimited(w, v)
-			return err
-		})
+		return encoderCloser{
+			encode: func(v *dto.MetricFamily) error {
+				_, err := pbutil.WriteDelimited(w, v)
+				return err
+			},
+			close: func() error { return nil },
+		}
 	case FmtProtoCompact:
-		return encoder(func(v *dto.MetricFamily) error {
-			_, err := fmt.Fprintln(w, v.String())
-			return err
-		})
+		return encoderCloser{
+			encode: func(v *dto.MetricFamily) error {
+				_, err := fmt.Fprintln(w, v.String())
+				return err
+			},
+			close: func() error { return nil },
+		}
 	case FmtProtoText:
-		return encoder(func(v *dto.MetricFamily) error {
-			_, err := fmt.Fprintln(w, proto.MarshalTextString(v))
-			return err
-		})
+		return encoderCloser{
+			encode: func(v *dto.MetricFamily) error {
+				_, err := fmt.Fprintln(w, proto.MarshalTextString(v))
+				return err
+			},
+			close: func() error { return nil },
+		}
 	case FmtText:
-		return encoder(func(v *dto.MetricFamily) error {
-			_, err := MetricFamilyToText(w, v)
-			return err
-		})
+		return encoderCloser{
+			encode: func(v *dto.MetricFamily) error {
+				_, err := MetricFamilyToText(w, v)
+				return err
+			},
+			close: func() error { return nil },
+		}
+	case FmtOpenMetrics:
+		return encoderCloser{
+			encode: func(v *dto.MetricFamily) error {
+				_, err := MetricFamilyToOpenMetrics(w, v)
+				return err
+			},
+			close: func() error {
+				_, err := FinalizeOpenMetrics(w)
+				return err
+			},
+		}
 	}
-	panic("expfmt.NewEncoder: unknown format")
+	panic(fmt.Errorf("expfmt.NewEncoder: unknown format %q", format))
 }
diff --git a/expfmt/expfmt.go b/expfmt/expfmt.go
index c71bcb9..0f176fa 100644
--- a/expfmt/expfmt.go
+++ b/expfmt/expfmt.go
@@ -19,10 +19,12 @@ type Format string
 
 // Constants to assemble the Content-Type values for the different wire protocols.
 const (
-	TextVersion   = "0.0.4"
-	ProtoType     = `application/vnd.google.protobuf`
-	ProtoProtocol = `io.prometheus.client.MetricFamily`
-	ProtoFmt      = ProtoType + "; proto=" + ProtoProtocol + ";"
+	TextVersion        = "0.0.4"
+	ProtoType          = `application/vnd.google.protobuf`
+	ProtoProtocol      = `io.prometheus.client.MetricFamily`
+	ProtoFmt           = ProtoType + "; proto=" + ProtoProtocol + ";"
+	OpenMetricsType    = `application/openmetrics-text`
+	OpenMetricsVersion = "0.0.1"
 
 	// The Content-Type values for the different wire protocols.
 	FmtUnknown      Format = `<unknown>`
@@ -30,6 +32,7 @@ const (
 	FmtProtoDelim   Format = ProtoFmt + ` encoding=delimited`
 	FmtProtoText    Format = ProtoFmt + ` encoding=text`
 	FmtProtoCompact Format = ProtoFmt + ` encoding=compact-text`
+	FmtOpenMetrics  Format = OpenMetricsType + `; version=` + OpenMetricsVersion + `; charset=utf-8`
 )
 
 const (
diff --git a/expfmt/openmetrics_create.go b/expfmt/openmetrics_create.go
new file mode 100644
index 0000000..8a9313a
--- /dev/null
+++ b/expfmt/openmetrics_create.go
@@ -0,0 +1,527 @@
+// Copyright 2020 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 (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"math"
+	"strconv"
+	"strings"
+
+	"github.com/golang/protobuf/ptypes"
+	"github.com/prometheus/common/model"
+
+	dto "github.com/prometheus/client_model/go"
+)
+
+// MetricFamilyToOpenMetrics converts a MetricFamily proto message into the
+// OpenMetrics text format and writes the resulting lines to 'out'. It returns
+// the number of bytes written and any error encountered. The output will have
+// the same order as the input, no further sorting is performed. Furthermore,
+// this function assumes the input is already sanitized and does not perform any
+// sanity checks. If the input contains duplicate metrics or invalid metric or
+// label names, the conversion will result in invalid text format output.
+//
+// This function fulfills the type 'expfmt.encoder'.
+//
+// Note that OpenMetrics requires a final `# EOF` line. Since this function acts
+// on individual metric families, it is the responsibility of the caller to
+// append this line to 'out' once all metric families have been written.
+// Conveniently, this can be done by calling FinalizeOpenMetrics.
+//
+// The output should be fully OpenMetrics compliant. However, there are a few
+// missing features and peculiarities to avoid complications when switching from
+// Prometheus to OpenMetrics or vice versa:
+//
+// - Counters are expected to have the `_total` suffix in their metric name. In
+//   the output, the suffix will be truncated from the `# TYPE` and `# HELP`
+//   line. A counter with a missing `_total` suffix is not an error. However,
+//   its type will be set to `unknown` in that case to avoid invalid OpenMetrics
+//   output.
+//
+// - No support for the following (optional) features: `# UNIT` line, `_created`
+//   line, info type, stateset type, gaugehistogram type.
+//
+// - The size of exemplar labels is not checked (i.e. it's possible to create
+//   exemplars that are larger than allowed by the OpenMetrics specification).
+//
+// - The value of Counters is not checked. (OpenMetrics doesn't allow counters
+//   with a `NaN` value.)
+func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int, err error) {
+	name := in.GetName()
+	if name == "" {
+		return 0, fmt.Errorf("MetricFamily has no name: %s", in)
+	}
+
+	// Try the interface upgrade. If it doesn't work, we'll use a
+	// bufio.Writer from the sync.Pool.
+	w, ok := out.(enhancedWriter)
+	if !ok {
+		b := bufPool.Get().(*bufio.Writer)
+		b.Reset(out)
+		w = b
+		defer func() {
+			bErr := b.Flush()
+			if err == nil {
+				err = bErr
+			}
+			bufPool.Put(b)
+		}()
+	}
+
+	var (
+		n          int
+		metricType = in.GetType()
+		shortName  = name
+	)
+	if metricType == dto.MetricType_COUNTER && strings.HasSuffix(shortName, "_total") {
+		shortName = name[:len(name)-6]
+	}
+
+	// Comments, first HELP, then TYPE.
+	if in.Help != nil {
+		n, err = w.WriteString("# HELP ")
+		written += n
+		if err != nil {
+			return
+		}
+		n, err = w.WriteString(shortName)
+		written += n
+		if err != nil {
+			return
+		}
+		err = w.WriteByte(' ')
+		written++
+		if err != nil {
+			return
+		}
+		n, err = writeEscapedString(w, *in.Help, true)
+		written += n
+		if err != nil {
+			return
+		}
+		err = w.WriteByte('\n')
+		written++
+		if err != nil {
+			return
+		}
+	}
+	n, err = w.WriteString("# TYPE ")
+	written += n
+	if err != nil {
+		return
+	}
+	n, err = w.WriteString(shortName)
+	written += n
+	if err != nil {
+		return
+	}
+	switch metricType {
+	case dto.MetricType_COUNTER:
+		if strings.HasSuffix(name, "_total") {
+			n, err = w.WriteString(" counter\n")
+		} else {
+			n, err = w.WriteString(" unknown\n")
+		}
+	case dto.MetricType_GAUGE:
+		n, err = w.WriteString(" gauge\n")
+	case dto.MetricType_SUMMARY:
+		n, err = w.WriteString(" summary\n")
+	case dto.MetricType_UNTYPED:
+		n, err = w.WriteString(" unknown\n")
+	case dto.MetricType_HISTOGRAM:
+		n, err = w.WriteString(" histogram\n")
+	default:
+		return written, fmt.Errorf("unknown metric type %s", metricType.String())
+	}
+	written += n
+	if err != nil {
+		return
+	}
+
+	// Finally the samples, one line for each.
+	for _, metric := range in.Metric {
+		switch metricType {
+		case dto.MetricType_COUNTER:
+			if metric.Counter == nil {
+				return written, fmt.Errorf(
+					"expected counter in metric %s %s", name, metric,
+				)
+			}
+			// Note that we have ensured above that either the name
+			// ends on `_total` or that the rendered type is
+			// `unknown`. Therefore, no `_total` must be added here.
+			n, err = writeOpenMetricsSample(
+				w, name, "", metric, "", 0,
+				metric.Counter.GetValue(), 0, false,
+				metric.Counter.Exemplar,
+			)
+		case dto.MetricType_GAUGE:
+			if metric.Gauge == nil {
+				return written, fmt.Errorf(
+					"expected gauge in metric %s %s", name, metric,
+				)
+			}
+			n, err = writeOpenMetricsSample(
+				w, name, "", metric, "", 0,
+				metric.Gauge.GetValue(), 0, false,
+				nil,
+			)
+		case dto.MetricType_UNTYPED:
+			if metric.Untyped == nil {
+				return written, fmt.Errorf(
+					"expected untyped in metric %s %s", name, metric,
+				)
+			}
+			n, err = writeOpenMetricsSample(
+				w, name, "", metric, "", 0,
+				metric.Untyped.GetValue(), 0, false,
+				nil,
+			)
+		case dto.MetricType_SUMMARY:
+			if metric.Summary == nil {
+				return written, fmt.Errorf(
+					"expected summary in metric %s %s", name, metric,
+				)
+			}
+			for _, q := range metric.Summary.Quantile {
+				n, err = writeOpenMetricsSample(
+					w, name, "", metric,
+					model.QuantileLabel, q.GetQuantile(),
+					q.GetValue(), 0, false,
+					nil,
+				)
+				written += n
+				if err != nil {
+					return
+				}
+			}
+			n, err = writeOpenMetricsSample(
+				w, name, "_sum", metric, "", 0,
+				metric.Summary.GetSampleSum(), 0, false,
+				nil,
+			)
+			written += n
+			if err != nil {
+				return
+			}
+			n, err = writeOpenMetricsSample(
+				w, name, "_count", metric, "", 0,
+				0, metric.Summary.GetSampleCount(), true,
+				nil,
+			)
+		case dto.MetricType_HISTOGRAM:
+			if metric.Histogram == nil {
+				return written, fmt.Errorf(
+					"expected histogram in metric %s %s", name, metric,
+				)
+			}
+			infSeen := false
+			for _, b := range metric.Histogram.Bucket {
+				n, err = writeOpenMetricsSample(
+					w, name, "_bucket", metric,
+					model.BucketLabel, b.GetUpperBound(),
+					0, b.GetCumulativeCount(), true,
+					b.Exemplar,
+				)
+				written += n
+				if err != nil {
+					return
+				}
+				if math.IsInf(b.GetUpperBound(), +1) {
+					infSeen = true
+				}
+			}
+			if !infSeen {
+				n, err = writeOpenMetricsSample(
+					w, name, "_bucket", metric,
+					model.BucketLabel, math.Inf(+1),
+					0, metric.Histogram.GetSampleCount(), true,
+					nil,
+				)
+				written += n
+				if err != nil {
+					return
+				}
+			}
+			n, err = writeOpenMetricsSample(
+				w, name, "_sum", metric, "", 0,
+				metric.Histogram.GetSampleSum(), 0, false,
+				nil,
+			)
+			written += n
+			if err != nil {
+				return
+			}
+			n, err = writeOpenMetricsSample(
+				w, name, "_count", metric, "", 0,
+				0, metric.Histogram.GetSampleCount(), true,
+				nil,
+			)
+		default:
+			return written, fmt.Errorf(
+				"unexpected type in metric %s %s", name, metric,
+			)
+		}
+		written += n
+		if err != nil {
+			return
+		}
+	}
+	return
+}
+
+// FinalizeOpenMetrics writes the final `# EOF\n` line required by OpenMetrics.
+func FinalizeOpenMetrics(w io.Writer) (written int, err error) {
+	return w.Write([]byte("# EOF\n"))
+}
+
+// writeOpenMetricsSample writes a single sample in OpenMetrics text format to
+// w, given the metric name, the metric proto message itself, optionally an
+// additional label name with a float64 value (use empty string as label name if
+// not required), the value (optionally as float64 or uint64, determined by
+// useIntValue), and optionally an exemplar (use nil if not required). The
+// function returns the number of bytes written and any error encountered.
+func writeOpenMetricsSample(
+	w enhancedWriter,
+	name, suffix string,
+	metric *dto.Metric,
+	additionalLabelName string, additionalLabelValue float64,
+	floatValue float64, intValue uint64, useIntValue bool,
+	exemplar *dto.Exemplar,
+) (int, error) {
+	var written int
+	n, err := w.WriteString(name)
+	written += n
+	if err != nil {
+		return written, err
+	}
+	if suffix != "" {
+		n, err = w.WriteString(suffix)
+		written += n
+		if err != nil {
+			return written, err
+		}
+	}
+	n, err = writeOpenMetricsLabelPairs(
+		w, metric.Label, additionalLabelName, additionalLabelValue,
+	)
+	written += n
+	if err != nil {
+		return written, err
+	}
+	err = w.WriteByte(' ')
+	written++
+	if err != nil {
+		return written, err
+	}
+	if useIntValue {
+		n, err = writeUint(w, intValue)
+	} else {
+		n, err = writeOpenMetricsFloat(w, floatValue)
+	}
+	written += n
+	if err != nil {
+		return written, err
+	}
+	if metric.TimestampMs != nil {
+		err = w.WriteByte(' ')
+		written++
+		if err != nil {
+			return written, err
+		}
+		// TODO(beorn7): Format this directly without converting to a float first.
+		n, err = writeOpenMetricsFloat(w, float64(*metric.TimestampMs)/1000)
+		written += n
+		if err != nil {
+			return written, err
+		}
+	}
+	if exemplar != nil {
+		n, err = writeExemplar(w, exemplar)
+		written += n
+		if err != nil {
+			return written, err
+		}
+	}
+	err = w.WriteByte('\n')
+	written++
+	if err != nil {
+		return written, err
+	}
+	return written, nil
+}
+
+// writeOpenMetricsLabelPairs works like writeOpenMetrics but formats the float
+// in OpenMetrics style.
+func writeOpenMetricsLabelPairs(
+	w enhancedWriter,
+	in []*dto.LabelPair,
+	additionalLabelName string, additionalLabelValue float64,
+) (int, error) {
+	if len(in) == 0 && additionalLabelName == "" {
+		return 0, nil
+	}
+	var (
+		written   int
+		separator byte = '{'
+	)
+	for _, lp := range in {
+		err := w.WriteByte(separator)
+		written++
+		if err != nil {
+			return written, err
+		}
+		n, err := w.WriteString(lp.GetName())
+		written += n
+		if err != nil {
+			return written, err
+		}
+		n, err = w.WriteString(`="`)
+		written += n
+		if err != nil {
+			return written, err
+		}
+		n, err = writeEscapedString(w, lp.GetValue(), true)
+		written += n
+		if err != nil {
+			return written, err
+		}
+		err = w.WriteByte('"')
+		written++
+		if err != nil {
+			return written, err
+		}
+		separator = ','
+	}
+	if additionalLabelName != "" {
+		err := w.WriteByte(separator)
+		written++
+		if err != nil {
+			return written, err
+		}
+		n, err := w.WriteString(additionalLabelName)
+		written += n
+		if err != nil {
+			return written, err
+		}
+		n, err = w.WriteString(`="`)
+		written += n
+		if err != nil {
+			return written, err
+		}
+		n, err = writeOpenMetricsFloat(w, additionalLabelValue)
+		written += n
+		if err != nil {
+			return written, err
+		}
+		err = w.WriteByte('"')
+		written++
+		if err != nil {
+			return written, err
+		}
+	}
+	err := w.WriteByte('}')
+	written++
+	if err != nil {
+		return written, err
+	}
+	return written, nil
+}
+
+// writeExemplar writes the provided exemplar in OpenMetrics format to w. The
+// function returns the number of bytes written and any error encountered.
+func writeExemplar(w enhancedWriter, e *dto.Exemplar) (int, error) {
+	written := 0
+	n, err := w.WriteString(" # ")
+	written += n
+	if err != nil {
+		return written, err
+	}
+	n, err = writeOpenMetricsLabelPairs(w, e.Label, "", 0)
+	written += n
+	if err != nil {
+		return written, err
+	}
+	err = w.WriteByte(' ')
+	written++
+	if err != nil {
+		return written, err
+	}
+	n, err = writeOpenMetricsFloat(w, e.GetValue())
+	written += n
+	if err != nil {
+		return written, err
+	}
+	if e.Timestamp != nil {
+		err = w.WriteByte(' ')
+		written++
+		if err != nil {
+			return written, err
+		}
+		ts, err := ptypes.Timestamp((*e).Timestamp)
+		if err != nil {
+			return written, err
+		}
+		// TODO(beorn7): Format this directly from components of ts to
+		// avoid overflow/underflow and precision issues of the float
+		// conversion.
+		n, err = writeOpenMetricsFloat(w, float64(ts.UnixNano())/1e9)
+		written += n
+		if err != nil {
+			return written, err
+		}
+	}
+	return written, nil
+}
+
+// writeOpenMetricsFloat works like writeFloat but appends ".0" if the resulting
+// number would otherwise contain neither a "." nor an "e".
+func writeOpenMetricsFloat(w enhancedWriter, f float64) (int, error) {
+	switch {
+	case f == 1:
+		return w.WriteString("1.0")
+	case f == 0:
+		return w.WriteString("0.0")
+	case f == -1:
+		return w.WriteString("-1.0")
+	case math.IsNaN(f):
+		return w.WriteString("NaN")
+	case math.IsInf(f, +1):
+		return w.WriteString("+Inf")
+	case math.IsInf(f, -1):
+		return w.WriteString("-Inf")
+	default:
+		bp := numBufPool.Get().(*[]byte)
+		*bp = strconv.AppendFloat((*bp)[:0], f, 'g', -1, 64)
+		if !bytes.ContainsAny(*bp, "e.") {
+			*bp = append(*bp, '.', '0')
+		}
+		written, err := w.Write(*bp)
+		numBufPool.Put(bp)
+		return written, err
+	}
+}
+
+// writeUint is like writeInt just for uint64.
+func writeUint(w enhancedWriter, u uint64) (int, error) {
+	bp := numBufPool.Get().(*[]byte)
+	*bp = strconv.AppendUint((*bp)[:0], u, 10)
+	written, err := w.Write(*bp)
+	numBufPool.Put(bp)
+	return written, err
+}
diff --git a/expfmt/text_create.go b/expfmt/text_create.go
index 0327865..5ba503b 100644
--- a/expfmt/text_create.go
+++ b/expfmt/text_create.go
@@ -423,9 +423,8 @@ var (
 func writeEscapedString(w enhancedWriter, v string, includeDoubleQuote bool) (int, error) {
 	if includeDoubleQuote {
 		return quotedEscaper.WriteString(w, v)
-	} else {
-		return escaper.WriteString(w, v)
 	}
+	return escaper.WriteString(w, v)
 }
 
 // writeFloat is equivalent to fmt.Fprint with a float64 argument but hardcodes
diff --git a/go.mod b/go.mod
index 649bd33..de597e7 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223
 	github.com/pkg/errors v0.8.1
 	github.com/prometheus/client_golang v1.0.0
-	github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90
+	github.com/prometheus/client_model v0.2.0
 	github.com/sirupsen/logrus v1.4.2
 	golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 // indirect
 	golang.org/x/sys v0.0.0-20190422165155-953cdadca894
diff --git a/go.sum b/go.sum
index 1228dcf..6f1b3b6 100644
--- a/go.sum
+++ b/go.sum
@@ -58,6 +58,8 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d h1:GoAlyOgbOEIFdaDqxJVlbOQ1DtGmZWs/Qau0hIlk+WQ=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=


Test patch:
diff --git a/expfmt/openmetrics_create_test.go b/expfmt/openmetrics_create_test.go
new file mode 100644
index 0000000..4a5318b
--- /dev/null
+++ b/expfmt/openmetrics_create_test.go
@@ -0,0 +1,600 @@
+// Copyright 2020 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"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/golang/protobuf/proto"
+	"github.com/golang/protobuf/ptypes"
+
+	dto "github.com/prometheus/client_model/go"
+)
+
+func TestCreateOpenMetrics(t *testing.T) {
+	openMetricsTimestamp, err := ptypes.TimestampProto(time.Unix(12345, 600000000))
+	if err != nil {
+		t.Error(err)
+	}
+
+	var scenarios = []struct {
+		in  *dto.MetricFamily
+		out string
+	}{
+		// 0: Counter, timestamp given, no _total suffix.
+		{
+			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(42),
+						},
+					},
+					&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 unknown
+name{labelname="val1",basename="basevalue"} 42.0
+name{labelname="val2",basename="basevalue"} 0.23 1.23456789e+06
+`,
+		},
+		// 1: Gauge, some escaping required, +Inf as value, multi-byte characters in label values.
+		{
+			in: &dto.MetricFamily{
+				Name: proto.String("gauge_name"),
+				Help: proto.String("gauge\ndoc\nstr\"ing"),
+				Type: dto.MetricType_GAUGE.Enum(),
+				Metric: []*dto.Metric{
+					&dto.Metric{
+						Label: []*dto.LabelPair{
+							&dto.LabelPair{
+								Name:  proto.String("name_1"),
+								Value: proto.String("val with\nnew line"),
+							},
+							&dto.LabelPair{
+								Name:  proto.String("name_2"),
+								Value: proto.String("val with \\backslash and \"quotes\""),
+							},
+						},
+						Gauge: &dto.Gauge{
+							Value: proto.Float64(math.Inf(+1)),
+						},
+					},
+					&dto.Metric{
+						Label: []*dto.LabelPair{
+							&dto.LabelPair{
+								Name:  proto.String("name_1"),
+								Value: proto.String("Björn"),
+							},
+							&dto.LabelPair{
+								Name:  proto.String("name_2"),
+								Value: proto.String("佖佥"),
+							},
+						},
+						Gauge: &dto.Gauge{
+							Value: proto.Float64(3.14e42),
+						},
+					},
+				},
+			},
+			out: `# HELP gauge_name gauge\ndoc\nstr\"ing
+# TYPE gauge_name gauge
+gauge_name{name_1="val with\nnew line",name_2="val with \\backslash and \"quotes\""} +Inf
+gauge_name{name_1="Björn",name_2="佖佥"} 3.14e+42
+`,
+		},
+		// 2: Unknown, no help, one sample with no labels and -Inf as value, another sample with one label.
+		{
+			in: &dto.MetricFamily{
+				Name: proto.String("unknown_name"),
+				Type: dto.MetricType_UNTYPED.Enum(),
+				Metric: []*dto.Metric{
+					&dto.Metric{
+						Untyped: &dto.Untyped{
+							Value: proto.Float64(math.Inf(-1)),
+						},
+					},
+					&dto.Metric{
+						Label: []*dto.LabelPair{
+							&dto.LabelPair{
+								Name:  proto.String("name_1"),
+								Value: proto.String("value 1"),
+							},
+						},
+						Untyped: &dto.Untyped{
+							Value: proto.Float64(-1.23e-45),
+						},
+					},
+				},
+			},
+			out: `# TYPE unknown_name unknown
+unknown_name -Inf
+unknown_name{name_1="value 1"} -1.23e-45
+`,
+		},
+		// 3: Summary.
+		{
+			in: &dto.MetricFamily{
+				Name: proto.String("summary_name"),
+				Help: proto.String("summary docstring"),
+				Type: dto.MetricType_SUMMARY.Enum(),
+				Metric: []*dto.Metric{
+					&dto.Metric{
+						Summary: &dto.Summary{
+							SampleCount: proto.Uint64(42),
+							SampleSum:   proto.Float64(-3.4567),
+							Quantile: []*dto.Quantile{
+								&dto.Quantile{
+									Quantile: proto.Float64(0.5),
+									Value:    proto.Float64(-1.23),
+								},
+								&dto.Quantile{
+									Quantile: proto.Float64(0.9),
+									Value:    proto.Float64(.2342354),
+								},
+								&dto.Quantile{
+									Quantile: proto.Float64(0.99),
+									Value:    proto.Float64(0),
+								},
+							},
+						},
+					},
+					&dto.Metric{
+						Label: []*dto.LabelPair{
+							&dto.LabelPair{
+								Name:  proto.String("name_1"),
+								Value: proto.String("value 1"),
+							},
+							&dto.LabelPair{
+								Name:  proto.String("name_2"),
+								Value: proto.String("value 2"),
+							},
+						},
+						Summary: &dto.Summary{
+							SampleCount: proto.Uint64(4711),
+							SampleSum:   proto.Float64(2010.1971),
+							Quantile: []*dto.Quantile{
+								&dto.Quantile{
+									Quantile: proto.Float64(0.5),
+									Value:    proto.Float64(1),
+								},
+								&dto.Quantile{
+									Quantile: proto.Float64(0.9),
+									Value:    proto.Float64(2),
+								},
+								&dto.Quantile{
+									Quantile: proto.Float64(0.99),
+									Value:    proto.Float64(3),
+								},
+							},
+						},
+					},
+				},
+			},
+			out: `# HELP summary_name summary docstring
+# TYPE summary_name summary
+summary_name{quantile="0.5"} -1.23
+summary_name{quantile="0.9"} 0.2342354
+summary_name{quantile="0.99"} 0.0
+summary_name_sum -3.4567
+summary_name_count 42
+summary_name{name_1="value 1",name_2="value 2",quantile="0.5"} 1.0
+summary_name{name_1="value 1",name_2="value 2",quantile="0.9"} 2.0
+summary_name{name_1="value 1",name_2="value 2",quantile="0.99"} 3.0
+summary_name_sum{name_1="value 1",name_2="value 2"} 2010.1971
+summary_name_count{name_1="value 1",name_2="value 2"} 4711
+`,
+		},
+		// 4: Histogram
+		{
+			in: &dto.MetricFamily{
+				Name: proto.String("request_duration_microseconds"),
+				Help: proto.String("The response latency."),
+				Type: dto.MetricType_HISTOGRAM.Enum(),
+				Metric: []*dto.Metric{
+					&dto.Metric{
+						Histogram: &dto.Histogram{
+							SampleCount: proto.Uint64(2693),
+							SampleSum:   proto.Float64(1756047.3),
+							Bucket: []*dto.Bucket{
+								&dto.Bucket{
+									UpperBound:      proto.Float64(100),
+									CumulativeCount: proto.Uint64(123),
+								},
+								&dto.Bucket{
+									UpperBound:      proto.Float64(120),
+									CumulativeCount: proto.Uint64(412),
+								},
+								&dto.Bucket{
+									UpperBound:      proto.Float64(144),
+									CumulativeCount: proto.Uint64(592),
+								},
+								&dto.Bucket{
+									UpperBound:      proto.Float64(172.8),
+									CumulativeCount: proto.Uint64(1524),
+								},
+								&dto.Bucket{
+									UpperBound:      proto.Float64(math.Inf(+1)),
+									CumulativeCount: proto.Uint64(2693),
+								},
+							},
+						},
+					},
+				},
+			},
+			out: `# HELP request_duration_microseconds The response latency.
+# TYPE request_duration_microseconds histogram
+request_duration_microseconds_bucket{le="100.0"} 123
+request_duration_microseconds_bucket{le="120.0"} 412
+request_duration_microseconds_bucket{le="144.0"} 592
+request_duration_microseconds_bucket{le="172.8"} 1524
+request_duration_microseconds_bucket{le="+Inf"} 2693
+request_duration_microseconds_sum 1.7560473e+06
+request_duration_microseconds_count 2693
+`,
+		},
+		// 5: Histogram with missing +Inf bucket.
+		{
+			in: &dto.MetricFamily{
+				Name: proto.String("request_duration_microseconds"),
+				Help: proto.String("The response latency."),
+				Type: dto.MetricType_HISTOGRAM.Enum(),
+				Metric: []*dto.Metric{
+					&dto.Metric{
+						Histogram: &dto.Histogram{
+							SampleCount: proto.Uint64(2693),
+							SampleSum:   proto.Float64(1756047.3),
+							Bucket: []*dto.Bucket{
+								&dto.Bucket{
+									UpperBound:      proto.Float64(100),
+									CumulativeCount: proto.Uint64(123),
+								},
+								&dto.Bucket{
+									UpperBound:      proto.Float64(120),
+									CumulativeCount: proto.Uint64(412),
+								},
+								&dto.Bucket{
+									UpperBound:      proto.Float64(144),
+									CumulativeCount: proto.Uint64(592),
+								},
+								&dto.Bucket{
+									UpperBound:      proto.Float64(172.8),
+									CumulativeCount: proto.Uint64(1524),
+								},
+							},
+						},
+					},
+				},
+			},
+			out: `# HELP request_duration_microseconds The response latency.
+# TYPE request_duration_microseconds histogram
+request_duration_microseconds_bucket{le="100.0"} 123
+request_duration_microseconds_bucket{le="120.0"} 412
+request_duration_microseconds_bucket{le="144.0"} 592
+request_duration_microseconds_bucket{le="172.8"} 1524
+request_duration_microseconds_bucket{le="+Inf"} 2693
+request_duration_microseconds_sum 1.7560473e+06
+request_duration_microseconds_count 2693
+`,
+		},
+		// 6: Histogram with missing +Inf bucket but with different exemplars.
+		{
+			in: &dto.MetricFamily{
+				Name: proto.String("request_duration_microseconds"),
+				Help: proto.String("The response latency."),
+				Type: dto.MetricType_HISTOGRAM.Enum(),
+				Metric: []*dto.Metric{
+					&dto.Metric{
+						Histogram: &dto.Histogram{
+							SampleCount: proto.Uint64(2693),
+							SampleSum:   proto.Float64(1756047.3),
+							Bucket: []*dto.Bucket{
+								&dto.Bucket{
+									UpperBound:      proto.Float64(100),
+									CumulativeCount: proto.Uint64(123),
+								},
+								&dto.Bucket{
+									UpperBound:      proto.Float64(120),
+									CumulativeCount: proto.Uint64(412),
+									Exemplar: &dto.Exemplar{
+										Label: []*dto.LabelPair{
+											&dto.LabelPair{
+												Name:  proto.String("foo"),
+												Value: proto.String("bar"),
+											},
+										},
+										Value:     proto.Float64(119.9),
+										Timestamp: openMetricsTimestamp,
+									},
+								},
+								&dto.Bucket{
+									UpperBound:      proto.Float64(144),
+									CumulativeCount: proto.Uint64(592),
+									Exemplar: &dto.Exemplar{
+										Label: []*dto.LabelPair{
+											&dto.LabelPair{
+												Name:  proto.String("foo"),
+												Value: proto.String("baz"),
+											},
+											&dto.LabelPair{
+												Name:  proto.String("dings"),
+												Value: proto.String("bums"),
+											},
+										},
+										Value: proto.Float64(140.14),
+									},
+								},
+								&dto.Bucket{
+									UpperBound:      proto.Float64(172.8),
+									CumulativeCount: proto.Uint64(1524),
+								},
+							},
+						},
+					},
+				},
+			},
+			out: `# HELP request_duration_microseconds The response latency.
+# TYPE request_duration_microseconds histogram
+request_duration_microseconds_bucket{le="100.0"} 123
+request_duration_microseconds_bucket{le="120.0"} 412 # {foo="bar"} 119.9 12345.6
+request_duration_microseconds_bucket{le="144.0"} 592 # {foo="baz",dings="bums"} 140.14
+request_duration_microseconds_bucket{le="172.8"} 1524
+request_duration_microseconds_bucket{le="+Inf"} 2693
+request_duration_microseconds_sum 1.7560473e+06
+request_duration_microseconds_count 2693
+`,
+		},
+		// 7: Simple Counter.
+		{
+			in: &dto.MetricFamily{
+				Name: proto.String("foos_total"),
+				Help: proto.String("Number of foos."),
+				Type: dto.MetricType_COUNTER.Enum(),
+				Metric: []*dto.Metric{
+					&dto.Metric{
+						Counter: &dto.Counter{
+							Value: proto.Float64(42),
+						},
+					},
+				},
+			},
+			out: `# HELP foos Number of foos.
+# TYPE foos counter
+foos_total 42.0
+`,
+		},
+		// 8: No metric.
+		{
+			in: &dto.MetricFamily{
+				Name:   proto.String("name_total"),
+				Help:   proto.String("doc string"),
+				Type:   dto.MetricType_COUNTER.Enum(),
+				Metric: []*dto.Metric{},
+			},
+			out: `# HELP name doc string
+# TYPE name counter
+`,
+		},
+	}
+
+	for i, scenario := range scenarios {
+		out := bytes.NewBuffer(make([]byte, 0, len(scenario.out)))
+		n, err := MetricFamilyToOpenMetrics(out, scenario.in)
+		if err != nil {
+			t.Errorf("%d. error: %s", i, err)
+			continue
+		}
+		if expected, got := len(scenario.out), n; expected != got {
+			t.Errorf(
+				"%d. expected %d bytes written, got %d",
+				i, expected, got,
+			)
+		}
+		if expected, got := scenario.out, out.String(); expected != got {
+			t.Errorf(
+				"%d. expected out=%q, got %q",
+				i, expected, got,
+			)
+		}
+	}
+
+}
+
+func BenchmarkOpenMetricsCreate(b *testing.B) {
+	mf := &dto.MetricFamily{
+		Name: proto.String("request_duration_microseconds"),
+		Help: proto.String("The response latency."),
+		Type: dto.MetricType_HISTOGRAM.Enum(),
+		Metric: []*dto.Metric{
+			&dto.Metric{
+				Label: []*dto.LabelPair{
+					&dto.LabelPair{
+						Name:  proto.String("name_1"),
+						Value: proto.String("val with\nnew line"),
+					},
+					&dto.LabelPair{
+						Name:  proto.String("name_2"),
+						Value: proto.String("val with \\backslash and \"quotes\""),
+					},
+					&dto.LabelPair{
+						Name:  proto.String("name_3"),
+						Value: proto.String("Just a quite long label value to test performance."),
+					},
+				},
+				Histogram: &dto.Histogram{
+					SampleCount: proto.Uint64(2693),
+					SampleSum:   proto.Float64(1756047.3),
+					Bucket: []*dto.Bucket{
+						&dto.Bucket{
+							UpperBound:      proto.Float64(100),
+							CumulativeCount: proto.Uint64(123),
+						},
+						&dto.Bucket{
+							UpperBound:      proto.Float64(120),
+							CumulativeCount: proto.Uint64(412),
+						},
+						&dto.Bucket{
+							UpperBound:      proto.Float64(144),
+							CumulativeCount: proto.Uint64(592),
+						},
+						&dto.Bucket{
+							UpperBound:      proto.Float64(172.8),
+							CumulativeCount: proto.Uint64(1524),
+						},
+						&dto.Bucket{
+							UpperBound:      proto.Float64(math.Inf(+1)),
+							CumulativeCount: proto.Uint64(2693),
+						},
+					},
+				},
+			},
+			&dto.Metric{
+				Label: []*dto.LabelPair{
+					&dto.LabelPair{
+						Name:  proto.String("name_1"),
+						Value: proto.String("Björn"),
+					},
+					&dto.LabelPair{
+						Name:  proto.String("name_2"),
+						Value: proto.String("佖佥"),
+					},
+					&dto.LabelPair{
+						Name:  proto.String("name_3"),
+						Value: proto.String("Just a quite long label value to test performance."),
+					},
+				},
+				Histogram: &dto.Histogram{
+					SampleCount: proto.Uint64(5699),
+					SampleSum:   proto.Float64(49484343543.4343),
+					Bucket: []*dto.Bucket{
+						&dto.Bucket{
+							UpperBound:      proto.Float64(100),
+							CumulativeCount: proto.Uint64(120),
+						},
+						&dto.Bucket{
+							UpperBound:      proto.Float64(120),
+							CumulativeCount: proto.Uint64(412),
+						},
+						&dto.Bucket{
+							UpperBound:      proto.Float64(144),
+							CumulativeCount: proto.Uint64(596),
+						},
+						&dto.Bucket{
+							UpperBound:      proto.Float64(172.8),
+							CumulativeCount: proto.Uint64(1535),
+						},
+					},
+				},
+				TimestampMs: proto.Int64(1234567890),
+			},
+		},
+	}
+	out := bytes.NewBuffer(make([]byte, 0, 1024))
+
+	for i := 0; i < b.N; i++ {
+		_, err := MetricFamilyToOpenMetrics(out, mf)
+		if err != nil {
+			b.Fatal(err)
+		}
+		out.Reset()
+	}
+}
+
+func TestOpenMetricsCreateError(t *testing.T) {
+	var scenarios = []struct {
+		in  *dto.MetricFamily
+		err string
+	}{
+		// 0: No metric name.
+		{
+			in: &dto.MetricFamily{
+				Help: proto.String("doc string"),
+				Type: dto.MetricType_UNTYPED.Enum(),
+				Metric: []*dto.Metric{
+					&dto.Metric{
+						Untyped: &dto.Untyped{
+							Value: proto.Float64(math.Inf(-1)),
+						},
+					},
+				},
+			},
+			err: "MetricFamily has no name",
+		},
+		// 1: Wrong type.
+		{
+			in: &dto.MetricFamily{
+				Name: proto.String("name"),
+				Help: proto.String("doc string"),
+				Type: dto.MetricType_COUNTER.Enum(),
+				Metric: []*dto.Metric{
+					&dto.Metric{
+						Untyped: &dto.Untyped{
+							Value: proto.Float64(math.Inf(-1)),
+						},
+					},
+				},
+			},
+			err: "expected counter in metric",
+		},
+	}
+
+	for i, scenario := range scenarios {
+		var out bytes.Buffer
+		_, err := MetricFamilyToOpenMetrics(&out, scenario.in)
+		if err == nil {
+			t.Errorf("%d. expected error, got nil", i)
+			continue
+		}
+		if expected, got := scenario.err, err.Error(); strings.Index(got, expected) != 0 {
+			t.Errorf(
+				"%d. expected error starting with %q, got %q",
+				i, expected, got,
+			)
+		}
+	}
+
+}