Repository: prometheus/common
Pull request #571 (2024-02-01 17:46:28)

Bump github.com/aws/aws-sdk-go from 1.49.13 to 1.50.8 in /sigv4
Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.49.13 to 1.50.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/aws/aws-sdk-go/releases">github.com/aws/aws-sdk-go's releases</a>.</em></p>
<blockquote>
<h1>Release v1.50.8 (2024-01-31)</h1>
<h3>Service Client Updates</h3>
<ul>
<li><code>service/cloudformation</code>: Updates service API, documentation, paginators, and examples
<ul>
<li>CloudFormation IaC generator allows you to scan existing resources in your account and select resources to generate a template for a new or existing CloudFormation stack.</li>
</ul>
</li>
<li><code>service/elasticloadbalancingv2</code>: Updates service API and documentation</li>
<li><code>service/glue</code>: Updates service API and documentation
<ul>
<li>Update page size limits for GetJobRuns and GetTriggers APIs.</li>
</ul>
</li>
<li><code>service/ssm</code>: Updates service API and documentation
<ul>
<li>This release adds an optional Duration parameter to StateManager Associations. This allows customers to specify how long an apply-only-on-cron association execution should run. Once the specified Duration is out all the ongoing cancellable commands or automations are cancelled.</li>
</ul>
</li>
</ul>
<h1>Release v1.50.7 (2024-01-30)</h1>
<h3>Service Client Updates</h3>
<ul>
<li><code>service/datazone</code>: Updates service API and documentation</li>
<li><code>service/route53</code>: Updates service documentation
<ul>
<li>Update the SDKs for text changes in the APIs.</li>
</ul>
</li>
</ul>
<h1>Release v1.50.6 (2024-01-29)</h1>
<h3>Service Client Updates</h3>
<ul>
<li><code>service/autoscaling</code>: Updates service API and documentation
<ul>
<li>EC2 Auto Scaling customers who use attribute based instance-type selection can now intuitively define their Spot instances price protection limit as a percentage of the lowest priced On-Demand instance type.</li>
</ul>
</li>
<li><code>service/comprehend</code>: Updates service documentation</li>
<li><code>service/ec2</code>: Updates service API and documentation
<ul>
<li>EC2 Fleet customers who use attribute based instance-type selection can now intuitively define their Spot instances price protection limit as a percentage of the lowest priced On-Demand instance type.</li>
</ul>
</li>
<li><code>service/mwaa</code>: Updates service API and documentation</li>
<li><code>service/rds</code>: Updates service API, documentation, waiters, paginators, and examples
<ul>
<li>Introduced support for the InsufficientDBInstanceCapacityFault error in the RDS RestoreDBClusterFromSnapshot and RestoreDBClusterToPointInTime API methods. This provides enhanced error handling, ensuring a more robust experience.</li>
</ul>
</li>
<li><code>service/snowball</code>: Updates service documentation
<ul>
<li>Modified description of createaddress to include direction to add path when providing a JSON file.</li>
</ul>
</li>
</ul>
<h1>Release v1.50.5 (2024-01-26)</h1>
<h3>Service Client Updates</h3>
<ul>
<li><code>service/connect</code>: Updates service API</li>
<li><code>service/inspector2</code>: Updates service API and documentation</li>
<li><code>service/sagemaker</code>: Updates service API and documentation
<ul>
<li>Amazon SageMaker Automatic Model Tuning now provides an API to programmatically delete tuning jobs.</li>
</ul>
</li>
</ul>
<h1>Release v1.50.4 (2024-01-25)</h1>
<h3>Service Client Updates</h3>
<ul>
<li><code>service/acm-pca</code>: Updates service API, documentation, and waiters</li>
<li><code>service/lightsail</code>: Updates service API and documentation</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/aws/aws-sdk-go/commit/3e2c844a50e888261e19e7d4c0991346580c2b98"><code>3e2c844</code></a> Release v1.50.8 (2024-01-31) (<a href="https://redirect.github.com/aws/aws-sdk-go/issues/5158">#5158</a>)</li>
<li><a href="https://github.com/aws/aws-sdk-go/commit/87be112c47d946793fd717a8136a9b346552d491"><code>87be112</code></a> Merge pull request <a href="https://redirect.github.com/aws/aws-sdk-go/issues/5155">#5155</a> from kellertk/main</li>
<li><a href="https://github.com/aws/aws-sdk-go/commit/160195237e8832c26540ce8b33990bcb2b35e5a7"><code>1601952</code></a> Release v1.50.7 (2024-01-30) (<a href="https://redirect.github.com/aws/aws-sdk-go/issues/5157">#5157</a>)</li>
<li><a href="https://github.com/aws/aws-sdk-go/commit/e8a5e0e401b4d7d35701b46995d832628eba2f37"><code>e8a5e0e</code></a> Release v1.50.6 (2024-01-29) (<a href="https://redirect.github.com/aws/aws-sdk-go/issues/5156">#5156</a>)</li>
<li><a href="https://github.com/aws/aws-sdk-go/commit/9adcf752a6f0df19247dc58bce5808f42c30ec2a"><code>9adcf75</code></a> chore: relax stale issue timings</li>
<li><a href="https://github.com/aws/aws-sdk-go/commit/8eb22a11235a6a0ed86c7cbd64a55c247aedc19a"><code>8eb22a1</code></a> chore: a more polite closed issue message</li>
<li><a href="https://github.com/aws/aws-sdk-go/commit/32e0058e28462a63fca67a181320c7ee44136b79"><code>32e0058</code></a> Release v1.50.5 (2024-01-26) (<a href="https://redirect.github.com/aws/aws-sdk-go/issues/5154">#5154</a>)</li>
<li><a href="https://github.com/aws/aws-sdk-go/commit/d5e1369ac3eaff011139a6b6797117341491e480"><code>d5e1369</code></a> Release v1.50.4 (2024-01-25) (<a href="https://redirect.github.com/aws/aws-sdk-go/issues/5152">#5152</a>)</li>
<li><a href="https://github.com/aws/aws-sdk-go/commit/15beed0ddd19a44650057734f3a2f5c68e459a7f"><code>15beed0</code></a> Release v1.50.3 (2024-01-24) (<a href="https://redirect.github.com/aws/aws-sdk-go/issues/5151">#5151</a>)</li>
<li><a href="https://github.com/aws/aws-sdk-go/commit/8fd61086a31d0194c4d472714044035504f40f31"><code>8fd6108</code></a> Release v1.50.2 (2024-01-23) (<a href="https://redirect.github.com/aws/aws-sdk-go/issues/5150">#5150</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/aws/aws-sdk-go/compare/v1.49.13...v1.50.8">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/aws/aws-sdk-go&package-manager=go_modules&previous-version=1.49.13&new-version=1.50.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/Makefile.common b/Makefile.common
index bc2a07d..5fd1782 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -62,10 +62,10 @@ SKIP_GOLANGCI_LINT :=
 GOLANGCI_LINT :=
 GOLANGCI_LINT_OPTS ?=
 GOLANGCI_LINT_VERSION ?= v1.55.2
-# golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
+# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
 # windows isn't included here because of the path separator being different.
 ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
-	ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386))
+	ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64))
 		# If we're in CI and there is an Actions file, that means the linter
 		# is being run in Actions, so we don't need to run it here.
 		ifneq (,$(SKIP_GOLANGCI_LINT))
diff --git a/expfmt/decode.go b/expfmt/decode.go
index a909b17..b2b89b0 100644
--- a/expfmt/decode.go
+++ b/expfmt/decode.go
@@ -45,7 +45,7 @@ func ResponseFormat(h http.Header) Format {
 
 	mediatype, params, err := mime.ParseMediaType(ct)
 	if err != nil {
-		return FmtUnknown
+		return fmtUnknown
 	}
 
 	const textType = "text/plain"
@@ -53,28 +53,28 @@ func ResponseFormat(h http.Header) Format {
 	switch mediatype {
 	case ProtoType:
 		if p, ok := params["proto"]; ok && p != ProtoProtocol {
-			return FmtUnknown
+			return fmtUnknown
 		}
 		if e, ok := params["encoding"]; ok && e != "delimited" {
-			return FmtUnknown
+			return fmtUnknown
 		}
-		return FmtProtoDelim
+		return fmtProtoDelim
 
 	case textType:
 		if v, ok := params["version"]; ok && v != TextVersion {
-			return FmtUnknown
+			return fmtUnknown
 		}
-		return FmtText
+		return fmtText
 	}
 
-	return FmtUnknown
+	return fmtUnknown
 }
 
 // NewDecoder returns a new decoder based on the given input format.
 // If the input format does not imply otherwise, a text format decoder is returned.
 func NewDecoder(r io.Reader, format Format) Decoder {
-	switch format {
-	case FmtProtoDelim:
+	switch format.FormatType() {
+	case TypeProtoDelim:
 		return &protoDecoder{r: r}
 	}
 	return &textDecoder{r: r}
diff --git a/expfmt/encode.go b/expfmt/encode.go
index 02b7a5e..8fd8061 100644
--- a/expfmt/encode.go
+++ b/expfmt/encode.go
@@ -22,6 +22,7 @@ import (
 	"google.golang.org/protobuf/encoding/prototext"
 
 	"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg"
+	"github.com/prometheus/common/model"
 
 	dto "github.com/prometheus/client_model/go"
 )
@@ -61,23 +62,32 @@ func (ec encoderCloser) Close() error {
 // as the support is still experimental. To include the option to negotiate
 // FmtOpenMetrics, use NegotiateOpenMetrics.
 func Negotiate(h http.Header) Format {
+	escapingScheme := Format(fmt.Sprintf("; escaping=%s", Format(model.NameEscapingScheme.String())))
 	for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) {
+		if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" {
+			switch Format(escapeParam) {
+			case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues:
+				escapingScheme = Format(fmt.Sprintf("; escaping=%s", escapeParam))
+			default:
+				// If the escaping parameter is unknown, ignore it.
+			}
+		}
 		ver := ac.Params["version"]
 		if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol {
 			switch ac.Params["encoding"] {
 			case "delimited":
-				return FmtProtoDelim
+				return fmtProtoDelim + escapingScheme
 			case "text":
-				return FmtProtoText
+				return fmtProtoText + escapingScheme
 			case "compact-text":
-				return FmtProtoCompact
+				return fmtProtoCompact + escapingScheme
 			}
 		}
 		if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
-			return FmtText
+			return fmtText + escapingScheme
 		}
 	}
-	return FmtText
+	return fmtText + escapingScheme
 }
 
 // NegotiateIncludingOpenMetrics works like Negotiate but includes
@@ -85,29 +95,40 @@ func Negotiate(h http.Header) Format {
 // 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 {
+	escapingScheme := Format(fmt.Sprintf("; escaping=%s", Format(model.NameEscapingScheme.String())))
 	for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) {
+		if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" {
+			switch Format(escapeParam) {
+			case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues:
+				escapingScheme = Format(fmt.Sprintf("; escaping=%s", escapeParam))
+			default:
+				// If the escaping parameter is unknown, ignore it.
+			}
+		}
 		ver := ac.Params["version"]
 		if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol {
 			switch ac.Params["encoding"] {
 			case "delimited":
-				return FmtProtoDelim
+				return fmtProtoDelim + escapingScheme
 			case "text":
-				return FmtProtoText
+				return fmtProtoText + escapingScheme
 			case "compact-text":
-				return FmtProtoCompact
+				return fmtProtoCompact + escapingScheme
 			}
 		}
 		if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
-			return FmtText
+			return fmtText + escapingScheme
 		}
 		if ac.Type+"/"+ac.SubType == OpenMetricsType && (ver == OpenMetricsVersion_0_0_1 || ver == OpenMetricsVersion_1_0_0 || ver == "") {
-			if ver == OpenMetricsVersion_1_0_0 {
-				return FmtOpenMetrics_1_0_0
+			switch ver {
+			case OpenMetricsVersion_1_0_0:
+				return fmtOpenMetrics_1_0_0 + escapingScheme
+			default:
+				return fmtOpenMetrics_0_0_1 + escapingScheme
 			}
-			return FmtOpenMetrics_0_0_1
 		}
 	}
-	return FmtText
+	return fmtText + escapingScheme
 }
 
 // NewEncoder returns a new encoder based on content type negotiation. All
@@ -116,9 +137,13 @@ func NegotiateIncludingOpenMetrics(h http.Header) Format {
 // 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.
+// In cases where the Format does not allow for UTF-8 names, the global
+// NameEscapingScheme will be applied.
 func NewEncoder(w io.Writer, format Format) Encoder {
-	switch format {
-	case FmtProtoDelim:
+	escapingScheme := format.ToEscapingScheme()
+
+	switch format.FormatType() {
+	case TypeProtoDelim:
 		return encoderCloser{
 			encode: func(v *dto.MetricFamily) error {
 				_, err := protodelim.MarshalTo(w, v)
@@ -126,34 +151,34 @@ func NewEncoder(w io.Writer, format Format) Encoder {
 			},
 			close: func() error { return nil },
 		}
-	case FmtProtoCompact:
+	case TypeProtoCompact:
 		return encoderCloser{
 			encode: func(v *dto.MetricFamily) error {
-				_, err := fmt.Fprintln(w, v.String())
+				_, err := fmt.Fprintln(w, model.EscapeMetricFamily(v, escapingScheme).String())
 				return err
 			},
 			close: func() error { return nil },
 		}
-	case FmtProtoText:
+	case TypeProtoText:
 		return encoderCloser{
 			encode: func(v *dto.MetricFamily) error {
-				_, err := fmt.Fprintln(w, prototext.Format(v))
+				_, err := fmt.Fprintln(w, prototext.Format(model.EscapeMetricFamily(v, escapingScheme)))
 				return err
 			},
 			close: func() error { return nil },
 		}
-	case FmtText:
+	case TypeTextPlain:
 		return encoderCloser{
 			encode: func(v *dto.MetricFamily) error {
-				_, err := MetricFamilyToText(w, v)
+				_, err := MetricFamilyToText(w, model.EscapeMetricFamily(v, escapingScheme))
 				return err
 			},
 			close: func() error { return nil },
 		}
-	case FmtOpenMetrics_0_0_1, FmtOpenMetrics_1_0_0:
+	case TypeOpenMetrics:
 		return encoderCloser{
 			encode: func(v *dto.MetricFamily) error {
-				_, err := MetricFamilyToOpenMetrics(w, v)
+				_, err := MetricFamilyToOpenMetrics(w, model.EscapeMetricFamily(v, escapingScheme))
 				return err
 			},
 			close: func() error {
diff --git a/expfmt/expfmt.go b/expfmt/expfmt.go
index d866b47..6fc9555 100644
--- a/expfmt/expfmt.go
+++ b/expfmt/expfmt.go
@@ -14,6 +14,12 @@
 // Package expfmt contains tools for reading and writing Prometheus metrics.
 package expfmt
 
+import (
+	"strings"
+
+	"github.com/prometheus/common/model"
+)
+
 // Format specifies the HTTP content type of the different wire protocols.
 type Format string
 
@@ -28,22 +34,134 @@ const (
 	TextVersion              = "0.0.4"
 	ProtoType                = `application/vnd.google.protobuf`
 	ProtoProtocol            = `io.prometheus.client.MetricFamily`
-	ProtoFmt                 = ProtoType + "; proto=" + ProtoProtocol + ";"
+	protoFmt                 = ProtoType + "; proto=" + ProtoProtocol + ";"
 	OpenMetricsType          = `application/openmetrics-text`
 	OpenMetricsVersion_0_0_1 = "0.0.1"
 	OpenMetricsVersion_1_0_0 = "1.0.0"
 
-	// The Content-Type values for the different wire protocols.
-	FmtUnknown           Format = `<unknown>`
-	FmtText              Format = `text/plain; version=` + TextVersion + `; charset=utf-8`
-	FmtProtoDelim        Format = ProtoFmt + ` encoding=delimited`
-	FmtProtoText         Format = ProtoFmt + ` encoding=text`
-	FmtProtoCompact      Format = ProtoFmt + ` encoding=compact-text`
-	FmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8`
-	FmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8`
+	// The Content-Type values for the different wire protocols. Note that these
+	// values are now unexported. If code was relying on comparisons to these
+	// constants, instead use FormatType().
+	fmtUnknown           Format = `<unknown>`
+	fmtText              Format = `text/plain; version=` + TextVersion + `; charset=utf-8`
+	fmtProtoDelim        Format = protoFmt + ` encoding=delimited`
+	fmtProtoText         Format = protoFmt + ` encoding=text`
+	fmtProtoCompact      Format = protoFmt + ` encoding=compact-text`
+	fmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8`
+	fmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8`
 )
 
 const (
 	hdrContentType = "Content-Type"
 	hdrAccept      = "Accept"
 )
+
+// FormatType is a Go enum representing the overall category for the given
+// Format. As the number of Format permutations increases, doing basic string
+// comparisons are not feasible, so this enum captures the most useful
+// high-level attribute of the Format string.
+type FormatType int
+
+const (
+	TypeUnknown = iota
+	TypeProtoCompact
+	TypeProtoDelim
+	TypeProtoText
+	TypeTextPlain
+	TypeOpenMetrics
+)
+
+// 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.
+func NewFormat(t FormatType) Format {
+	switch t {
+	case TypeProtoCompact:
+		return fmtProtoCompact
+	case TypeProtoDelim:
+		return fmtProtoDelim
+	case TypeProtoText:
+		return fmtProtoText
+	case TypeTextPlain:
+		return fmtText
+	case TypeOpenMetrics:
+		return fmtOpenMetrics_1_0_0
+	default:
+		return fmtUnknown
+	}
+}
+
+// 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 {
+			continue
+		}
+		args := strings.Split(t, "=")
+		if len(args) != 2 {
+			continue
+		}
+		params[strings.TrimSpace(args[0])] = strings.TrimSpace(args[1])
+	}
+
+	switch strings.TrimSpace(toks[0]) {
+	case ProtoType:
+		if params["proto"] != ProtoProtocol {
+			return TypeUnknown
+		}
+		switch params["encoding"] {
+		case "delimited":
+			return TypeProtoDelim
+		case "text":
+			return TypeProtoText
+		case "compact-text":
+			return TypeProtoCompact
+		default:
+			return TypeUnknown
+		}
+	case OpenMetricsType:
+		if params["charset"] != "utf-8" {
+			return TypeUnknown
+		}
+		return TypeOpenMetrics
+	case "text/plain":
+		v, ok := params["version"]
+		if !ok {
+			return TypeTextPlain
+		}
+		if v == TextVersion {
+			return TypeTextPlain
+		}
+		return TypeUnknown
+	default:
+		return TypeUnknown
+	}
+}
+
+// ToEscapingScheme returns an EscapingScheme depending on the Format. Iff the
+// Format contains a escaping=allow-utf-8 term, it will select NoEscaping. If a valid
+// "escaping" term exists, that will be used. Otherwise, the global default will
+// be returned.
+func (format Format) ToEscapingScheme() model.EscapingScheme {
+	for _, p := range strings.Split(string(format), ";") {
+		toks := strings.Split(p, "=")
+		if len(toks) != 2 {
+			continue
+		}
+		key, value := strings.TrimSpace(toks[0]), strings.TrimSpace(toks[1])
+		if key == model.EscapingKey {
+			scheme, err := model.ToEscapingScheme(value)
+			if err != nil {
+				return model.NameEscapingScheme
+			}
+			return scheme
+		}
+	}
+	return model.NameEscapingScheme
+}
diff --git a/go.mod b/go.mod
index 06abd44..95321f7 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.20
 require (
 	github.com/alecthomas/kingpin/v2 v2.4.0
 	github.com/go-kit/log v0.2.1
+	github.com/google/go-cmp v0.6.0
 	github.com/julienschmidt/httprouter v1.3.0
 	github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f
 	github.com/prometheus/client_golang v1.18.0
diff --git a/go.sum b/go.sum
index 0a1ce92..c38b5a5 100644
--- a/go.sum
+++ b/go.sum
@@ -17,7 +17,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
diff --git a/model/labels.go b/model/labels.go
index 73dafe4..3317ce2 100644
--- a/model/labels.go
+++ b/model/labels.go
@@ -172,7 +172,7 @@ func (l LabelNames) String() string {
 // A LabelValue is an associated value for a LabelName.
 type LabelValue string
 
-// IsValid returns true iff the string is a valid UTF8.
+// IsValid returns true iff the string is a valid UTF-8.
 func (lv LabelValue) IsValid() bool {
 	return utf8.ValidString(string(lv))
 }
diff --git a/model/metric.go b/model/metric.go
index 9aa0b51..0bd29b3 100644
--- a/model/metric.go
+++ b/model/metric.go
@@ -19,6 +19,25 @@ import (
 	"sort"
 	"strings"
 	"unicode/utf8"
+
+	dto "github.com/prometheus/client_model/go"
+	"google.golang.org/protobuf/proto"
+)
+
+var (
+	// NameValidationScheme determines the method of name validation to be used by
+	// all calls to IsValidMetricName() and LabelName IsValid(). Setting UTF-8 mode
+	// in isolation from other components that don't support UTF-8 may result in
+	// bugs or other undefined behavior. This value is intended to be set by
+	// UTF-8-aware binaries as part of their startup. To avoid need for locking,
+	// this value should be set once, ideally in an init(), before multiple
+	// goroutines are started.
+	NameValidationScheme = LegacyValidation
+
+	// NameEscapingScheme defines the default way that names will be
+	// escaped when presented to systems that do not support UTF-8 names. If the
+	// Content-Type "escaping" term is specified, that will override this value.
+	NameEscapingScheme = ValueEncodingEscaping
 )
 
 // ValidationScheme is a Go enum for determining how metric and label names will
@@ -31,27 +50,52 @@ const (
 	// MetricNameRE and LabelNameRE.
 	LegacyValidation ValidationScheme = iota
 
-	// UTF8Validation only requires that metric and label names be valid UTF8
+	// UTF8Validation only requires that metric and label names be valid UTF-8
 	// strings.
 	UTF8Validation
 )
 
-var (
-	// NameValidationScheme determines the method of name validation to be used by
-	// all calls to IsValidMetricName() and LabelName IsValid(). Setting UTF8 mode
-	// in isolation from other components that don't support UTF8 may result in
-	// bugs or other undefined behavior. This value is intended to be set by
-	// UTF8-aware binaries as part of their startup. To avoid need for locking,
-	// this value should be set once, ideally in an init(), before multiple
-	// goroutines are started.
-	NameValidationScheme = LegacyValidation
+type EscapingScheme int
+
+const (
+	// NoEscaping indicates that a name will not be escaped. Unescaped names that
+	// do not conform to the legacy validity check will use a new exposition
+	// format syntax that will be officially standardized in future versions.
+	NoEscaping EscapingScheme = iota
+
+	// UnderscoreEscaping replaces all legacy-invalid characters with underscores.
+	UnderscoreEscaping
+
+	// DotsEscaping is similar to UnderscoreEscaping, except that dots are
+	// converted to `_dot_` and pre-existing underscores are converted to `__`.
+	DotsEscaping
 
-	// MetricNameRE is a regular expression matching valid metric
-	// names. Note that the IsValidMetricName function performs the same
-	// check but faster than a match with this regular expression.
-	MetricNameRE = regexp.MustCompile(`^[a-zA-Z_:][a-zA-Z0-9_:]*$`)
+	// ValueEncodingEscaping prepends the name with `U__` and replaces all invalid
+	// characters with the unicode value, surrounded by underscores. Single
+	// underscores are replaced with double underscores.
+	ValueEncodingEscaping
 )
 
+const (
+	// EscapingKey is the key in an Accept or Content-Type header that defines how
+	// metric and label names that do not conform to the legacy character
+	// requirements should be escaped when being scraped by a legacy prometheus
+	// system. If a system does not explicitly pass an escaping parameter in the
+	// Accept header, the default NameEscapingScheme will be used.
+	EscapingKey = "escaping"
+
+	// Possible values for Escaping Key:
+	AllowUTF8         = "allow-utf-8" // No escaping required.
+	EscapeUnderscores = "underscores"
+	EscapeDots        = "dots"
+	EscapeValues      = "values"
+)
+
+// MetricNameRE is a regular expression matching valid metric
+// names. Note that the IsValidMetricName function performs the same
+// check but faster than a match with this regular expression.
+var MetricNameRE = regexp.MustCompile(`^[a-zA-Z_:][a-zA-Z0-9_:]*$`)
+
 // A Metric is similar to a LabelSet, but the key difference is that a Metric is
 // a singleton and refers to one and only one stream of samples.
 type Metric LabelSet
@@ -137,9 +181,276 @@ func IsValidLegacyMetricName(n LabelValue) bool {
 		return false
 	}
 	for i, b := range n {
-		if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || b == ':' || (b >= '0' && b <= '9' && i > 0)) {
+		if !isValidLegacyRune(b, i) {
 			return false
 		}
 	}
 	return true
 }
+
+// EscapeMetricFamily escapes the given metric names and labels with the given
+// escaping scheme. Returns a new object that uses the same pointers to fields
+// when possible and creates new escaped versions so as not to mutate the
+// input.
+func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricFamily {
+	if v == nil {
+		return nil
+	}
+
+	if scheme == NoEscaping {
+		return v
+	}
+
+	out := &dto.MetricFamily{
+		Help: v.Help,
+		Type: v.Type,
+	}
+
+	// If the name is nil, copy as-is, don't try to escape.
+	if v.Name == nil || IsValidLegacyMetricName(LabelValue(v.GetName())) {
+		out.Name = v.Name
+	} else {
+		out.Name = proto.String(EscapeName(v.GetName(), scheme))
+	}
+	for _, m := range v.Metric {
+		if !metricNeedsEscaping(m) {
+			out.Metric = append(out.Metric, m)
+			continue
+		}
+
+		escaped := &dto.Metric{
+			Gauge:       m.Gauge,
+			Counter:     m.Counter,
+			Summary:     m.Summary,
+			Untyped:     m.Untyped,
+			Histogram:   m.Histogram,
+			TimestampMs: m.TimestampMs,
+		}
+
+		for _, l := range m.Label {
+			if l.GetName() == MetricNameLabel {
+				if l.Value == nil || IsValidLegacyMetricName(LabelValue(l.GetValue())) {
+					escaped.Label = append(escaped.Label, l)
+					continue
+				}
+				escaped.Label = append(escaped.Label, &dto.LabelPair{
+					Name:  proto.String(MetricNameLabel),
+					Value: proto.String(EscapeName(l.GetValue(), scheme)),
+				})
+				continue
+			}
+			if l.Name == nil || IsValidLegacyMetricName(LabelValue(l.GetName())) {
+				escaped.Label = append(escaped.Label, l)
+				continue
+			}
+			escaped.Label = append(escaped.Label, &dto.LabelPair{
+				Name:  proto.String(EscapeName(l.GetName(), scheme)),
+				Value: l.Value,
+			})
+		}
+		out.Metric = append(out.Metric, escaped)
+	}
+	return out
+}
+
+func metricNeedsEscaping(m *dto.Metric) bool {
+	for _, l := range m.Label {
+		if l.GetName() == MetricNameLabel && !IsValidLegacyMetricName(LabelValue(l.GetValue())) {
+			return true
+		}
+		if !IsValidLegacyMetricName(LabelValue(l.GetName())) {
+			return true
+		}
+	}
+	return false
+}
+
+const (
+	lowerhex = "0123456789abcdef"
+)
+
+// EscapeName escapes the incoming name according to the provided escaping
+// scheme. Depending on the rules of escaping, this may cause no change in the
+// string that is returned. (Especially NoEscaping, which by definition is a
+// noop). This function does not do any validation of the name.
+func EscapeName(name string, scheme EscapingScheme) string {
+	if len(name) == 0 {
+		return name
+	}
+	var escaped strings.Builder
+	switch scheme {
+	case NoEscaping:
+		return name
+	case UnderscoreEscaping:
+		if IsValidLegacyMetricName(LabelValue(name)) {
+			return name
+		}
+		for i, b := range name {
+			if isValidLegacyRune(b, i) {
+				escaped.WriteRune(b)
+			} else {
+				escaped.WriteRune('_')
+			}
+		}
+		return escaped.String()
+	case DotsEscaping:
+		// Do not early return for legacy valid names, we still escape underscores.
+		for i, b := range name {
+			if b == '_' {
+				escaped.WriteString("__")
+			} else if b == '.' {
+				escaped.WriteString("_dot_")
+			} else if isValidLegacyRune(b, i) {
+				escaped.WriteRune(b)
+			} else {
+				escaped.WriteRune('_')
+			}
+		}
+		return escaped.String()
+	case ValueEncodingEscaping:
+		if IsValidLegacyMetricName(LabelValue(name)) {
+			return name
+		}
+		escaped.WriteString("U__")
+		for i, b := range name {
+			if isValidLegacyRune(b, i) {
+				escaped.WriteRune(b)
+			} else if !utf8.ValidRune(b) {
+				escaped.WriteString("_FFFD_")
+			} else if b < 0x100 {
+				escaped.WriteRune('_')
+				for s := 4; s >= 0; s -= 4 {
+					escaped.WriteByte(lowerhex[b>>uint(s)&0xF])
+				}
+				escaped.WriteRune('_')
+			} else if b < 0x10000 {
+				escaped.WriteRune('_')
+				for s := 12; s >= 0; s -= 4 {
+					escaped.WriteByte(lowerhex[b>>uint(s)&0xF])
+				}
+				escaped.WriteRune('_')
+			}
+		}
+		return escaped.String()
+	default:
+		panic(fmt.Sprintf("invalid escaping scheme %d", scheme))
+	}
+}
+
+// lower function taken from strconv.atoi
+func lower(c byte) byte {
+	return c | ('x' - 'X')
+}
+
+// UnescapeName unescapes the incoming name according to the provided escaping
+// scheme if possible. Some schemes are partially or totally non-roundtripable.
+// If any error is enountered, returns the original input.
+func UnescapeName(name string, scheme EscapingScheme) string {
+	if len(name) == 0 {
+		return name
+	}
+	switch scheme {
+	case NoEscaping:
+		return name
+	case UnderscoreEscaping:
+		// It is not possible to unescape from underscore replacement.
+		return name
+	case DotsEscaping:
+		name = strings.ReplaceAll(name, "_dot_", ".")
+		name = strings.ReplaceAll(name, "__", "_")
+		return name
+	case ValueEncodingEscaping:
+		escapedName, found := strings.CutPrefix(name, "U__")
+		if !found {
+			return name
+		}
+
+		var unescaped strings.Builder
+	TOP:
+		for i := 0; i < len(escapedName); i++ {
+			// All non-underscores are treated normally.
+			if escapedName[i] != '_' {
+				unescaped.WriteByte(escapedName[i])
+				continue
+			}
+			i++
+			if i >= len(escapedName) {
+				return name
+			}
+			// A double underscore is a single underscore.
+			if escapedName[i] == '_' {
+				unescaped.WriteByte('_')
+				continue
+			}
+			// We think we are in a UTF-8 code, process it.
+			var utf8Val uint
+			for j := 0; i < len(escapedName); j++ {
+				// This is too many characters for a utf8 value.
+				if j > 4 {
+					return name
+				}
+				// Found a closing underscore, convert to a rune, check validity, and append.
+				if escapedName[i] == '_' {
+					utf8Rune := rune(utf8Val)
+					if !utf8.ValidRune(utf8Rune) {
+						return name
+					}
+					unescaped.WriteRune(utf8Rune)
+					continue TOP
+				}
+				r := lower(escapedName[i])
+				utf8Val *= 16
+				if r >= '0' && r <= '9' {
+					utf8Val += uint(r) - '0'
+				} else if r >= 'a' && r <= 'f' {
+					utf8Val += uint(r) - 'a' + 10
+				} else {
+					return name
+				}
+				i++
+			}
+			// Didn't find closing underscore, invalid.
+			return name
+		}
+		return unescaped.String()
+	default:
+		panic(fmt.Sprintf("invalid escaping scheme %d", scheme))
+	}
+}
+
+func isValidLegacyRune(b rune, i int) bool {
+	return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || b == ':' || (b >= '0' && b <= '9' && i > 0)
+}
+
+func (e EscapingScheme) String() string {
+	switch e {
+	case NoEscaping:
+		return AllowUTF8
+	case UnderscoreEscaping:
+		return EscapeUnderscores
+	case DotsEscaping:
+		return EscapeDots
+	case ValueEncodingEscaping:
+		return EscapeValues
+	default:
+		panic(fmt.Sprintf("unknown format scheme %d", e))
+	}
+}
+
+func ToEscapingScheme(s string) (EscapingScheme, error) {
+	if s == "" {
+		return NoEscaping, fmt.Errorf("got empty string instead of escaping scheme")
+	}
+	switch s {
+	case AllowUTF8:
+		return NoEscaping, nil
+	case EscapeUnderscores:
+		return UnderscoreEscaping, nil
+	case EscapeDots:
+		return DotsEscaping, nil
+	case EscapeValues:
+		return ValueEncodingEscaping, nil
+	default:
+		return NoEscaping, fmt.Errorf("unknown format scheme " + s)
+	}
+}
diff --git a/sigv4/go.mod b/sigv4/go.mod
index a1b57d9..3b7c200 100644
--- a/sigv4/go.mod
+++ b/sigv4/go.mod
@@ -5,7 +5,7 @@ go 1.20
 replace github.com/prometheus/common => ../
 
 require (
-	github.com/aws/aws-sdk-go v1.49.13
+	github.com/aws/aws-sdk-go v1.50.8
 	github.com/prometheus/client_golang v1.18.0
 	github.com/prometheus/common v0.45.0
 	github.com/stretchr/testify v1.8.4
diff --git a/sigv4/go.sum b/sigv4/go.sum
index 6fe5c92..03343f4 100644
--- a/sigv4/go.sum
+++ b/sigv4/go.sum
@@ -1,5 +1,5 @@
-github.com/aws/aws-sdk-go v1.49.13 h1:f4mGztsgnx2dR9r8FQYa9YW/RsKb+N7bgef4UGrOW1Y=
-github.com/aws/aws-sdk-go v1.49.13/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
+github.com/aws/aws-sdk-go v1.50.8 h1:gY0WoOW+/Wz6XmYSgDH9ge3wnAevYDSQWPxxJvqAkP4=
+github.com/aws/aws-sdk-go v1.50.8/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
@@ -13,7 +13,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=


Test patch:
diff --git a/expfmt/decode_test.go b/expfmt/decode_test.go
index 3c023f5..e5e245d 100644
--- a/expfmt/decode_test.go
+++ b/expfmt/decode_test.go
@@ -421,27 +421,27 @@ func testDiscriminatorHTTPHeader(t testing.TB) {
 	}{
 		{
 			input:  map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="io.prometheus.client.MetricFamily"; encoding="delimited"`},
-			output: FmtProtoDelim,
+			output: fmtProtoDelim,
 		},
 		{
 			input:  map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="illegal"; encoding="delimited"`},
-			output: FmtUnknown,
+			output: fmtUnknown,
 		},
 		{
 			input:  map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="io.prometheus.client.MetricFamily"; encoding="illegal"`},
-			output: FmtUnknown,
+			output: fmtUnknown,
 		},
 		{
 			input:  map[string]string{"Content-Type": `text/plain; version=0.0.4`},
-			output: FmtText,
+			output: fmtText,
 		},
 		{
 			input:  map[string]string{"Content-Type": `text/plain`},
-			output: FmtText,
+			output: fmtText,
 		},
 		{
 			input:  map[string]string{"Content-Type": `text/plain; version=0.0.3`},
-			output: FmtUnknown,
+			output: fmtUnknown,
 		},
 	}
 
@@ -547,7 +547,7 @@ func TestTextDecoderWithBufioReader(t *testing.T) {
 
 	var decoded bool
 	r := bufio.NewReader(strings.NewReader(example))
-	dec := NewDecoder(r, FmtText)
+	dec := NewDecoder(r, fmtText)
 	for {
 		var mf dto.MetricFamily
 		if err := dec.Decode(&mf); err != nil {
diff --git a/expfmt/encode_test.go b/expfmt/encode_test.go
index 6761461..c5e4146 100644
--- a/expfmt/encode_test.go
+++ b/expfmt/encode_test.go
@@ -18,8 +18,11 @@ import (
 	"net/http"
 	"testing"
 
-	dto "github.com/prometheus/client_model/go"
 	"google.golang.org/protobuf/proto"
+
+	"github.com/prometheus/common/model"
+
+	dto "github.com/prometheus/client_model/go"
 )
 
 func TestNegotiate(t *testing.T) {
@@ -32,38 +35,70 @@ func TestNegotiate(t *testing.T) {
 		{
 			name:              "delimited format",
 			acceptHeaderValue: acceptValuePrefix + ";encoding=delimited",
-			expectedFmt:       string(FmtProtoDelim),
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=underscores",
 		},
 		{
 			name:              "text format",
 			acceptHeaderValue: acceptValuePrefix + ";encoding=text",
-			expectedFmt:       string(FmtProtoText),
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=underscores",
 		},
 		{
 			name:              "compact text format",
 			acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text",
-			expectedFmt:       string(FmtProtoCompact),
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores",
 		},
 		{
 			name:              "plain text format",
 			acceptHeaderValue: "text/plain;version=0.0.4",
-			expectedFmt:       string(FmtText),
+			expectedFmt:       "text/plain; version=0.0.4; charset=utf-8; escaping=underscores",
+		},
+		{
+			name:              "delimited format utf-8",
+			acceptHeaderValue: acceptValuePrefix + ";encoding=delimited; escaping=allow-utf-8;",
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=allow-utf-8",
+		},
+		{
+			name:              "text format utf-8",
+			acceptHeaderValue: acceptValuePrefix + ";encoding=text; escaping=allow-utf-8;",
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=allow-utf-8",
+		},
+		{
+			name:              "compact text format utf-8",
+			acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=allow-utf-8;",
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=allow-utf-8",
+		},
+		{
+			name:              "plain text format 0.0.4 with utf-8 not valid, falls back",
+			acceptHeaderValue: "text/plain;version=0.0.4;",
+			expectedFmt:       "text/plain; version=0.0.4; charset=utf-8; escaping=underscores",
+		},
+		{
+			name:              "plain text format 0.0.4 with utf-8 not valid, falls back",
+			acceptHeaderValue: "text/plain;version=0.0.4; escaping=values;",
+			expectedFmt:       "text/plain; version=0.0.4; charset=utf-8; escaping=values",
 		},
 	}
 
-	for _, test := range tests {
+	oldDefault := model.NameEscapingScheme
+	model.NameEscapingScheme = model.UnderscoreEscaping
+	defer func() {
+		model.NameEscapingScheme = oldDefault
+	}()
+
+	for i, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			h := http.Header{}
 			h.Add(hdrAccept, test.acceptHeaderValue)
 			actualFmt := string(Negotiate(h))
 			if actualFmt != test.expectedFmt {
-				t.Errorf("expected Negotiate to return format %s, but got %s instead", test.expectedFmt, actualFmt)
+				t.Errorf("case %d: expected Negotiate to return format %s, but got %s instead", i, test.expectedFmt, actualFmt)
 			}
 		})
 	}
 }
 
 func TestNegotiateOpenMetrics(t *testing.T) {
+	acceptValuePrefix := "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily"
 	tests := []struct {
 		name              string
 		acceptHeaderValue string
@@ -72,32 +107,93 @@ func TestNegotiateOpenMetrics(t *testing.T) {
 		{
 			name:              "OM format, no version",
 			acceptHeaderValue: "application/openmetrics-text",
-			expectedFmt:       string(FmtOpenMetrics_0_0_1),
+			expectedFmt:       "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=values",
 		},
 		{
 			name:              "OM format, 0.0.1 version",
-			acceptHeaderValue: "application/openmetrics-text;version=0.0.1",
-			expectedFmt:       string(FmtOpenMetrics_0_0_1),
+			acceptHeaderValue: "application/openmetrics-text;version=0.0.1; escaping=underscores",
+			expectedFmt:       "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=underscores",
 		},
 		{
 			name:              "OM format, 1.0.0 version",
 			acceptHeaderValue: "application/openmetrics-text;version=1.0.0",
-			expectedFmt:       string(FmtOpenMetrics_1_0_0),
+			expectedFmt:       "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=values",
+		},
+		{
+			name:              "OM format, 0.0.1 version with utf-8 is not valid, falls back",
+			acceptHeaderValue: "application/openmetrics-text;version=0.0.1",
+			expectedFmt:       "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=values",
+		},
+		{
+			name:              "OM format, 1.0.0 version with utf-8 is not valid, falls back",
+			acceptHeaderValue: "application/openmetrics-text;version=1.0.0; escaping=values;",
+			expectedFmt:       "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=values",
 		},
 		{
 			name:              "OM format, invalid version",
 			acceptHeaderValue: "application/openmetrics-text;version=0.0.4",
-			expectedFmt:       string(FmtText),
+			expectedFmt:       "text/plain; version=0.0.4; charset=utf-8; escaping=values",
+		},
+		{
+			name:              "compact text format",
+			acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=underscores",
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores",
+		},
+		{
+			name:              "plain text format",
+			acceptHeaderValue: "text/plain;version=0.0.4",
+			expectedFmt:       "text/plain; version=0.0.4; charset=utf-8; escaping=values",
+		},
+		{
+			name:              "plain text format 0.0.4",
+			acceptHeaderValue: "text/plain;version=0.0.4; escaping=allow-utf-8",
+			expectedFmt:       "text/plain; version=0.0.4; charset=utf-8; escaping=allow-utf-8",
+		},
+		{
+			name:              "delimited format utf-8",
+			acceptHeaderValue: acceptValuePrefix + ";encoding=delimited; escaping=allow-utf-8;",
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=allow-utf-8",
+		},
+		{
+			name:              "text format utf-8",
+			acceptHeaderValue: acceptValuePrefix + ";encoding=text; escaping=allow-utf-8;",
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=allow-utf-8",
+		},
+		{
+			name:              "compact text format utf-8",
+			acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=allow-utf-8;",
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=allow-utf-8",
+		},
+		{
+			name:              "delimited format escaped",
+			acceptHeaderValue: acceptValuePrefix + ";encoding=delimited; escaping=underscores;",
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=underscores",
+		},
+		{
+			name:              "text format escaped",
+			acceptHeaderValue: acceptValuePrefix + ";encoding=text; escaping=underscores;",
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=underscores",
+		},
+		{
+			name:              "compact text format escaped",
+			acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=underscores;",
+			expectedFmt:       "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores",
 		},
 	}
 
-	for _, test := range tests {
+	oldDefault := model.NameEscapingScheme
+	model.NameEscapingScheme = model.ValueEncodingEscaping
+	defer func() {
+		model.NameEscapingScheme = oldDefault
+	}()
+
+	for i, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			h := http.Header{}
 			h.Add(hdrAccept, test.acceptHeaderValue)
 			actualFmt := string(NegotiateIncludingOpenMetrics(h))
 			if actualFmt != test.expectedFmt {
-				t.Errorf("expected Negotiate to return format %s, but got %s instead", test.expectedFmt, actualFmt)
+				t.Errorf("case %d: expected Negotiate to return format %s, but got %s instead", i, test.expectedFmt, actualFmt)
 			}
 		})
 	}
@@ -105,7 +201,7 @@ func TestNegotiateOpenMetrics(t *testing.T) {
 
 func TestEncode(t *testing.T) {
 	var buff bytes.Buffer
-	delimEncoder := NewEncoder(&buff, FmtProtoDelim)
+	delimEncoder := NewEncoder(&buff, fmtProtoDelim)
 	metric := &dto.MetricFamily{
 		Name: proto.String("foo_metric"),
 		Type: dto.MetricType_UNTYPED.Enum(),
@@ -130,7 +226,7 @@ func TestEncode(t *testing.T) {
 
 	buff.Reset()
 
-	compactEncoder := NewEncoder(&buff, FmtProtoCompact)
+	compactEncoder := NewEncoder(&buff, fmtProtoCompact)
 	err = compactEncoder.Encode(metric)
 	if err != nil {
 		t.Errorf("unexpected error during encode: %s", err.Error())
@@ -143,7 +239,7 @@ func TestEncode(t *testing.T) {
 
 	buff.Reset()
 
-	protoTextEncoder := NewEncoder(&buff, FmtProtoText)
+	protoTextEncoder := NewEncoder(&buff, fmtProtoText)
 	err = protoTextEncoder.Encode(metric)
 	if err != nil {
 		t.Errorf("unexpected error during encode: %s", err.Error())
@@ -156,7 +252,7 @@ func TestEncode(t *testing.T) {
 
 	buff.Reset()
 
-	textEncoder := NewEncoder(&buff, FmtText)
+	textEncoder := NewEncoder(&buff, fmtText)
 	err = textEncoder.Encode(metric)
 	if err != nil {
 		t.Errorf("unexpected error during encode: %s", err.Error())
@@ -174,3 +270,88 @@ func TestEncode(t *testing.T) {
 		t.Errorf("expected TextEncoder to return %s, but got %s instead", expected, string(out))
 	}
 }
+
+func TestEscapedEncode(t *testing.T) {
+	var buff bytes.Buffer
+	delimEncoder := NewEncoder(&buff, fmtProtoDelim+"; escaping=underscores")
+	metric := &dto.MetricFamily{
+		Name: proto.String("foo.metric"),
+		Type: dto.MetricType_UNTYPED.Enum(),
+		Metric: []*dto.Metric{
+			{
+				Untyped: &dto.Untyped{
+					Value: proto.Float64(1.234),
+				},
+			},
+			{
+				Label: []*dto.LabelPair{
+					{
+						Name:  proto.String("dotted.label.name"),
+						Value: proto.String("my.label.value"),
+					},
+				},
+				Untyped: &dto.Untyped{
+					Value: proto.Float64(8),
+				},
+			},
+		},
+	}
+
+	err := delimEncoder.Encode(metric)
+	if err != nil {
+		t.Errorf("unexpected error during encode: %s", err.Error())
+	}
+
+	out := buff.Bytes()
+	if len(out) == 0 {
+		t.Errorf("expected the output bytes buffer to be non-empty")
+	}
+
+	buff.Reset()
+
+	compactEncoder := NewEncoder(&buff, fmtProtoCompact)
+	err = compactEncoder.Encode(metric)
+	if err != nil {
+		t.Errorf("unexpected error during encode: %s", err.Error())
+	}
+
+	out = buff.Bytes()
+	if len(out) == 0 {
+		t.Errorf("expected the output bytes buffer to be non-empty")
+	}
+
+	buff.Reset()
+
+	protoTextEncoder := NewEncoder(&buff, fmtProtoText)
+	err = protoTextEncoder.Encode(metric)
+	if err != nil {
+		t.Errorf("unexpected error during encode: %s", err.Error())
+	}
+
+	out = buff.Bytes()
+	if len(out) == 0 {
+		t.Errorf("expected the output bytes buffer to be non-empty")
+	}
+
+	buff.Reset()
+
+	textEncoder := NewEncoder(&buff, fmtText)
+	err = textEncoder.Encode(metric)
+	if err != nil {
+		t.Errorf("unexpected error during encode: %s", err.Error())
+	}
+
+	out = buff.Bytes()
+	if len(out) == 0 {
+		t.Errorf("expected the output bytes buffer to be non-empty")
+	}
+
+	expected := `# TYPE U__foo_2e_metric untyped
+U__foo_2e_metric 1.234
+U__foo_2e_metric{U__dotted_2e_label_2e_name="my.label.value"} 8
+`
+
+	if string(out) != expected {
+		t.Errorf("expected TextEncoder to return %s, but got %s instead", expected, string(out))
+	}
+}
diff --git a/expfmt/openmetrics_create_test.go b/expfmt/openmetrics_create_test.go
index 7b20165..8601834 100644
--- a/expfmt/openmetrics_create_test.go
+++ b/expfmt/openmetrics_create_test.go
@@ -24,6 +24,8 @@ import (
 	"google.golang.org/protobuf/types/known/timestamppb"
 
 	dto "github.com/prometheus/client_model/go"
+
+	"github.com/prometheus/common/model"
 )
 
 func TestCreateOpenMetrics(t *testing.T) {
@@ -32,6 +34,12 @@ func TestCreateOpenMetrics(t *testing.T) {
 		t.Error(err)
 	}
 
+	oldDefaultScheme := model.NameEscapingScheme
+	model.NameEscapingScheme = model.NoEscaping
+	defer func() {
+		model.NameEscapingScheme = oldDefaultScheme
+	}()
+
 	scenarios := []struct {
 		in  *dto.MetricFamily
 		out string
@@ -199,7 +207,7 @@ gauge_name{name_1="val with\nnew line",name_2="val with \\backslash and \"quotes
 gauge_name{name_1="Björn",name_2="佖佥"} 3.14e+42
 `,
 		},
-		// 4: Gauge, utf8, some escaping required, +Inf as value, multi-byte characters in label values.
+		// 4: Gauge, utf-8, some escaping required, +Inf as value, multi-byte characters in label values.
 		{
 			in: &dto.MetricFamily{
 				Name: proto.String("gauge.name\""),
diff --git a/expfmt/text_create_test.go b/expfmt/text_create_test.go
index 41bd408..7cf0428 100644
--- a/expfmt/text_create_test.go
+++ b/expfmt/text_create_test.go
@@ -22,9 +22,17 @@ import (
 	"google.golang.org/protobuf/proto"
 
 	dto "github.com/prometheus/client_model/go"
+
+	"github.com/prometheus/common/model"
 )
 
 func TestCreate(t *testing.T) {
+	oldDefaultScheme := model.NameEscapingScheme
+	model.NameEscapingScheme = model.NoEscaping
+	defer func() {
+		model.NameEscapingScheme = oldDefaultScheme
+	}()
+
 	scenarios := []struct {
 		in  *dto.MetricFamily
 		out string
@@ -120,7 +128,7 @@ gauge_name{name_1="val with\nnew line",name_2="val with \\backslash and \"quotes
 gauge_name{name_1="Björn",name_2="佖佥"} 3.14e+42
 `,
 		},
-		// 2: Gauge, utf8, +Inf as value, multi-byte characters in label values.
+		// 2: Gauge, utf-8, +Inf as value, multi-byte characters in label values.
 		{
 			in: &dto.MetricFamily{
 				Name: proto.String("gauge.name"),
diff --git a/model/labels_test.go b/model/labels_test.go
index 5ec94a4..2339543 100644
--- a/model/labels_test.go
+++ b/model/labels_test.go
@@ -153,7 +153,7 @@ func TestLabelNameIsValid(t *testing.T) {
 		}
 		NameValidationScheme = UTF8Validation
 		if s.ln.IsValid() != s.utf8Valid {
-			t.Errorf("Expected %v for %q using UTF8 IsValid method", s.legacyValid, s.ln)
+			t.Errorf("Expected %v for %q using UTF-8 IsValid method", s.legacyValid, s.ln)
 		}
 	}
 }
diff --git a/model/metric_test.go b/model/metric_test.go
index 60f8293..b794c04 100644
--- a/model/metric_test.go
+++ b/model/metric_test.go
@@ -13,7 +13,14 @@
 
 package model
 
-import "testing"
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	dto "github.com/prometheus/client_model/go"
+	"google.golang.org/protobuf/proto"
+)
 
 func testMetric(t testing.TB) {
 	scenarios := []struct {
@@ -150,7 +157,7 @@ func TestMetricNameIsLegacyValid(t *testing.T) {
 		}
 		NameValidationScheme = UTF8Validation
 		if IsValidMetricName(s.mn) != s.utf8Valid {
-			t.Errorf("Expected %v for %q using utf8 IsValidMetricName method", s.legacyValid, s.mn)
+			t.Errorf("Expected %v for %q using utf-8 IsValidMetricName method", s.legacyValid, s.mn)
 		}
 	}
 }
@@ -218,7 +225,423 @@ func TestMetricToString(t *testing.T) {
 		t.Run(scenario.name, func(t *testing.T) {
 			actual := scenario.input.String()
 			if actual != scenario.expected {
-				t.Errorf("expected string output %s but got %s", actual, scenario.expected)
+				t.Errorf("expected string output %s but got %s", scenario.expected, actual)
+			}
+		})
+	}
+}
+
+func TestEscapeName(t *testing.T) {
+	scenarios := []struct {
+		name                  string
+		input                 string
+		expectedUnderscores   string
+		expectedDots          string
+		expectedUnescapedDots string
+		expectedValue         string
+	}{
+		{
+			name: "empty string",
+		},
+		{
+			name:                "legacy valid name",
+			input:               "no:escaping_required",
+			expectedUnderscores: "no:escaping_required",
+			// Dots escaping will escape underscores even though it's not strictly
+			// necessary for compatibility.
+			expectedDots:          "no:escaping__required",
+			expectedUnescapedDots: "no:escaping_required",
+			expectedValue:         "no:escaping_required",
+		},
+		{
+			name:                  "name with dots",
+			input:                 "mysystem.prod.west.cpu.load",
+			expectedUnderscores:   "mysystem_prod_west_cpu_load",
+			expectedDots:          "mysystem_dot_prod_dot_west_dot_cpu_dot_load",
+			expectedUnescapedDots: "mysystem.prod.west.cpu.load",
+			expectedValue:         "U__mysystem_2e_prod_2e_west_2e_cpu_2e_load",
+		},
+		{
+			name:                  "name with dots and colon",
+			input:                 "http.status:sum",
+			expectedUnderscores:   "http_status:sum",
+			expectedDots:          "http_dot_status:sum",
+			expectedUnescapedDots: "http.status:sum",
+			expectedValue:         "U__http_2e_status:sum",
+		},
+		{
+			name:                "name with unicode characters > 0x100",
+			input:               "花火",
+			expectedUnderscores: "__",
+			expectedDots:        "__",
+			// Dots-replacement does not know the difference between two replaced
+			// characters and a single underscore.
+			expectedUnescapedDots: "_",
+			expectedValue:         "U___82b1__706b_",
+		},
+	}
+
+	for _, scenario := range scenarios {
+		t.Run(scenario.name, func(t *testing.T) {
+			got := EscapeName(scenario.input, UnderscoreEscaping)
+			if got != scenario.expectedUnderscores {
+				t.Errorf("expected string output %s but got %s", scenario.expectedUnderscores, got)
+			}
+			// Unescaping with the underscore method is a noop.
+			got = UnescapeName(got, UnderscoreEscaping)
+			if got != scenario.expectedUnderscores {
+				t.Errorf("expected unescaped string output %s but got %s", scenario.expectedUnderscores, got)
+			}
+
+			got = EscapeName(scenario.input, DotsEscaping)
+			if got != scenario.expectedDots {
+				t.Errorf("expected string output %s but got %s", scenario.expectedDots, got)
+			}
+			got = UnescapeName(got, DotsEscaping)
+			if got != scenario.expectedUnescapedDots {
+				t.Errorf("expected unescaped string output %s but got %s", scenario.expectedUnescapedDots, got)
+			}
+
+			got = EscapeName(scenario.input, ValueEncodingEscaping)
+			if got != scenario.expectedValue {
+				t.Errorf("expected string output %s but got %s", scenario.expectedValue, got)
+			}
+			// Unescaped result should always be identical to the original input.
+			got = UnescapeName(got, ValueEncodingEscaping)
+			if got != scenario.input {
+				t.Errorf("expected unescaped string output %s but got %s", scenario.input, got)
+			}
+		})
+	}
+}
+
+func TestValueUnescapeErrors(t *testing.T) {
+	scenarios := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name: "empty string",
+		},
+		{
+			name:     "basic case, no error",
+			input:    "U__no:unescapingrequired",
+			expected: "no:unescapingrequired",
+		},
+		{
+			name:     "capitals ok, no error",
+			input:    "U__capitals_2E_ok",
+			expected: "capitals.ok",
+		},
+		{
+			name:     "underscores, no error",
+			input:    "U__underscores__doubled__",
+			expected: "underscores_doubled_",
+		},
+		{
+			name:     "invalid single underscore",
+			input:    "U__underscores_doubled_",
+			expected: "U__underscores_doubled_",
+		},
+		{
+			name:     "invalid single underscore, 2",
+			input:    "U__underscores__doubled_",
+			expected: "U__underscores__doubled_",
+		},
+		{
+			name:     "giant fake utf-8 code",
+			input:    "U__my__hack_2e_attempt_872348732fabdabbab_",
+			expected: "U__my__hack_2e_attempt_872348732fabdabbab_",
+		},
+		{
+			name:     "trailing utf-8",
+			input:    "U__my__hack_2e",
+			expected: "U__my__hack_2e",
+		},
+		{
+			name:     "invalid utf-8 value",
+			input:    "U__bad__utf_2eg_",
+			expected: "U__bad__utf_2eg_",
+		},
+		{
+			name:     "surrogate utf-8 value",
+			input:    "U__bad__utf_D900_",
+			expected: "U__bad__utf_D900_",
+		},
+	}
+
+	for _, scenario := range scenarios {
+		t.Run(scenario.name, func(t *testing.T) {
+			got := UnescapeName(scenario.input, ValueEncodingEscaping)
+			if got != scenario.expected {
+				t.Errorf("expected unescaped string output %s but got %s", scenario.expected, got)
+			}
+		})
+	}
+}
+
+func TestEscapeMetricFamily(t *testing.T) {
+	scenarios := []struct {
+		name     string
+		input    *dto.MetricFamily
+		scheme   EscapingScheme
+		expected *dto.MetricFamily
+	}{
+		{
+			name:     "empty",
+			input:    &dto.MetricFamily{},
+			scheme:   ValueEncodingEscaping,
+			expected: &dto.MetricFamily{},
+		},
+		{
+			name:   "simple, no escaping needed",
+			scheme: ValueEncodingEscaping,
+			input: &dto.MetricFamily{
+				Name: proto.String("my_metric"),
+				Help: proto.String("some help text"),
+				Type: dto.MetricType_COUNTER.Enum(),
+				Metric: []*dto.Metric{
+					{
+						Counter: &dto.Counter{
+							Value: proto.Float64(34.2),
+						},
+						Label: []*dto.LabelPair{
+							{
+								Name:  proto.String("__name__"),
+								Value: proto.String("my_metric"),
+							},
+							{
+								Name:  proto.String("some_label"),
+								Value: proto.String("labelvalue"),
+							},
+						},
+					},
+				},
+			},
+			expected: &dto.MetricFamily{
+				Name: proto.String("my_metric"),
+				Help: proto.String("some help text"),
+				Type: dto.MetricType_COUNTER.Enum(),
+				Metric: []*dto.Metric{
+					{
+						Counter: &dto.Counter{
+							Value: proto.Float64(34.2),
+						},
+						Label: []*dto.LabelPair{
+							{
+								Name:  proto.String("__name__"),
+								Value: proto.String("my_metric"),
+							},
+							{
+								Name:  proto.String("some_label"),
+								Value: proto.String("labelvalue"),
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:   "label name escaping needed",
+			scheme: ValueEncodingEscaping,
+			input: &dto.MetricFamily{
+				Name: proto.String("my_metric"),
+				Help: proto.String("some help text"),
+				Type: dto.MetricType_COUNTER.Enum(),
+				Metric: []*dto.Metric{
+					{
+						Counter: &dto.Counter{
+							Value: proto.Float64(34.2),
+						},
+						Label: []*dto.LabelPair{
+							{
+								Name:  proto.String("__name__"),
+								Value: proto.String("my_metric"),
+							},
+							{
+								Name:  proto.String("some.label"),
+								Value: proto.String("labelvalue"),
+							},
+						},
+					},
+				},
+			},
+			expected: &dto.MetricFamily{
+				Name: proto.String("my_metric"),
+				Help: proto.String("some help text"),
+				Type: dto.MetricType_COUNTER.Enum(),
+				Metric: []*dto.Metric{
+					{
+						Counter: &dto.Counter{
+							Value: proto.Float64(34.2),
+						},
+						Label: []*dto.LabelPair{
+							{
+								Name:  proto.String("__name__"),
+								Value: proto.String("my_metric"),
+							},
+							{
+								Name:  proto.String("U__some_2e_label"),
+								Value: proto.String("labelvalue"),
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:   "counter, escaping needed",
+			scheme: ValueEncodingEscaping,
+			input: &dto.MetricFamily{
+				Name: proto.String("my.metric"),
+				Help: proto.String("some help text"),
+				Type: dto.MetricType_COUNTER.Enum(),
+				Metric: []*dto.Metric{
+					{
+						Counter: &dto.Counter{
+							Value: proto.Float64(34.2),
+						},
+						Label: []*dto.LabelPair{
+							{
+								Name:  proto.String("__name__"),
+								Value: proto.String("my.metric"),
+							},
+							{
+								Name:  proto.String("some?label"),
+								Value: proto.String("label??value"),
+							},
+						},
+					},
+				},
+			},
+			expected: &dto.MetricFamily{
+				Name: proto.String("U__my_2e_metric"),
+				Help: proto.String("some help text"),
+				Type: dto.MetricType_COUNTER.Enum(),
+				Metric: []*dto.Metric{
+					{
+						Counter: &dto.Counter{
+							Value: proto.Float64(34.2),
+						},
+						Label: []*dto.LabelPair{
+							{
+								Name:  proto.String("__name__"),
+								Value: proto.String("U__my_2e_metric"),
+							},
+							{
+								Name:  proto.String("U__some_3f_label"),
+								Value: proto.String("label??value"),
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:   "gauge, escaping needed",
+			scheme: DotsEscaping,
+			input: &dto.MetricFamily{
+				Name: proto.String("unicode.and.dots.花火"),
+				Help: proto.String("some help text"),
+				Type: dto.MetricType_GAUGE.Enum(),
+				Metric: []*dto.Metric{
+					{
+						Gauge: &dto.Gauge{
+							Value: proto.Float64(34.2),
+						},
+						Label: []*dto.LabelPair{
+							{
+								Name:  proto.String("__name__"),
+								Value: proto.String("unicode.and.dots.花火"),
+							},
+							{
+								Name:  proto.String("some_label"),
+								Value: proto.String("label??value"),
+							},
+						},
+					},
+				},
+			},
+			expected: &dto.MetricFamily{
+				Name: proto.String("unicode_dot_and_dot_dots_dot___"),
+				Help: proto.String("some help text"),
+				Type: dto.MetricType_GAUGE.Enum(),
+				Metric: []*dto.Metric{
+					{
+						Gauge: &dto.Gauge{
+							Value: proto.Float64(34.2),
+						},
+						Label: []*dto.LabelPair{
+							{
+								Name:  proto.String("__name__"),
+								Value: proto.String("unicode_dot_and_dot_dots_dot___"),
+							},
+							{
+								Name:  proto.String("some_label"),
+								Value: proto.String("label??value"),
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	unexportList := []interface{}{dto.MetricFamily{}, dto.Metric{}, dto.LabelPair{}, dto.Counter{}, dto.Gauge{}}
+
+	for _, scenario := range scenarios {
+		t.Run(scenario.name, func(t *testing.T) {
+			original := proto.Clone(scenario.input)
+			got := EscapeMetricFamily(scenario.input, scenario.scheme)
+			if !cmp.Equal(scenario.expected, got, cmpopts.IgnoreUnexported(unexportList...)) {
+				t.Errorf("unexpected difference in escaped output:" + cmp.Diff(scenario.expected, got, cmpopts.IgnoreUnexported(unexportList...)))
+			}
+			if !cmp.Equal(scenario.input, original, cmpopts.IgnoreUnexported(unexportList...)) {
+				t.Errorf("input was mutated during escaping" + cmp.Diff(scenario.expected, got, cmpopts.IgnoreUnexported(unexportList...)))
+			}
+		})
+	}
+}
+
+// TestProtoFormatUnchanged checks to see if the proto format changed, in which
+// case EscapeMetricFamily will need to be updated.
+func TestProtoFormatUnchanged(t *testing.T) {
+	scenarios := []struct {
+		name         string
+		input        proto.Message
+		expectFields []string
+	}{
+		{
+			name:         "MetricFamily",
+			input:        &dto.MetricFamily{},
+			expectFields: []string{"name", "help", "type", "metric"},
+		},
+		{
+			name:         "Metric",
+			input:        &dto.Metric{},
+			expectFields: []string{"label", "gauge", "counter", "summary", "untyped", "histogram", "timestamp_ms"},
+		},
+		{
+			name:         "LabelPair",
+			input:        &dto.LabelPair{},
+			expectFields: []string{"name", "value"},
+		},
+	}
+
+	for _, scenario := range scenarios {
+		t.Run(scenario.name, func(t *testing.T) {
+			desc := scenario.input.ProtoReflect().Descriptor()
+			fields := desc.Fields()
+			if fields.Len() != len(scenario.expectFields) {
+				t.Errorf("dto.MetricFamily changed length, expected %d, got %d", len(scenario.expectFields), fields.Len())
+			}
+
+			for i := 0; i < fields.Len(); i++ {
+				got := fields.Get(i).TextName()
+				if got != scenario.expectFields[i] {
+					t.Errorf("dto.MetricFamily field mismatch, expected %s got %s", scenario.expectFields[i], got)
+				}
 			}
 		})
 	}
diff --git a/model/silence_test.go b/model/silence_test.go
index 0b5ad32..4e4508d 100644
--- a/model/silence_test.go
+++ b/model/silence_test.go
@@ -92,7 +92,7 @@ func TestMatcherValidate(t *testing.T) {
 				t.Errorf("%d. Expected error for legacy validation %q but got none", i, c.legacyErr)
 			}
 			if c.utf8Err != "" {
-				t.Errorf("%d. Expected error for utf8 validation %q but got none", i, c.utf8Err)
+				t.Errorf("%d. Expected error for utf-8 validation %q but got none", i, c.utf8Err)
 			}
 			continue
 		}
@@ -105,7 +105,7 @@ func TestMatcherValidate(t *testing.T) {
 		}
 		if utf8Err != nil {
 			if c.utf8Err == "" {
-				t.Errorf("%d. Expected no utf8 validation error but got %q", i, utf8Err)
+				t.Errorf("%d. Expected no utf-8 validation error but got %q", i, utf8Err)
 				continue
 			}
 			if !strings.Contains(utf8Err.Error(), c.utf8Err) {