Repository: prometheus/prometheus
Pull request #11023 (2022-07-14 16:32:37)

Fix YAML and JSON doc section
Signed-off-by: Julien Pivotto <roidelapluie@o11y.eu>

<!--
    Don't forget!

    - Please sign CNCF's Developer Certificate of Origin and sign-off your commits by adding the -s / --sign-off flag to `git commit`. See https://github.com/apps/dco for more information.

    - If the PR adds or changes a behaviour or fixes a bug of an exported API it would need a unit/e2e test.

    - Where possible use only exported APIs for tests to simplify the review and make it as close as possible to an actual library usage.

    - No tests are needed for internal implementation changes.

    - Performance improvements would need a benchmark test to prove it.

    - All exposed objects should have a comment.

    - All comments should start with a capital letter and end with a full stop.
 -->



Patch:
diff --git a/.github/workflows/buf-lint.yml b/.github/workflows/buf-lint.yml
index 3b60b8267..37756adbf 100644
--- a/.github/workflows/buf-lint.yml
+++ b/.github/workflows/buf-lint.yml
@@ -10,7 +10,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
-      - uses: bufbuild/buf-setup-action@v1.6.0
+      - uses: bufbuild/buf-setup-action@v1.7.0
       - uses: bufbuild/buf-lint-action@v1
         with:
           input: 'prompb'
diff --git a/.github/workflows/buf.yml b/.github/workflows/buf.yml
index 94db35337..4fe8c86b3 100644
--- a/.github/workflows/buf.yml
+++ b/.github/workflows/buf.yml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
-      - uses: bufbuild/buf-setup-action@v1.6.0
+      - uses: bufbuild/buf-setup-action@v1.7.0
       - uses: bufbuild/buf-lint-action@v1
         with:
           input: 'prompb'
diff --git a/.github/workflows/ui_build_and_release.yml b/.github/workflows/ui_build_and_release.yml
index 630ce992c..bea176936 100644
--- a/.github/workflows/ui_build_and_release.yml
+++ b/.github/workflows/ui_build_and_release.yml
@@ -18,7 +18,7 @@ jobs:
         with:
           node-version-file: "web/ui/.nvmrc"
           registry-url: "https://registry.npmjs.org"
-      - uses: actions/cache@v3.0.4
+      - uses: actions/cache@v3.0.5
         with:
           path: ~/.npm
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go
index cacefbb20..05f0d51b9 100644
--- a/cmd/prometheus/main.go
+++ b/cmd/prometheus/main.go
@@ -58,6 +58,7 @@ import (
 	"github.com/prometheus/prometheus/discovery/targetgroup"
 	"github.com/prometheus/prometheus/model/exemplar"
 	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/model/metadata"
 	"github.com/prometheus/prometheus/model/relabel"
 	"github.com/prometheus/prometheus/notifier"
 	_ "github.com/prometheus/prometheus/plugins" // Register plugins.
@@ -178,7 +179,7 @@ func (c *flagConfig) setFeatureListOptions(logger log.Logger) error {
 				level.Info(logger).Log("msg", "Experimental memory snapshot on shutdown enabled")
 			case "extra-scrape-metrics":
 				c.scrape.ExtraMetrics = true
-				level.Info(logger).Log("msg", "Experimental additional scrape metrics")
+				level.Info(logger).Log("msg", "Experimental additional scrape metrics enabled")
 			case "new-service-discovery-manager":
 				c.enableNewSDManager = true
 				level.Info(logger).Log("msg", "Experimental service discovery manager")
@@ -191,6 +192,9 @@ func (c *flagConfig) setFeatureListOptions(logger log.Logger) error {
 			case "auto-gomaxprocs":
 				c.enableAutoGOMAXPROCS = true
 				level.Info(logger).Log("msg", "Automatically set GOMAXPROCS to match Linux container CPU quota")
+			case "no-default-scrape-port":
+				c.scrape.NoDefaultPort = true
+				level.Info(logger).Log("msg", "No default port will be appended to scrape targets' addresses.")
 			case "":
 				continue
 			case "promql-at-modifier", "promql-negative-offset":
@@ -390,7 +394,7 @@ func main() {
 	a.Flag("scrape.discovery-reload-interval", "Interval used by scrape manager to throttle target groups updates.").
 		Hidden().Default("5s").SetValue(&cfg.scrape.DiscoveryReloadInterval)
 
-	a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: agent, exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-at-modifier, promql-negative-offset, promql-per-step-stats, remote-write-receiver (DEPRECATED), extra-scrape-metrics, new-service-discovery-manager, auto-gomaxprocs. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details.").
+	a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: agent, exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-at-modifier, promql-negative-offset, promql-per-step-stats, remote-write-receiver (DEPRECATED), extra-scrape-metrics, new-service-discovery-manager, auto-gomaxprocs, no-default-scrape-port. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details.").
 		Default("").StringsVar(&cfg.featureList)
 
 	promlogflag.AddFlags(a, &cfg.promlogConfig)
@@ -1403,6 +1407,10 @@ func (n notReadyAppender) AppendExemplar(ref storage.SeriesRef, l labels.Labels,
 	return 0, tsdb.ErrNotReady
 }
 
+func (n notReadyAppender) UpdateMetadata(ref storage.SeriesRef, l labels.Labels, m metadata.Metadata) (storage.SeriesRef, error) {
+	return 0, tsdb.ErrNotReady
+}
+
 func (n notReadyAppender) Commit() error { return tsdb.ErrNotReady }
 
 func (n notReadyAppender) Rollback() error { return tsdb.ErrNotReady }
diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go
index 96d7f3851..fdb05a190 100644
--- a/cmd/promtool/main.go
+++ b/cmd/promtool/main.go
@@ -211,7 +211,7 @@ func main() {
 		"A list of one or more files containing recording rules to be backfilled. All recording rules listed in the files will be backfilled. Alerting rules are not evaluated.",
 	).Required().ExistingFiles()
 
-	featureList := app.Flag("enable-feature", "Comma separated feature names to enable (only PromQL related). See https://prometheus.io/docs/prometheus/latest/feature_flags/ for the options and more details.").Default("").Strings()
+	featureList := app.Flag("enable-feature", "Comma separated feature names to enable (only PromQL related and no-default-scrape-port). See https://prometheus.io/docs/prometheus/latest/feature_flags/ for the options and more details.").Default("").Strings()
 
 	parsedCmd := kingpin.MustParse(app.Parse(os.Args[1:]))
 
@@ -223,10 +223,13 @@ func main() {
 		p = &promqlPrinter{}
 	}
 
+	var noDefaultScrapePort bool
 	for _, f := range *featureList {
 		opts := strings.Split(f, ",")
 		for _, o := range opts {
 			switch o {
+			case "no-default-scrape-port":
+				noDefaultScrapePort = true
 			case "":
 				continue
 			case "promql-at-modifier", "promql-negative-offset":
@@ -239,7 +242,7 @@ func main() {
 
 	switch parsedCmd {
 	case sdCheckCmd.FullCommand():
-		os.Exit(CheckSD(*sdConfigFile, *sdJobName, *sdTimeout))
+		os.Exit(CheckSD(*sdConfigFile, *sdJobName, *sdTimeout, noDefaultScrapePort))
 
 	case checkConfigCmd.FullCommand():
 		os.Exit(CheckConfig(*agentMode, *checkConfigSyntaxOnly, newLintConfig(*checkConfigLint, *checkConfigLintFatal), *configFiles...))
@@ -1217,7 +1220,7 @@ func checkTargetGroupsForAlertmanager(targetGroups []*targetgroup.Group, amcfg *
 
 func checkTargetGroupsForScrapeConfig(targetGroups []*targetgroup.Group, scfg *config.ScrapeConfig) error {
 	for _, tg := range targetGroups {
-		_, failures := scrape.TargetsFromGroup(tg, scfg)
+		_, failures := scrape.TargetsFromGroup(tg, scfg, false)
 		if len(failures) > 0 {
 			first := failures[0]
 			return first
diff --git a/cmd/promtool/sd.go b/cmd/promtool/sd.go
index ce2bb3de5..981a67af1 100644
--- a/cmd/promtool/sd.go
+++ b/cmd/promtool/sd.go
@@ -37,7 +37,7 @@ type sdCheckResult struct {
 }
 
 // CheckSD performs service discovery for the given job name and reports the results.
-func CheckSD(sdConfigFiles, sdJobName string, sdTimeout time.Duration) int {
+func CheckSD(sdConfigFiles, sdJobName string, sdTimeout time.Duration, noDefaultScrapePort bool) int {
 	logger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))
 
 	cfg, err := config.LoadFile(sdConfigFiles, false, false, logger)
@@ -94,7 +94,7 @@ outerLoop:
 	}
 	results := []sdCheckResult{}
 	for _, tgs := range sdCheckResults {
-		results = append(results, getSDCheckResult(tgs, scrapeConfig)...)
+		results = append(results, getSDCheckResult(tgs, scrapeConfig, noDefaultScrapePort)...)
 	}
 
 	res, err := json.MarshalIndent(results, "", "  ")
@@ -107,7 +107,7 @@ outerLoop:
 	return successExitCode
 }
 
-func getSDCheckResult(targetGroups []*targetgroup.Group, scrapeConfig *config.ScrapeConfig) []sdCheckResult {
+func getSDCheckResult(targetGroups []*targetgroup.Group, scrapeConfig *config.ScrapeConfig, noDefaultScrapePort bool) []sdCheckResult {
 	sdCheckResults := []sdCheckResult{}
 	for _, targetGroup := range targetGroups {
 		for _, target := range targetGroup.Targets {
@@ -124,7 +124,7 @@ func getSDCheckResult(targetGroups []*targetgroup.Group, scrapeConfig *config.Sc
 			}
 
 			targetLabels := labels.New(labelSlice...)
-			res, orig, err := scrape.PopulateLabels(targetLabels, scrapeConfig)
+			res, orig, err := scrape.PopulateLabels(targetLabels, scrapeConfig, noDefaultScrapePort)
 			result := sdCheckResult{
 				DiscoveredLabels: orig,
 				Labels:           res,
diff --git a/discovery/kubernetes/endpoints.go b/discovery/kubernetes/endpoints.go
index f1fd82672..1f39c23e7 100644
--- a/discovery/kubernetes/endpoints.go
+++ b/discovery/kubernetes/endpoints.go
@@ -322,6 +322,7 @@ func (e *Endpoints) buildEndpoints(eps *apiv1.Endpoints) *targetgroup.Group {
 					ports := strconv.FormatUint(uint64(port.Port), 10)
 
 					target[podContainerNameLabel] = lv(c.Name)
+					target[podContainerImageLabel] = lv(c.Image)
 					target[podContainerPortNameLabel] = lv(cport.Name)
 					target[podContainerPortNumberLabel] = lv(ports)
 					target[podContainerPortProtocolLabel] = lv(string(port.Protocol))
@@ -380,6 +381,7 @@ func (e *Endpoints) buildEndpoints(eps *apiv1.Endpoints) *targetgroup.Group {
 				target := model.LabelSet{
 					model.AddressLabel:            lv(a),
 					podContainerNameLabel:         lv(c.Name),
+					podContainerImageLabel:        lv(c.Image),
 					podContainerPortNameLabel:     lv(cport.Name),
 					podContainerPortNumberLabel:   lv(ports),
 					podContainerPortProtocolLabel: lv(string(cport.Protocol)),
diff --git a/discovery/kubernetes/endpointslice.go b/discovery/kubernetes/endpointslice.go
index 6d4c2c8f2..594759f45 100644
--- a/discovery/kubernetes/endpointslice.go
+++ b/discovery/kubernetes/endpointslice.go
@@ -350,6 +350,7 @@ func (e *EndpointSlice) buildEndpointSlice(eps endpointSliceAdaptor) *targetgrou
 					ports := strconv.FormatUint(uint64(*port.port()), 10)
 
 					target[podContainerNameLabel] = lv(c.Name)
+					target[podContainerImageLabel] = lv(c.Image)
 					target[podContainerPortNameLabel] = lv(cport.Name)
 					target[podContainerPortNumberLabel] = lv(ports)
 					target[podContainerPortProtocolLabel] = lv(string(cport.Protocol))
@@ -398,6 +399,7 @@ func (e *EndpointSlice) buildEndpointSlice(eps endpointSliceAdaptor) *targetgrou
 				target := model.LabelSet{
 					model.AddressLabel:            lv(a),
 					podContainerNameLabel:         lv(c.Name),
+					podContainerImageLabel:        lv(c.Image),
 					podContainerPortNameLabel:     lv(cport.Name),
 					podContainerPortNumberLabel:   lv(ports),
 					podContainerPortProtocolLabel: lv(string(cport.Protocol)),
diff --git a/discovery/kubernetes/pod.go b/discovery/kubernetes/pod.go
index 2e55dce78..e19a33c1a 100644
--- a/discovery/kubernetes/pod.go
+++ b/discovery/kubernetes/pod.go
@@ -177,6 +177,7 @@ const (
 	podNameLabel                  = metaLabelPrefix + "pod_name"
 	podIPLabel                    = metaLabelPrefix + "pod_ip"
 	podContainerNameLabel         = metaLabelPrefix + "pod_container_name"
+	podContainerImageLabel        = metaLabelPrefix + "pod_container_image"
 	podContainerPortNameLabel     = metaLabelPrefix + "pod_container_port_name"
 	podContainerPortNumberLabel   = metaLabelPrefix + "pod_container_port_number"
 	podContainerPortProtocolLabel = metaLabelPrefix + "pod_container_port_protocol"
@@ -266,9 +267,10 @@ func (p *Pod) buildPod(pod *apiv1.Pod) *targetgroup.Group {
 			// We don't have a port so we just set the address label to the pod IP.
 			// The user has to add a port manually.
 			tg.Targets = append(tg.Targets, model.LabelSet{
-				model.AddressLabel:    lv(pod.Status.PodIP),
-				podContainerNameLabel: lv(c.Name),
-				podContainerIsInit:    lv(strconv.FormatBool(isInit)),
+				model.AddressLabel:     lv(pod.Status.PodIP),
+				podContainerNameLabel:  lv(c.Name),
+				podContainerImageLabel: lv(c.Image),
+				podContainerIsInit:     lv(strconv.FormatBool(isInit)),
 			})
 			continue
 		}
@@ -280,6 +282,7 @@ func (p *Pod) buildPod(pod *apiv1.Pod) *targetgroup.Group {
 			tg.Targets = append(tg.Targets, model.LabelSet{
 				model.AddressLabel:            lv(addr),
 				podContainerNameLabel:         lv(c.Name),
+				podContainerImageLabel:        lv(c.Image),
 				podContainerPortNumberLabel:   lv(ports),
 				podContainerPortNameLabel:     lv(port.Name),
 				podContainerPortProtocolLabel: lv(string(port.Protocol)),
diff --git a/discovery/kubernetes/service.go b/discovery/kubernetes/service.go
index 4b3c4de51..3db48ab6e 100644
--- a/discovery/kubernetes/service.go
+++ b/discovery/kubernetes/service.go
@@ -150,6 +150,7 @@ const (
 	serviceAnnotationPrefix        = metaLabelPrefix + "service_annotation_"
 	serviceAnnotationPresentPrefix = metaLabelPrefix + "service_annotationpresent_"
 	servicePortNameLabel           = metaLabelPrefix + "service_port_name"
+	servicePortNumberLabel         = metaLabelPrefix + "service_port_number"
 	servicePortProtocolLabel       = metaLabelPrefix + "service_port_protocol"
 	serviceClusterIPLabel          = metaLabelPrefix + "service_cluster_ip"
 	serviceExternalNameLabel       = metaLabelPrefix + "service_external_name"
@@ -189,6 +190,7 @@ func (s *Service) buildService(svc *apiv1.Service) *targetgroup.Group {
 		labelSet := model.LabelSet{
 			model.AddressLabel:       lv(addr),
 			servicePortNameLabel:     lv(port.Name),
+			servicePortNumberLabel:   lv(strconv.FormatInt(int64(port.Port), 10)),
 			servicePortProtocolLabel: lv(string(port.Protocol)),
 			serviceType:              lv(string(svc.Spec.Type)),
 		}
diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md
index a2875da9c..a0c8e2142 100644
--- a/docs/configuration/configuration.md
+++ b/docs/configuration/configuration.md
@@ -1269,6 +1269,7 @@ changes resulting in well-formed target groups are applied.
 Files must contain a list of static configs, using these formats:
 
 **JSON**
+
 ```json
 [
   {
@@ -1282,6 +1283,7 @@ Files must contain a list of static configs, using these formats:
 ```
 
 **YAML**
+
 ```yaml
 - targets:
   [ - '<host>' ]
@@ -1675,6 +1677,7 @@ Available meta labels:
 * `__meta_kubernetes_service_labelpresent_<labelname>`: `true` for each label of the service object.
 * `__meta_kubernetes_service_name`: The name of the service object.
 * `__meta_kubernetes_service_port_name`: Name of the service port for the target.
+* `__meta_kubernetes_service_port_number`: Number of the service port for the target.
 * `__meta_kubernetes_service_port_protocol`: Protocol of the service port for the target.
 * `__meta_kubernetes_service_type`: The type of the service.
 
diff --git a/docs/configuration/template_reference.md b/docs/configuration/template_reference.md
index 5c96ee597..12effae32 100644
--- a/docs/configuration/template_reference.md
+++ b/docs/configuration/template_reference.md
@@ -51,13 +51,14 @@ If functions are used in a pipeline, the pipeline value is passed as the last ar
 
 ### Numbers
 
-| Name               | Arguments        | Returns |  Notes    |
-| ------------------ | -----------------| --------| --------- |
-| humanize           | number or string | string  | Converts a number to a more readable format, using [metric prefixes](https://en.wikipedia.org/wiki/Metric_prefix).
-| humanize1024       | number or string | string  | Like `humanize`, but uses 1024 as the base rather than 1000. |
-| humanizeDuration   | number or string | string  | Converts a duration in seconds to a more readable format. |
-| humanizePercentage | number or string | string  | Converts a ratio value to a fraction of 100. |
-| humanizeTimestamp  | number or string | string  | Converts a Unix timestamp in seconds to a more readable format. |
+| Name                | Arguments        | Returns |  Notes    |
+|---------------------| -----------------| --------| --------- |
+| humanize            | number or string | string  | Converts a number to a more readable format, using [metric prefixes](https://en.wikipedia.org/wiki/Metric_prefix).
+| humanize1024        | number or string | string  | Like `humanize`, but uses 1024 as the base rather than 1000. |
+| humanizeDuration    | number or string | string  | Converts a duration in seconds to a more readable format. |
+| humanizePercentage  | number or string | string  | Converts a ratio value to a fraction of 100. |
+| humanizeTimestamp   | number or string | string     | Converts a Unix timestamp in seconds to a more readable format. |
+| toTime              | number or string | *time.Time | Converts a Unix timestamp in seconds to a time.Time.            |
 
 Humanizing functions are intended to produce reasonable output for consumption
 by humans, and are not guaranteed to return the same results between Prometheus
diff --git a/docs/feature_flags.md b/docs/feature_flags.md
index dfeaeb0dd..ddf10798d 100644
--- a/docs/feature_flags.md
+++ b/docs/feature_flags.md
@@ -94,3 +94,12 @@ computed at all.
 `--enable-feature=auto-gomaxprocs`
 
 When enabled, GOMAXPROCS variable is automatically set to match Linux container CPU quota.
+
+## No default scrape port
+
+`--enable-feature=no-default-scrape-port`
+
+When enabled, the default ports for HTTP (`:80`) or HTTPS (`:443`) will _not_ be added to
+the address used to scrape a target (the value of the `__address_` label), contrary to the default behavior.
+In addition, if a default HTTP or HTTPS port has already been added either in a static configuration or
+by a service discovery mechanism and the respective scheme is specified (`http` or `https`), that port will be removed.
diff --git a/docs/querying/api.md b/docs/querying/api.md
index 8428fc332..5d6024e51 100644
--- a/docs/querying/api.md
+++ b/docs/querying/api.md
@@ -206,6 +206,35 @@ $ curl 'http://localhost:9090/api/v1/query_range?query=up&start=2015-07-01T20:10
 }
 ```
 
+## Formatting query expressions
+
+The following endpoint formats a PromQL expression in a prettified way:
+
+```
+GET /api/v1/format_query
+POST /api/v1/format_query
+```
+
+URL query parameters:
+
+- `query=<string>`: Prometheus expression query string.
+
+You can URL-encode these parameters directly in the request body by using the `POST` method and
+`Content-Type: application/x-www-form-urlencoded` header. This is useful when specifying a large
+query that may breach server-side URL character limits.
+
+The `data` section of the query result is a string containing the formatted query expression. Note that any comments are removed in the formatted string.
+
+The following example formats the expression `foo/bar`:
+
+```json
+$ curl 'http://localhost:9090/api/v1/format_query?query=foo/bar'
+{
+   "status" : "success",
+   "data" : "foo / bar"
+}
+```
+
 ## Querying metadata
 
 Prometheus offers a set of API endpoints to query metadata about series and their labels.
diff --git a/documentation/examples/remote_storage/go.mod b/documentation/examples/remote_storage/go.mod
index 2c80a60b7..2fc50d15a 100644
--- a/documentation/examples/remote_storage/go.mod
+++ b/documentation/examples/remote_storage/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/golang/snappy v0.0.4
 	github.com/influxdata/influxdb v1.9.8
 	github.com/prometheus/client_golang v1.12.2
-	github.com/prometheus/common v0.36.0
+	github.com/prometheus/common v0.37.0
 	github.com/stretchr/testify v1.8.0
 	gopkg.in/alecthomas/kingpin.v2 v2.2.6
 )
diff --git a/documentation/examples/remote_storage/go.sum b/documentation/examples/remote_storage/go.sum
index 76dd06383..a8306ff63 100644
--- a/documentation/examples/remote_storage/go.sum
+++ b/documentation/examples/remote_storage/go.sum
@@ -1002,8 +1002,8 @@ github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9
 github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
 github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
 github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
-github.com/prometheus/common v0.36.0 h1:78hJTing+BLYLjhXE+Z2BubeEymH5Lr0/Mt8FKkxxYo=
-github.com/prometheus/common v0.36.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
+github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
+github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
 github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI=
 github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4=
 github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI=
diff --git a/documentation/prometheus-mixin/alerts.libsonnet b/documentation/prometheus-mixin/alerts.libsonnet
index 524e36ec0..0ee5d83c7 100644
--- a/documentation/prometheus-mixin/alerts.libsonnet
+++ b/documentation/prometheus-mixin/alerts.libsonnet
@@ -320,7 +320,7 @@
           {
             alert: 'PrometheusHighQueryLoad',
             expr: |||
-              avg_over_time(prometheus_engine_queries{job="prometheus-k8s"}[5m]) / max_over_time(prometheus_engine_queries_concurrent_max{job="prometheus-k8s"}[5m]) > 0.8
+              avg_over_time(prometheus_engine_queries{%(prometheusSelector)s}[5m]) / max_over_time(prometheus_engine_queries_concurrent_max{%(prometheusSelector)s}[5m]) > 0.8
             ||| % $._config,
             'for': '15m',
             labels: {
diff --git a/go.mod b/go.mod
index e51a01d63..46559811b 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/Azure/go-autorest/autorest v0.11.27
 	github.com/Azure/go-autorest/autorest/adal v0.9.20
 	github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137
-	github.com/aws/aws-sdk-go v1.44.47
+	github.com/aws/aws-sdk-go v1.44.67
 	github.com/cespare/xxhash/v2 v2.1.2
 	github.com/dennwc/varint v1.0.0
 	github.com/dgryski/go-sip13 v0.0.0-20200911182023-62edffca9245
@@ -27,10 +27,10 @@ require (
 	github.com/gophercloud/gophercloud v0.25.0
 	github.com/grafana/regexp v0.0.0-20220304095617-2e8d9baf4ac2
 	github.com/grpc-ecosystem/grpc-gateway v1.16.0
-	github.com/hashicorp/consul/api v1.13.0
+	github.com/hashicorp/consul/api v1.13.1
 	github.com/hashicorp/nomad/api v0.0.0-20220629141207-c2428e1673ec
 	github.com/hetznercloud/hcloud-go v1.35.0
-	github.com/ionos-cloud/sdk-go/v6 v6.1.0
+	github.com/ionos-cloud/sdk-go/v6 v6.1.1
 	github.com/json-iterator/go v1.1.12
 	github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b
 	github.com/linode/linodego v1.8.0
@@ -51,12 +51,12 @@ require (
 	github.com/stretchr/testify v1.8.0
 	github.com/vultr/govultr/v2 v2.17.2
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.32.0
-	go.opentelemetry.io/otel v1.7.0
+	go.opentelemetry.io/otel v1.8.0
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.7.0
 	go.opentelemetry.io/otel/sdk v1.7.0
-	go.opentelemetry.io/otel/trace v1.7.0
+	go.opentelemetry.io/otel/trace v1.8.0
 	go.uber.org/atomic v1.9.0
 	go.uber.org/automaxprocs v1.5.1
 	go.uber.org/goleak v1.1.12
@@ -66,15 +66,15 @@ require (
 	golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b
 	golang.org/x/time v0.0.0-20220609170525-579cf78fd858
 	golang.org/x/tools v0.1.11
-	google.golang.org/api v0.86.0
+	google.golang.org/api v0.90.0
 	google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03
-	google.golang.org/grpc v1.47.0
+	google.golang.org/grpc v1.48.0
 	google.golang.org/protobuf v1.28.0
 	gopkg.in/alecthomas/kingpin.v2 v2.2.6
 	gopkg.in/yaml.v2 v2.4.0
 	gopkg.in/yaml.v3 v3.0.1
 	k8s.io/api v0.24.2
-	k8s.io/apimachinery v0.24.2
+	k8s.io/apimachinery v0.24.3
 	k8s.io/client-go v0.24.2
 	k8s.io/klog v1.0.0
 	k8s.io/klog/v2 v2.70.0
@@ -101,7 +101,7 @@ require (
 	github.com/docker/distribution v2.7.1+incompatible // indirect
 	github.com/docker/go-connections v0.4.0 // indirect
 	github.com/docker/go-units v0.4.0 // indirect
-	github.com/emicklei/go-restful v2.9.5+incompatible // indirect
+	github.com/emicklei/go-restful v2.16.0+incompatible // indirect
 	github.com/evanphx/json-patch v4.12.0+incompatible // indirect
 	github.com/fatih/color v1.13.0 // indirect
 	github.com/felixge/httpsnoop v1.0.3 // indirect
diff --git a/go.sum b/go.sum
index c38afd512..9dc3e70ba 100644
--- a/go.sum
+++ b/go.sum
@@ -124,8 +124,8 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ
 github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
 github.com/aws/aws-sdk-go v1.43.11/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
-github.com/aws/aws-sdk-go v1.44.47 h1:uyiNvoR4wfZ8Bp4ghgbyzGFIg5knjZMUAd5S9ba9qNU=
-github.com/aws/aws-sdk-go v1.44.47/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
+github.com/aws/aws-sdk-go v1.44.67 h1:+nxfXbMe8QUB6svLsuLYsp+WhZBKM26w62Zidir739A=
+github.com/aws/aws-sdk-go v1.44.67/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -206,8 +206,9 @@ github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
 github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
-github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
 github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/emicklei/go-restful v2.16.0+incompatible h1:rgqiKNjTnFQA6kkhFe16D8epTksy9HQ1MyrbDXSdYhM=
+github.com/emicklei/go-restful v2.16.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -470,11 +471,11 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4Zs
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.2 h1:ERKrevVTnCw3Wu4I3mtR15QU3gtWy86cBo6De0jEohg=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.2/go.mod h1:chrfS3YoLAlKTRE5cFWvCbt8uGAjshktT4PveTUpsFQ=
 github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
-github.com/hashicorp/consul/api v1.13.0 h1:2hnLQ0GjQvw7f3O61jMO8gbasZviZTrt9R8WzgiirHc=
-github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
+github.com/hashicorp/consul/api v1.13.1 h1:r5cPdVFUy+pFF7nt+0ArLD9hm+E39OewJkvNdjKXcL4=
+github.com/hashicorp/consul/api v1.13.1/go.mod h1:+1VcOos0TVdQFqXxphG4zmGcwQB4KVGkp1maPqnkDpE=
 github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
-github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU=
-github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
+github.com/hashicorp/consul/sdk v0.10.0 h1:rGLEh2AWK4K0KCMvqWAz2EYxQqgciIfMagWZ0nVe5MI=
+github.com/hashicorp/consul/sdk v0.10.0/go.mod h1:yPkX5Q6CsxTFMjQQDJwzeNmUUF5NUGGbrDsv9wTb8cw=
 github.com/hashicorp/cronexpr v1.1.1 h1:NJZDd87hGXjoZBdvyCF9mX4DCq5Wy7+A/w+A7q0wn6c=
 github.com/hashicorp/cronexpr v1.1.1/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
@@ -540,8 +541,8 @@ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
-github.com/ionos-cloud/sdk-go/v6 v6.1.0 h1:0EZz5H+t6W23zHt6dgHYkKavr72/30O9nA97E3FZaS4=
-github.com/ionos-cloud/sdk-go/v6 v6.1.0/go.mod h1:Ox3W0iiEz0GHnfY9e5LmAxwklsxguuNFEUSu0gVRTME=
+github.com/ionos-cloud/sdk-go/v6 v6.1.1 h1:3JoVhAVIUNxRG61EiO/kJgfYDgfnt9k8MaV0zUyLCgs=
+github.com/ionos-cloud/sdk-go/v6 v6.1.1/go.mod h1:Ox3W0iiEz0GHnfY9e5LmAxwklsxguuNFEUSu0gVRTME=
 github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@@ -880,8 +881,9 @@ go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.32.0 h1:mac9BKRqwaX6zxHPDe3pvmWpwuuIM0vuXv2juCnQevE=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.32.0/go.mod h1:5eCOqeGphOyz6TsY3ZDNjE33SM/TFAK3RGuCL2naTgY=
-go.opentelemetry.io/otel v1.7.0 h1:Z2lA3Tdch0iDcrhJXDIlC94XE+bxok1F9B+4Lz/lGsM=
 go.opentelemetry.io/otel v1.7.0/go.mod h1:5BdUoMIz5WEs0vt0CUEMtSSaTSHBBVwrhnz7+nrD5xk=
+go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg=
+go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM=
 go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0 h1:7Yxsak1q4XrJ5y7XBnNwqWx9amMZvoidCctv62XOQ6Y=
 go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0/go.mod h1:M1hVZHNxcbkAlcvrOMlpQ4YOO3Awf+4N2dxkZL3xm04=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0 h1:cMDtmgJ5FpRvqx9x2Aq+Mm0O6K/zcUkH73SFz20TuBw=
@@ -894,8 +896,9 @@ go.opentelemetry.io/otel/metric v0.30.0 h1:Hs8eQZ8aQgs0U49diZoaS6Uaxw3+bBE3lcMUK
 go.opentelemetry.io/otel/metric v0.30.0/go.mod h1:/ShZ7+TS4dHzDFmfi1kSXMhMVubNoP0oIaBp70J6UXU=
 go.opentelemetry.io/otel/sdk v1.7.0 h1:4OmStpcKVOfvDOgCt7UriAPtKolwIhxpnSNI/yK+1B0=
 go.opentelemetry.io/otel/sdk v1.7.0/go.mod h1:uTEOTwaqIVuTGiJN7ii13Ibp75wJmYUDe374q6cZwUU=
-go.opentelemetry.io/otel/trace v1.7.0 h1:O37Iogk1lEkMRXewVtZ1BBTVn5JEp8GrJvP92bJqC6o=
 go.opentelemetry.io/otel/trace v1.7.0/go.mod h1:fzLSB9nqR2eXzxPXb2JW9IKE+ScyXA48yyE4TNvoHqU=
+go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY=
+go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
 go.opentelemetry.io/proto/otlp v0.16.0 h1:WHzDWdXUvbc5bG2ObdrGfaNpQz7ft7QN9HHmJlbiB1E=
@@ -1109,7 +1112,6 @@ golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1306,8 +1308,8 @@ google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69
 google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
 google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
 google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
-google.golang.org/api v0.86.0 h1:ZAnyOHQFIuWso1BodVfSaRyffD74T9ERGFa3k1fNk/U=
-google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
+google.golang.org/api v0.90.0 h1:WMnUWAvihIClUYFNeFA69VTuR3duKS3IalMGDQcLvq8=
+google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1435,8 +1437,9 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5
 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
 google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
 google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
 google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w=
+google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -1503,8 +1506,9 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 k8s.io/api v0.24.2 h1:g518dPU/L7VRLxWfcadQn2OnsiGWVOadTLpdnqgY2OI=
 k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg=
-k8s.io/apimachinery v0.24.2 h1:5QlH9SL2C8KMcrNJPor+LbXVTaZRReml7svPEh4OKDM=
 k8s.io/apimachinery v0.24.2/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM=
+k8s.io/apimachinery v0.24.3 h1:hrFiNSA2cBZqllakVYyH/VyEh4B581bQRmqATJSeQTg=
+k8s.io/apimachinery v0.24.3/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM=
 k8s.io/client-go v0.24.2 h1:CoXFSf8if+bLEbinDqN9ePIDGzcLtqhfd6jpfnwGOFA=
 k8s.io/client-go v0.24.2/go.mod h1:zg4Xaoo+umDsfCWr4fCnmLEtQXyCNXCvJuSsglNcV30=
 k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
diff --git a/model/metadata/metadata.go b/model/metadata/metadata.go
new file mode 100644
index 000000000..f227af0b9
--- /dev/null
+++ b/model/metadata/metadata.go
@@ -0,0 +1,23 @@
+// Copyright 2022 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metadata
+
+import "github.com/prometheus/prometheus/model/textparse"
+
+// Metadata stores a series' metadata information.
+type Metadata struct {
+	Type textparse.MetricType
+	Unit string
+	Help string
+}
diff --git a/promql/engine.go b/promql/engine.go
index a6b3cc723..34ea2a3e9 100644
--- a/promql/engine.go
+++ b/promql/engine.go
@@ -128,6 +128,8 @@ type Query interface {
 type QueryOpts struct {
 	// Enables recording per-step statistics if the engine has it enabled as well. Disabled by default.
 	EnablePerStepStats bool
+	// Lookback delta duration for this query.
+	LookbackDelta time.Duration
 }
 
 // query implements the Query interface.
@@ -437,11 +439,17 @@ func (ng *Engine) newQuery(q storage.Queryable, opts *QueryOpts, expr parser.Exp
 		opts = &QueryOpts{}
 	}
 
+	lookbackDelta := opts.LookbackDelta
+	if lookbackDelta <= 0 {
+		lookbackDelta = ng.lookbackDelta
+	}
+
 	es := &parser.EvalStmt{
-		Expr:     PreprocessExpr(expr, start, end),
-		Start:    start,
-		End:      end,
-		Interval: interval,
+		Expr:          PreprocessExpr(expr, start, end),
+		Start:         start,
+		End:           end,
+		Interval:      interval,
+		LookbackDelta: lookbackDelta,
 	}
 	qry := &query{
 		stmt:        es,
@@ -636,7 +644,7 @@ func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.Eval
 			ctx:                      ctxInnerEval,
 			maxSamples:               ng.maxSamplesPerQuery,
 			logger:                   ng.logger,
-			lookbackDelta:            ng.lookbackDelta,
+			lookbackDelta:            s.LookbackDelta,
 			samplesStats:             query.sampleStats,
 			noStepSubqueryIntervalFn: ng.noStepSubqueryIntervalFn,
 		}
@@ -688,7 +696,7 @@ func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.Eval
 		ctx:                      ctxInnerEval,
 		maxSamples:               ng.maxSamplesPerQuery,
 		logger:                   ng.logger,
-		lookbackDelta:            ng.lookbackDelta,
+		lookbackDelta:            s.LookbackDelta,
 		samplesStats:             query.sampleStats,
 		noStepSubqueryIntervalFn: ng.noStepSubqueryIntervalFn,
 	}
@@ -801,7 +809,7 @@ func (ng *Engine) getTimeRangesForSelector(s *parser.EvalStmt, n *parser.VectorS
 	}
 
 	if evalRange == 0 {
-		start = start - durationMilliseconds(ng.lookbackDelta)
+		start = start - durationMilliseconds(s.LookbackDelta)
 	} else {
 		// For all matrix queries we want to ensure that we have (end-start) + range selected
 		// this way we have `range` data before the start time
diff --git a/promql/parser/ast.go b/promql/parser/ast.go
index 89e6fb6da..7ae7cb112 100644
--- a/promql/parser/ast.go
+++ b/promql/parser/ast.go
@@ -68,6 +68,8 @@ type EvalStmt struct {
 	Start, End time.Time
 	// Time between two evaluated instants for the range [Start:End].
 	Interval time.Duration
+	// Lookback delta to use for this evaluation.
+	LookbackDelta time.Duration
 }
 
 func (*EvalStmt) PromQLStmt() {}
diff --git a/promql/parser/printer.go b/promql/parser/printer.go
index f053c167f..1f15eeef3 100644
--- a/promql/parser/printer.go
+++ b/promql/parser/printer.go
@@ -77,9 +77,9 @@ func (node *AggregateExpr) getAggOpStr() string {
 
 	switch {
 	case node.Without:
-		aggrString += fmt.Sprintf(" without(%s) ", strings.Join(node.Grouping, ", "))
+		aggrString += fmt.Sprintf(" without (%s) ", strings.Join(node.Grouping, ", "))
 	case len(node.Grouping) > 0:
-		aggrString += fmt.Sprintf(" by(%s) ", strings.Join(node.Grouping, ", "))
+		aggrString += fmt.Sprintf(" by (%s) ", strings.Join(node.Grouping, ", "))
 	}
 
 	return aggrString
@@ -103,14 +103,14 @@ func (node *BinaryExpr) getMatchingStr() string {
 		if vm.On {
 			vmTag = "on"
 		}
-		matching = fmt.Sprintf(" %s(%s)", vmTag, strings.Join(vm.MatchingLabels, ", "))
+		matching = fmt.Sprintf(" %s (%s)", vmTag, strings.Join(vm.MatchingLabels, ", "))
 
 		if vm.Card == CardManyToOne || vm.Card == CardOneToMany {
 			vmCard := "right"
 			if vm.Card == CardManyToOne {
 				vmCard = "left"
 			}
-			matching += fmt.Sprintf(" group_%s(%s)", vmCard, strings.Join(vm.Include, ", "))
+			matching += fmt.Sprintf(" group_%s (%s)", vmCard, strings.Join(vm.Include, ", "))
 		}
 	}
 	return matching
diff --git a/rules/alerting.go b/rules/alerting.go
index bb6f93a6d..4740d3457 100644
--- a/rules/alerting.go
+++ b/rules/alerting.go
@@ -24,6 +24,7 @@ import (
 	"github.com/go-kit/log"
 	"github.com/go-kit/log/level"
 	"github.com/prometheus/common/model"
+	"go.uber.org/atomic"
 	yaml "gopkg.in/yaml.v2"
 
 	"github.com/prometheus/prometheus/model/labels"
@@ -121,17 +122,17 @@ type AlertingRule struct {
 	externalURL string
 	// true if old state has been restored. We start persisting samples for ALERT_FOR_STATE
 	// only after the restoration.
-	restored bool
-	// Protects the below.
-	mtx sync.Mutex
+	restored *atomic.Bool
 	// Time in seconds taken to evaluate rule.
-	evaluationDuration time.Duration
+	evaluationDuration *atomic.Duration
 	// Timestamp of last evaluation of rule.
-	evaluationTimestamp time.Time
+	evaluationTimestamp *atomic.Time
 	// The health of the alerting rule.
-	health RuleHealth
+	health *atomic.String
 	// The last error seen by the alerting rule.
-	lastError error
+	lastError *atomic.Error
+	// activeMtx Protects the `active` map.
+	activeMtx sync.Mutex
 	// A map of alerts which are currently active (Pending or Firing), keyed by
 	// the fingerprint of the labelset they correspond to.
 	active map[uint64]*Alert
@@ -151,17 +152,20 @@ func NewAlertingRule(
 	}
 
 	return &AlertingRule{
-		name:           name,
-		vector:         vec,
-		holdDuration:   hold,
-		labels:         labels,
-		annotations:    annotations,
-		externalLabels: el,
-		externalURL:    externalURL,
-		health:         HealthUnknown,
-		active:         map[uint64]*Alert{},
-		logger:         logger,
-		restored:       restored,
+		name:                name,
+		vector:              vec,
+		holdDuration:        hold,
+		labels:              labels,
+		annotations:         annotations,
+		externalLabels:      el,
+		externalURL:         externalURL,
+		active:              map[uint64]*Alert{},
+		logger:              logger,
+		restored:            atomic.NewBool(restored),
+		health:              atomic.NewString(string(HealthUnknown)),
+		evaluationTimestamp: atomic.NewTime(time.Time{}),
+		evaluationDuration:  atomic.NewDuration(0),
+		lastError:           atomic.NewError(nil),
 	}
 }
 
@@ -172,30 +176,22 @@ func (r *AlertingRule) Name() string {
 
 // SetLastError sets the current error seen by the alerting rule.
 func (r *AlertingRule) SetLastError(err error) {
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
-	r.lastError = err
+	r.lastError.Store(err)
 }
 
 // LastError returns the last error seen by the alerting rule.
 func (r *AlertingRule) LastError() error {
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
-	return r.lastError
+	return r.lastError.Load()
 }
 
 // SetHealth sets the current health of the alerting rule.
 func (r *AlertingRule) SetHealth(health RuleHealth) {
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
-	r.health = health
+	r.health.Store(string(health))
 }
 
 // Health returns the current health of the alerting rule.
 func (r *AlertingRule) Health() RuleHealth {
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
-	return r.health
+	return RuleHealth(r.health.String())
 }
 
 // Query returns the query expression of the alerting rule.
@@ -283,42 +279,32 @@ func (r *AlertingRule) QueryforStateSeries(alert *Alert, q storage.Querier) (sto
 
 // SetEvaluationDuration updates evaluationDuration to the duration it took to evaluate the rule on its last evaluation.
 func (r *AlertingRule) SetEvaluationDuration(dur time.Duration) {
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
-	r.evaluationDuration = dur
+	r.evaluationDuration.Store(dur)
 }
 
 // GetEvaluationDuration returns the time in seconds it took to evaluate the alerting rule.
 func (r *AlertingRule) GetEvaluationDuration() time.Duration {
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
-	return r.evaluationDuration
+	return r.evaluationDuration.Load()
 }
 
 // SetEvaluationTimestamp updates evaluationTimestamp to the timestamp of when the rule was last evaluated.
 func (r *AlertingRule) SetEvaluationTimestamp(ts time.Time) {
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
-	r.evaluationTimestamp = ts
+	r.evaluationTimestamp.Store(ts)
 }
 
 // GetEvaluationTimestamp returns the time the evaluation took place.
 func (r *AlertingRule) GetEvaluationTimestamp() time.Time {
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
-	return r.evaluationTimestamp
+	return r.evaluationTimestamp.Load()
 }
 
 // SetRestored updates the restoration state of the alerting rule.
 func (r *AlertingRule) SetRestored(restored bool) {
-	r.restored = restored
+	r.restored.Store(restored)
 }
 
 // Restored returns the restoration state of the alerting rule.
 func (r *AlertingRule) Restored() bool {
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
-	return r.restored
+	return r.restored.Load()
 }
 
 // resolvedRetention is the duration for which a resolved alert instance
@@ -333,9 +319,6 @@ func (r *AlertingRule) Eval(ctx context.Context, ts time.Time, query QueryFunc,
 		return nil, err
 	}
 
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
-
 	// Create pending alerts for any new vector elements in the alert expression
 	// or update the expression value for existing elements.
 	resultFPs := map[uint64]struct{}{}
@@ -407,6 +390,9 @@ func (r *AlertingRule) Eval(ctx context.Context, ts time.Time, query QueryFunc,
 		}
 	}
 
+	r.activeMtx.Lock()
+	defer r.activeMtx.Unlock()
+
 	for h, a := range alerts {
 		// Check whether we already have alerting state for the identifying label set.
 		// Update the last value and annotations if so, create a new alert entry otherwise.
@@ -441,7 +427,7 @@ func (r *AlertingRule) Eval(ctx context.Context, ts time.Time, query QueryFunc,
 			a.FiredAt = ts
 		}
 
-		if r.restored {
+		if r.restored.Load() {
 			vec = append(vec, r.sample(a, ts))
 			vec = append(vec, r.forStateSample(a, ts, float64(a.ActiveAt.Unix())))
 		}
@@ -458,8 +444,8 @@ func (r *AlertingRule) Eval(ctx context.Context, ts time.Time, query QueryFunc,
 // State returns the maximum state of alert instances for this rule.
 // StateFiring > StatePending > StateInactive
 func (r *AlertingRule) State() AlertState {
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
+	r.activeMtx.Lock()
+	defer r.activeMtx.Unlock()
 
 	maxState := StateInactive
 	for _, a := range r.active {
@@ -484,8 +470,8 @@ func (r *AlertingRule) ActiveAlerts() []*Alert {
 // currentAlerts returns all instances of alerts for this rule. This may include
 // inactive alerts that were previously firing.
 func (r *AlertingRule) currentAlerts() []*Alert {
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
+	r.activeMtx.Lock()
+	defer r.activeMtx.Unlock()
 
 	alerts := make([]*Alert, 0, len(r.active))
 
@@ -501,8 +487,8 @@ func (r *AlertingRule) currentAlerts() []*Alert {
 // and not on its copy.
 // If you want to run on a copy of alerts then don't use this, get the alerts from 'ActiveAlerts()'.
 func (r *AlertingRule) ForEachActiveAlert(f func(*Alert)) {
-	r.mtx.Lock()
-	defer r.mtx.Unlock()
+	r.activeMtx.Lock()
+	defer r.activeMtx.Unlock()
 
 	for _, a := range r.active {
 		f(a)
diff --git a/rules/recording.go b/rules/recording.go
index 2ce15979f..6d16a80b0 100644
--- a/rules/recording.go
+++ b/rules/recording.go
@@ -17,9 +17,9 @@ import (
 	"context"
 	"fmt"
 	"net/url"
-	"sync"
 	"time"
 
+	"go.uber.org/atomic"
 	yaml "gopkg.in/yaml.v2"
 
 	"github.com/prometheus/prometheus/model/labels"
@@ -33,25 +33,26 @@ type RecordingRule struct {
 	name   string
 	vector parser.Expr
 	labels labels.Labels
-	// Protects the below.
-	mtx sync.Mutex
 	// The health of the recording rule.
-	health RuleHealth
+	health *atomic.String
 	// Timestamp of last evaluation of the recording rule.
-	evaluationTimestamp time.Time
+	evaluationTimestamp *atomic.Time
 	// The last error seen by the recording rule.
-	lastError error
+	lastError *atomic.Error
 	// Duration of how long it took to evaluate the recording rule.
-	evaluationDuration time.Duration
+	evaluationDuration *atomic.Duration
 }
 
 // NewRecordingRule returns a new recording rule.
 func NewRecordingRule(name string, vector parser.Expr, lset labels.Labels) *RecordingRule {
 	return &RecordingRule{
-		name:   name,
-		vector: vector,
-		health: HealthUnknown,
-		labels: lset,
+		name:                name,
+		vector:              vector,
+		labels:              lset,
+		health:              atomic.NewString(string(HealthUnknown)),
+		evaluationTimestamp: atomic.NewTime(time.Time{}),
+		evaluationDuration:  atomic.NewDuration(0),
+		lastError:           atomic.NewError(nil),
 	}
 }
 
@@ -124,56 +125,40 @@ func (rule *RecordingRule) String() string {
 
 // SetEvaluationDuration updates evaluationDuration to the time in seconds it took to evaluate the rule on its last evaluation.
 func (rule *RecordingRule) SetEvaluationDuration(dur time.Duration) {
-	rule.mtx.Lock()
-	defer rule.mtx.Unlock()
-	rule.evaluationDuration = dur
+	rule.evaluationDuration.Store(dur)
 }
 
 // SetLastError sets the current error seen by the recording rule.
 func (rule *RecordingRule) SetLastError(err error) {
-	rule.mtx.Lock()
-	defer rule.mtx.Unlock()
-	rule.lastError = err
+	rule.lastError.Store(err)
 }
 
 // LastError returns the last error seen by the recording rule.
 func (rule *RecordingRule) LastError() error {
-	rule.mtx.Lock()
-	defer rule.mtx.Unlock()
-	return rule.lastError
+	return rule.lastError.Load()
 }
 
 // SetHealth sets the current health of the recording rule.
 func (rule *RecordingRule) SetHealth(health RuleHealth) {
-	rule.mtx.Lock()
-	defer rule.mtx.Unlock()
-	rule.health = health
+	rule.health.Store(string(health))
 }
 
 // Health returns the current health of the recording rule.
 func (rule *RecordingRule) Health() RuleHealth {
-	rule.mtx.Lock()
-	defer rule.mtx.Unlock()
-	return rule.health
+	return RuleHealth(rule.health.Load())
 }
 
 // GetEvaluationDuration returns the time in seconds it took to evaluate the recording rule.
 func (rule *RecordingRule) GetEvaluationDuration() time.Duration {
-	rule.mtx.Lock()
-	defer rule.mtx.Unlock()
-	return rule.evaluationDuration
+	return rule.evaluationDuration.Load()
 }
 
 // SetEvaluationTimestamp updates evaluationTimestamp to the timestamp of when the rule was last evaluated.
 func (rule *RecordingRule) SetEvaluationTimestamp(ts time.Time) {
-	rule.mtx.Lock()
-	defer rule.mtx.Unlock()
-	rule.evaluationTimestamp = ts
+	rule.evaluationTimestamp.Store(ts)
 }
 
 // GetEvaluationTimestamp returns the time the evaluation took place.
 func (rule *RecordingRule) GetEvaluationTimestamp() time.Time {
-	rule.mtx.Lock()
-	defer rule.mtx.Unlock()
-	return rule.evaluationTimestamp
+	return rule.evaluationTimestamp.Load()
 }
diff --git a/scrape/manager.go b/scrape/manager.go
index 92ecbc31d..81ee4ff63 100644
--- a/scrape/manager.go
+++ b/scrape/manager.go
@@ -124,7 +124,8 @@ func NewManager(o *Options, logger log.Logger, app storage.Appendable) *Manager
 
 // Options are the configuration parameters to the scrape manager.
 type Options struct {
-	ExtraMetrics bool
+	ExtraMetrics  bool
+	NoDefaultPort bool
 	// Option used by downstream scraper users like OpenTelemetry Collector
 	// to help lookup metric metadata. Should be false for Prometheus.
 	PassMetadataInContext bool
@@ -207,7 +208,7 @@ func (m *Manager) reload() {
 				level.Error(m.logger).Log("msg", "error reloading target set", "err", "invalid config id:"+setName)
 				continue
 			}
-			sp, err := newScrapePool(scrapeConfig, m.append, m.jitterSeed, log.With(m.logger, "scrape_pool", setName), m.opts.ExtraMetrics, m.opts.PassMetadataInContext, m.opts.HTTPClientOptions)
+			sp, err := newScrapePool(scrapeConfig, m.append, m.jitterSeed, log.With(m.logger, "scrape_pool", setName), m.opts)
 			if err != nil {
 				level.Error(m.logger).Log("msg", "error creating new scrape pool", "err", err, "scrape_pool", setName)
 				continue
diff --git a/scrape/scrape.go b/scrape/scrape.go
index 9304baecd..e044e72e1 100644
--- a/scrape/scrape.go
+++ b/scrape/scrape.go
@@ -239,6 +239,8 @@ type scrapePool struct {
 
 	// Constructor for new scrape loops. This is settable for testing convenience.
 	newLoop func(scrapeLoopOptions) loop
+
+	noDefaultPort bool
 }
 
 type labelLimits struct {
@@ -264,13 +266,13 @@ const maxAheadTime = 10 * time.Minute
 
 type labelsMutator func(labels.Labels) labels.Labels
 
-func newScrapePool(cfg *config.ScrapeConfig, app storage.Appendable, jitterSeed uint64, logger log.Logger, reportExtraMetrics, passMetadataInContext bool, httpOpts []config_util.HTTPClientOption) (*scrapePool, error) {
+func newScrapePool(cfg *config.ScrapeConfig, app storage.Appendable, jitterSeed uint64, logger log.Logger, options *Options) (*scrapePool, error) {
 	targetScrapePools.Inc()
 	if logger == nil {
 		logger = log.NewNopLogger()
 	}
 
-	client, err := config_util.NewClientFromConfig(cfg.HTTPClientConfig, cfg.JobName, httpOpts...)
+	client, err := config_util.NewClientFromConfig(cfg.HTTPClientConfig, cfg.JobName, options.HTTPClientOptions...)
 	if err != nil {
 		targetScrapePoolsFailed.Inc()
 		return nil, errors.Wrap(err, "error creating HTTP client")
@@ -287,7 +289,8 @@ func newScrapePool(cfg *config.ScrapeConfig, app storage.Appendable, jitterSeed
 		activeTargets: map[uint64]*Target{},
 		loops:         map[uint64]loop{},
 		logger:        logger,
-		httpOpts:      httpOpts,
+		httpOpts:      options.HTTPClientOptions,
+		noDefaultPort: options.NoDefaultPort,
 	}
 	sp.newLoop = func(opts scrapeLoopOptions) loop {
 		// Update the targets retrieval function for metadata to a new scrape cache.
@@ -314,10 +317,10 @@ func newScrapePool(cfg *config.ScrapeConfig, app storage.Appendable, jitterSeed
 			opts.labelLimits,
 			opts.interval,
 			opts.timeout,
-			reportExtraMetrics,
+			options.ExtraMetrics,
 			opts.target,
 			cache,
-			passMetadataInContext,
+			options.PassMetadataInContext,
 		)
 	}
 
@@ -480,7 +483,7 @@ func (sp *scrapePool) Sync(tgs []*targetgroup.Group) {
 	var all []*Target
 	sp.droppedTargets = []*Target{}
 	for _, tg := range tgs {
-		targets, failures := TargetsFromGroup(tg, sp.config)
+		targets, failures := TargetsFromGroup(tg, sp.config, sp.noDefaultPort)
 		for _, err := range failures {
 			level.Error(sp.logger).Log("msg", "Creating target failed", "err", err)
 		}
diff --git a/scrape/target.go b/scrape/target.go
index e017a4584..55d6d29d7 100644
--- a/scrape/target.go
+++ b/scrape/target.go
@@ -351,7 +351,7 @@ func (app *timeLimitAppender) Append(ref storage.SeriesRef, lset labels.Labels,
 // PopulateLabels builds a label set from the given label set and scrape configuration.
 // It returns a label set before relabeling was applied as the second return value.
 // Returns the original discovered label set found before relabelling was applied if the target is dropped during relabeling.
-func PopulateLabels(lset labels.Labels, cfg *config.ScrapeConfig) (res, orig labels.Labels, err error) {
+func PopulateLabels(lset labels.Labels, cfg *config.ScrapeConfig, noDefaultPort bool) (res, orig labels.Labels, err error) {
 	// Copy labels into the labelset for the target if they are not set already.
 	scrapeLabels := []labels.Label{
 		{Name: model.JobLabel, Value: cfg.JobName},
@@ -389,21 +389,25 @@ func PopulateLabels(lset labels.Labels, cfg *config.ScrapeConfig) (res, orig lab
 
 	// addPort checks whether we should add a default port to the address.
 	// If the address is not valid, we don't append a port either.
-	addPort := func(s string) bool {
+	addPort := func(s string) (string, string, bool) {
 		// If we can split, a port exists and we don't have to add one.
-		if _, _, err := net.SplitHostPort(s); err == nil {
-			return false
+		if host, port, err := net.SplitHostPort(s); err == nil {
+			return host, port, false
 		}
 		// If adding a port makes it valid, the previous error
 		// was not due to an invalid address and we can append a port.
 		_, _, err := net.SplitHostPort(s + ":1234")
-		return err == nil
+		return "", "", err == nil
 	}
+
 	addr := lset.Get(model.AddressLabel)
-	// If it's an address with no trailing port, infer it based on the used scheme.
-	if addPort(addr) {
+	scheme := lset.Get(model.SchemeLabel)
+	host, port, add := addPort(addr)
+	// If it's an address with no trailing port, infer it based on the used scheme
+	// unless the no-default-scrape-port feature flag is present.
+	if !noDefaultPort && add {
 		// Addresses reaching this point are already wrapped in [] if necessary.
-		switch lset.Get(model.SchemeLabel) {
+		switch scheme {
 		case "http", "":
 			addr = addr + ":80"
 		case "https":
@@ -414,6 +418,21 @@ func PopulateLabels(lset labels.Labels, cfg *config.ScrapeConfig) (res, orig lab
 		lb.Set(model.AddressLabel, addr)
 	}
 
+	if noDefaultPort {
+		// If it's an address with a trailing default port and the
+		// no-default-scrape-port flag is present, remove the port.
+		switch port {
+		case "80":
+			if scheme == "http" {
+				lb.Set(model.AddressLabel, host)
+			}
+		case "443":
+			if scheme == "https" {
+				lb.Set(model.AddressLabel, host)
+			}
+		}
+	}
+
 	if err := config.CheckTargetAddress(model.LabelValue(addr)); err != nil {
 		return nil, nil, err
 	}
@@ -464,7 +483,7 @@ func PopulateLabels(lset labels.Labels, cfg *config.ScrapeConfig) (res, orig lab
 }
 
 // TargetsFromGroup builds targets based on the given TargetGroup and config.
-func TargetsFromGroup(tg *targetgroup.Group, cfg *config.ScrapeConfig) ([]*Target, []error) {
+func TargetsFromGroup(tg *targetgroup.Group, cfg *config.ScrapeConfig, noDefaultPort bool) ([]*Target, []error) {
 	targets := make([]*Target, 0, len(tg.Targets))
 	failures := []error{}
 
@@ -482,7 +501,7 @@ func TargetsFromGroup(tg *targetgroup.Group, cfg *config.ScrapeConfig) ([]*Targe
 
 		lset := labels.New(lbls...)
 
-		lbls, origLabels, err := PopulateLabels(lset, cfg)
+		lbls, origLabels, err := PopulateLabels(lset, cfg, noDefaultPort)
 		if err != nil {
 			failures = append(failures, errors.Wrapf(err, "instance %d in group %s", i, tg))
 		}
diff --git a/storage/fanout.go b/storage/fanout.go
index 2d0d33215..a51526f06 100644
--- a/storage/fanout.go
+++ b/storage/fanout.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/prometheus/prometheus/model/exemplar"
 	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/model/metadata"
 	tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
 )
 
@@ -172,6 +173,20 @@ func (f *fanoutAppender) AppendExemplar(ref SeriesRef, l labels.Labels, e exempl
 	return ref, nil
 }
 
+func (f *fanoutAppender) UpdateMetadata(ref SeriesRef, l labels.Labels, m metadata.Metadata) (SeriesRef, error) {
+	ref, err := f.primary.UpdateMetadata(ref, l, m)
+	if err != nil {
+		return ref, err
+	}
+
+	for _, appender := range f.secondaries {
+		if _, err := appender.UpdateMetadata(ref, l, m); err != nil {
+			return 0, err
+		}
+	}
+	return ref, nil
+}
+
 func (f *fanoutAppender) Commit() (err error) {
 	err = f.primary.Commit()
 
diff --git a/storage/interface.go b/storage/interface.go
index 6ce147b08..f5af49eb7 100644
--- a/storage/interface.go
+++ b/storage/interface.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/prometheus/prometheus/model/exemplar"
 	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/model/metadata"
 	"github.com/prometheus/prometheus/tsdb/chunkenc"
 	"github.com/prometheus/prometheus/tsdb/chunks"
 )
@@ -222,6 +223,7 @@ type Appender interface {
 	// Appender has to be discarded after rollback.
 	Rollback() error
 	ExemplarAppender
+	MetadataUpdater
 }
 
 // GetRef is an extra interface on Appenders used by downstream projects
@@ -250,6 +252,18 @@ type ExemplarAppender interface {
 	AppendExemplar(ref SeriesRef, l labels.Labels, e exemplar.Exemplar) (SeriesRef, error)
 }
 
+// MetadataUpdater provides an interface for associating metadata to stored series.
+type MetadataUpdater interface {
+	// UpdateMetadata updates a metadata entry for the given series and labels.
+	// A series reference number is returned which can be used to modify the
+	// metadata of the given series in the same or later transactions.
+	// Returned reference numbers are ephemeral and may be rejected in calls
+	// to UpdateMetadata() at any point. If the series does not exist,
+	// UpdateMetadata returns an error.
+	// If the reference is 0 it must not be used for caching.
+	UpdateMetadata(ref SeriesRef, l labels.Labels, m metadata.Metadata) (SeriesRef, error)
+}
+
 // SeriesSet contains a set of series.
 type SeriesSet interface {
 	Next() bool
diff --git a/storage/remote/write.go b/storage/remote/write.go
index a7399ee53..be401e95d 100644
--- a/storage/remote/write.go
+++ b/storage/remote/write.go
@@ -27,6 +27,7 @@ import (
 	"github.com/prometheus/prometheus/config"
 	"github.com/prometheus/prometheus/model/exemplar"
 	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/model/metadata"
 	"github.com/prometheus/prometheus/storage"
 	"github.com/prometheus/prometheus/tsdb/wal"
 )
@@ -268,6 +269,12 @@ func (t *timestampTracker) AppendExemplar(_ storage.SeriesRef, _ labels.Labels,
 	return 0, nil
 }
 
+func (t *timestampTracker) UpdateMetadata(_ storage.SeriesRef, _ labels.Labels, _ metadata.Metadata) (storage.SeriesRef, error) {
+	// TODO: Add and increment a `metadata` field when we get around to wiring metadata in remote_write.
+	// UpadteMetadata is no-op for remote write (where timestampTracker is being used) for now.
+	return 0, nil
+}
+
 // Commit implements storage.Appender.
 func (t *timestampTracker) Commit() error {
 	t.writeStorage.samplesIn.incr(t.samples + t.exemplars)
diff --git a/template/template.go b/template/template.go
index 0b1659ce0..d5f80006f 100644
--- a/template/template.go
+++ b/template/template.go
@@ -45,6 +45,8 @@ var (
 		Name: "prometheus_template_text_expansions_total",
 		Help: "The total number of template text expansions.",
 	})
+
+	errNaNOrInf = errors.New("value is NaN or Inf")
 )
 
 func init() {
@@ -315,15 +317,24 @@ func NewTemplateExpander(
 				if err != nil {
 					return "", err
 				}
-				if math.IsNaN(v) || math.IsInf(v, 0) {
+
+				tm, err := floatToTime(v)
+				switch {
+				case errors.Is(err, errNaNOrInf):
 					return fmt.Sprintf("%.4g", v), nil
+				case err != nil:
+					return "", err
 				}
-				timestamp := v * 1e9
-				if timestamp > math.MaxInt64 || timestamp < math.MinInt64 {
-					return "", fmt.Errorf("%v cannot be represented as a nanoseconds timestamp since it overflows int64", v)
+
+				return fmt.Sprint(tm), nil
+			},
+			"toTime": func(i interface{}) (*time.Time, error) {
+				v, err := convertToFloat(i)
+				if err != nil {
+					return nil, err
 				}
-				t := model.TimeFromUnixNano(int64(timestamp)).Time().UTC()
-				return fmt.Sprint(t), nil
+
+				return floatToTime(v)
 			},
 			"pathPrefix": func() string {
 				return externalURL.Path
@@ -446,3 +457,15 @@ func (te Expander) ParseTest() error {
 	}
 	return nil
 }
+
+func floatToTime(v float64) (*time.Time, error) {
+	if math.IsNaN(v) || math.IsInf(v, 0) {
+		return nil, errNaNOrInf
+	}
+	timestamp := v * 1e9
+	if timestamp > math.MaxInt64 || timestamp < math.MinInt64 {
+		return nil, fmt.Errorf("%v cannot be represented as a nanoseconds timestamp since it overflows int64", v)
+	}
+	t := model.TimeFromUnixNano(int64(timestamp)).Time().UTC()
+	return &t, nil
+}
diff --git a/tsdb/agent/db.go b/tsdb/agent/db.go
index 0f9446785..3feb55623 100644
--- a/tsdb/agent/db.go
+++ b/tsdb/agent/db.go
@@ -31,6 +31,7 @@ import (
 
 	"github.com/prometheus/prometheus/model/exemplar"
 	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/model/metadata"
 	"github.com/prometheus/prometheus/model/timestamp"
 	"github.com/prometheus/prometheus/storage"
 	"github.com/prometheus/prometheus/storage/remote"
@@ -812,6 +813,11 @@ func (a *appender) AppendExemplar(ref storage.SeriesRef, l labels.Labels, e exem
 	return storage.SeriesRef(s.ref), nil
 }
 
+func (a *appender) UpdateMetadata(ref storage.SeriesRef, l labels.Labels, m metadata.Metadata) (storage.SeriesRef, error) {
+	// TODO: Wire metadata in the Agent's appender.
+	return 0, nil
+}
+
 // Commit submits the collected samples and purges the batch.
 func (a *appender) Commit() error {
 	a.mtx.RLock()
diff --git a/tsdb/docs/format/index.md b/tsdb/docs/format/index.md
index 854316cb4..6279d34f6 100644
--- a/tsdb/docs/format/index.md
+++ b/tsdb/docs/format/index.md
@@ -126,7 +126,7 @@ After the labels, the number of indexed chunks is encoded, followed by a sequenc
 ### Label Index
 
 A label index section indexes the existing (combined) values for one or more label names.
-The `#names` field determines the number of indexed label names, followed by the total number of entries in the `#entries` field. The body holds #entries / #names tuples of symbol table references, each tuple being of #names length. The value tuples are sorted in lexicographically increasing order. This is no longer used.
+The `#names` field determines the number of indexed label names, followed by the total number of entries in the `#entries` field. The body holds `#entries / #names` tuples of symbol table references, each tuple being of #names length. The value tuples are sorted in lexicographically increasing order. This is no longer used.
 
 ```
 ┌───────────────┬────────────────┬────────────────┐
diff --git a/tsdb/docs/format/wal.md b/tsdb/docs/format/wal.md
index 7714b4fa2..39be2a808 100644
--- a/tsdb/docs/format/wal.md
+++ b/tsdb/docs/format/wal.md
@@ -118,3 +118,33 @@ See: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/Op
 │                              . . .                               │
 └──────────────────────────────────────────────────────────────────┘
 ```
+
+### Metadata records
+
+Metadata records encode the metadata updates associated with a series.
+
+```
+┌────────────────────────────────────────────┐
+│ type = 6 <1b>                              │
+├────────────────────────────────────────────┤
+│ ┌────────────────────────────────────────┐ │
+│ │ series_id <uvarint>                    │ │
+│ ├────────────────────────────────────────┤ │
+│ │ metric_type <1b>                       │ │
+│ ├────────────────────────────────────────┤ │
+│ │ num_fields <uvarint>                   │ │
+│ ├───────────────────────┬────────────────┤ │
+│ │ len(name_1) <uvarint> │ name_1 <bytes> │ │
+│ ├───────────────────────┼────────────────┤ │
+│ │ len(val_1) <uvarint>  │ val_1 <bytes>  │ │
+│ ├───────────────────────┴────────────────┤ │
+│ │                . . .                   │ │
+│ ├───────────────────────┬────────────────┤ │
+│ │ len(name_n) <uvarint> │ name_n <bytes> │ │
+│ ├───────────────────────┼────────────────┤ │
+│ │ len(val_n) <uvarint>  │ val_n <bytes>  │ │
+│ └───────────────────────┴────────────────┘ │
+│                  . . .                     │
+└────────────────────────────────────────────┘
+```
+
diff --git a/tsdb/head.go b/tsdb/head.go
index 669b7eaaa..ae2a2384a 100644
--- a/tsdb/head.go
+++ b/tsdb/head.go
@@ -31,6 +31,7 @@ import (
 	"github.com/prometheus/prometheus/config"
 	"github.com/prometheus/prometheus/model/exemplar"
 	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/model/metadata"
 	"github.com/prometheus/prometheus/storage"
 	"github.com/prometheus/prometheus/tsdb/chunkenc"
 	"github.com/prometheus/prometheus/tsdb/chunks"
@@ -75,6 +76,7 @@ type Head struct {
 	logger          log.Logger
 	appendPool      sync.Pool
 	exemplarsPool   sync.Pool
+	metadataPool    sync.Pool
 	seriesPool      sync.Pool
 	bytesPool       sync.Pool
 	memChunkPool    sync.Pool
@@ -1514,6 +1516,7 @@ type memSeries struct {
 
 	ref  chunks.HeadSeriesRef
 	lset labels.Labels
+	meta metadata.Metadata
 
 	// Immutable chunks on disk that have not yet gone into a block, in order of ascending time stamps.
 	// When compaction runs, chunks get moved into a block and all pointers are shifted like so:
diff --git a/tsdb/head_append.go b/tsdb/head_append.go
index 13d56de7b..6b0428dd6 100644
--- a/tsdb/head_append.go
+++ b/tsdb/head_append.go
@@ -23,6 +23,7 @@ import (
 
 	"github.com/prometheus/prometheus/model/exemplar"
 	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/model/metadata"
 	"github.com/prometheus/prometheus/storage"
 	"github.com/prometheus/prometheus/tsdb/chunkenc"
 	"github.com/prometheus/prometheus/tsdb/chunks"
@@ -65,6 +66,15 @@ func (a *initAppender) AppendExemplar(ref storage.SeriesRef, l labels.Labels, e
 	return a.app.AppendExemplar(ref, l, e)
 }
 
+func (a *initAppender) UpdateMetadata(ref storage.SeriesRef, l labels.Labels, m metadata.Metadata) (storage.SeriesRef, error) {
+	if a.app != nil {
+		return a.app.UpdateMetadata(ref, l, m)
+	}
+
+	a.app = a.head.appender()
+	return a.app.UpdateMetadata(ref, l, m)
+}
+
 // initTime initializes a head with the first timestamp. This only needs to be called
 // for a completely fresh head with an empty WAL.
 func (h *Head) initTime(t int64) {
@@ -130,6 +140,7 @@ func (h *Head) appender() *headAppender {
 		samples:               h.getAppendBuffer(),
 		sampleSeries:          h.getSeriesBuffer(),
 		exemplars:             exemplarsBuf,
+		metadata:              h.getMetadataBuffer(),
 		appendID:              appendID,
 		cleanupAppendIDsBelow: cleanupAppendIDsBelow,
 	}
@@ -196,6 +207,19 @@ func (h *Head) putExemplarBuffer(b []exemplarWithSeriesRef) {
 	h.exemplarsPool.Put(b[:0])
 }
 
+func (h *Head) getMetadataBuffer() []record.RefMetadata {
+	b := h.metadataPool.Get()
+	if b == nil {
+		return make([]record.RefMetadata, 0, 512)
+	}
+	return b.([]record.RefMetadata)
+}
+
+func (h *Head) putMetadataBuffer(b []record.RefMetadata) {
+	//nolint:staticcheck // Ignore SA6002 safe to ignore and actually fixing it has some performance penalty.
+	h.metadataPool.Put(b[:0])
+}
+
 func (h *Head) getSeriesBuffer() []*memSeries {
 	b := h.seriesPool.Get()
 	if b == nil {
@@ -232,10 +256,12 @@ type headAppender struct {
 	minValidTime int64 // No samples below this timestamp are allowed.
 	mint, maxt   int64
 
-	series       []record.RefSeries      // New series held by this appender.
-	samples      []record.RefSample      // New samples held by this appender.
-	exemplars    []exemplarWithSeriesRef // New exemplars held by this appender.
-	sampleSeries []*memSeries            // Series corresponding to the samples held by this appender (using corresponding slice indices - same series may appear more than once).
+	series         []record.RefSeries      // New series held by this appender.
+	metadata       []record.RefMetadata    // New metadata held by this appender.
+	samples        []record.RefSample      // New samples held by this appender.
+	exemplars      []exemplarWithSeriesRef // New exemplars held by this appender.
+	sampleSeries   []*memSeries            // Series corresponding to the samples held by this appender (using corresponding slice indices - same series may appear more than once).
+	metadataSeries []*memSeries            // Series corresponding to the metadata held by this appender.
 
 	appendID, cleanupAppendIDsBelow uint64
 	closed                          bool
@@ -358,6 +384,37 @@ func (a *headAppender) AppendExemplar(ref storage.SeriesRef, lset labels.Labels,
 	return storage.SeriesRef(s.ref), nil
 }
 
+// UpdateMetadata for headAppender assumes the series ref already exists, and so it doesn't
+// use getOrCreate or make any of the lset sanity checks that Append does.
+func (a *headAppender) UpdateMetadata(ref storage.SeriesRef, lset labels.Labels, meta metadata.Metadata) (storage.SeriesRef, error) {
+	s := a.head.series.getByID(chunks.HeadSeriesRef(ref))
+	if s == nil {
+		s = a.head.series.getByHash(lset.Hash(), lset)
+		if s != nil {
+			ref = storage.SeriesRef(s.ref)
+		}
+	}
+	if s == nil {
+		return 0, fmt.Errorf("unknown series when trying to add metadata with HeadSeriesRef: %d and labels: %s", ref, lset)
+	}
+
+	s.RLock()
+	hasNewMetadata := s.meta != meta
+	s.RUnlock()
+
+	if hasNewMetadata {
+		a.metadata = append(a.metadata, record.RefMetadata{
+			Ref:  s.ref,
+			Type: record.GetMetricType(meta.Type),
+			Unit: meta.Unit,
+			Help: meta.Help,
+		})
+		a.metadataSeries = append(a.metadataSeries, s)
+	}
+
+	return ref, nil
+}
+
 var _ storage.GetRef = &headAppender{}
 
 func (a *headAppender) GetRef(lset labels.Labels) (storage.SeriesRef, labels.Labels) {
@@ -389,6 +446,14 @@ func (a *headAppender) log() error {
 			return errors.Wrap(err, "log series")
 		}
 	}
+	if len(a.metadata) > 0 {
+		rec = enc.Metadata(a.metadata, buf)
+		buf = rec[:0]
+
+		if err := a.head.wal.Log(rec); err != nil {
+			return errors.Wrap(err, "log metadata")
+		}
+	}
 	if len(a.samples) > 0 {
 		rec = enc.Samples(a.samples, buf)
 		buf = rec[:0]
@@ -449,6 +514,7 @@ func (a *headAppender) Commit() (err error) {
 	defer a.head.putAppendBuffer(a.samples)
 	defer a.head.putSeriesBuffer(a.sampleSeries)
 	defer a.head.putExemplarBuffer(a.exemplars)
+	defer a.head.putMetadataBuffer(a.metadata)
 	defer a.head.iso.closeAppend(a.appendID)
 
 	total := len(a.samples)
@@ -471,6 +537,13 @@ func (a *headAppender) Commit() (err error) {
 		}
 	}
 
+	for i, m := range a.metadata {
+		series = a.metadataSeries[i]
+		series.Lock()
+		series.meta = metadata.Metadata{Type: record.ToTextparseMetricType(m.Type), Unit: m.Unit, Help: m.Help}
+		series.Unlock()
+	}
+
 	a.head.metrics.samplesAppended.Add(float64(total))
 	a.head.updateMinMaxTime(a.mint, a.maxt)
 
@@ -619,8 +692,10 @@ func (a *headAppender) Rollback() (err error) {
 	}
 	a.head.putAppendBuffer(a.samples)
 	a.head.putExemplarBuffer(a.exemplars)
+	a.head.putMetadataBuffer(a.metadata)
 	a.samples = nil
 	a.exemplars = nil
+	a.metadata = nil
 
 	// Series are created in the head memory regardless of rollback. Thus we have
 	// to log them to the WAL in any case.
diff --git a/tsdb/head_wal.go b/tsdb/head_wal.go
index fecbb757f..f6990d8bf 100644
--- a/tsdb/head_wal.go
+++ b/tsdb/head_wal.go
@@ -30,6 +30,7 @@ import (
 
 	"github.com/prometheus/prometheus/model/exemplar"
 	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/model/metadata"
 	"github.com/prometheus/prometheus/storage"
 	"github.com/prometheus/prometheus/tsdb/chunkenc"
 	"github.com/prometheus/prometheus/tsdb/chunks"
@@ -46,6 +47,7 @@ func (h *Head) loadWAL(r *wal.Reader, multiRef map[chunks.HeadSeriesRef]chunks.H
 	// for error reporting.
 	var unknownRefs atomic.Uint64
 	var unknownExemplarRefs atomic.Uint64
+	var unknownMetadataRefs atomic.Uint64
 	// Track number of series records that had overlapping m-map chunks.
 	var mmapOverlappingChunks uint64
 
@@ -81,6 +83,11 @@ func (h *Head) loadWAL(r *wal.Reader, multiRef map[chunks.HeadSeriesRef]chunks.H
 				return []record.RefExemplar{}
 			},
 		}
+		metadataPool = sync.Pool{
+			New: func() interface{} {
+				return []record.RefMetadata{}
+			},
+		}
 	)
 
 	defer func() {
@@ -184,6 +191,18 @@ func (h *Head) loadWAL(r *wal.Reader, multiRef map[chunks.HeadSeriesRef]chunks.H
 					return
 				}
 				decoded <- exemplars
+			case record.Metadata:
+				meta := metadataPool.Get().([]record.RefMetadata)[:0]
+				meta, err := dec.Metadata(rec, meta)
+				if err != nil {
+					decodeErr = &wal.CorruptionErr{
+						Err:     errors.Wrap(err, "decode metadata"),
+						Segment: r.Segment(),
+						Offset:  r.Offset(),
+					}
+					return
+				}
+				decoded <- meta
 			default:
 				// Noop.
 			}
@@ -307,6 +326,21 @@ Outer:
 			}
 			//nolint:staticcheck // Ignore SA6002 relax staticcheck verification.
 			exemplarsPool.Put(v)
+		case []record.RefMetadata:
+			for _, m := range v {
+				s := h.series.getByID(chunks.HeadSeriesRef(m.Ref))
+				if s == nil {
+					unknownMetadataRefs.Inc()
+					continue
+				}
+				s.meta = metadata.Metadata{
+					Type: record.ToTextparseMetricType(m.Type),
+					Unit: m.Unit,
+					Help: m.Help,
+				}
+			}
+			//nolint:staticcheck // Ignore SA6002 relax staticcheck verification.
+			metadataPool.Put(v)
 		default:
 			panic(fmt.Errorf("unexpected decoded type: %T", d))
 		}
@@ -333,8 +367,8 @@ Outer:
 		return errors.Wrap(r.Err(), "read records")
 	}
 
-	if unknownRefs.Load() > 0 || unknownExemplarRefs.Load() > 0 {
-		level.Warn(h.logger).Log("msg", "Unknown series references", "samples", unknownRefs.Load(), "exemplars", unknownExemplarRefs.Load())
+	if unknownRefs.Load() > 0 || unknownExemplarRefs.Load() > 0 || unknownMetadataRefs.Load() > 0 {
+		level.Warn(h.logger).Log("msg", "Unknown series references", "samples", unknownRefs.Load(), "exemplars", unknownExemplarRefs.Load(), "metadata", unknownMetadataRefs.Load())
 	}
 	if mmapOverlappingChunks > 0 {
 		level.Info(h.logger).Log("msg", "Overlapping m-map chunks on duplicate series records", "count", mmapOverlappingChunks)
@@ -464,11 +498,7 @@ func (s *memSeries) encodeToSnapshotRecord(b []byte) []byte {
 
 	buf.PutByte(chunkSnapshotRecordTypeSeries)
 	buf.PutBE64(uint64(s.ref))
-	buf.PutUvarint(len(s.lset))
-	for _, l := range s.lset {
-		buf.PutUvarintStr(l.Name)
-		buf.PutUvarintStr(l.Value)
-	}
+	record.EncodeLabels(&buf, s.lset)
 	buf.PutBE64int64(s.chunkRange)
 
 	s.Lock()
@@ -491,7 +521,7 @@ func (s *memSeries) encodeToSnapshotRecord(b []byte) []byte {
 	return buf.Get()
 }
 
-func decodeSeriesFromChunkSnapshot(b []byte) (csr chunkSnapshotRecord, err error) {
+func decodeSeriesFromChunkSnapshot(d *record.Decoder, b []byte) (csr chunkSnapshotRecord, err error) {
 	dec := encoding.Decbuf{B: b}
 
 	if flag := dec.Byte(); flag != chunkSnapshotRecordTypeSeries {
@@ -499,13 +529,9 @@ func decodeSeriesFromChunkSnapshot(b []byte) (csr chunkSnapshotRecord, err error
 	}
 
 	csr.ref = chunks.HeadSeriesRef(dec.Be64())
-
 	// The label set written to the disk is already sorted.
-	csr.lset = make(labels.Labels, dec.Uvarint())
-	for i := range csr.lset {
-		csr.lset[i].Name = dec.UvarintStr()
-		csr.lset[i].Value = dec.UvarintStr()
-	}
+	// TODO: figure out why DecodeLabels calls Sort(), and perhaps remove it.
+	csr.lset = d.DecodeLabels(&dec)
 
 	csr.chunkRange = dec.Be64int64()
 	if dec.Uvarint() == 0 {
@@ -937,7 +963,7 @@ Outer:
 		switch rec[0] {
 		case chunkSnapshotRecordTypeSeries:
 			numSeries++
-			csr, err := decodeSeriesFromChunkSnapshot(rec)
+			csr, err := decodeSeriesFromChunkSnapshot(&dec, rec)
 			if err != nil {
 				loopErr = errors.Wrap(err, "decode series record")
 				break Outer
diff --git a/tsdb/record/record.go b/tsdb/record/record.go
index 12a404739..ee7169a45 100644
--- a/tsdb/record/record.go
+++ b/tsdb/record/record.go
@@ -22,6 +22,7 @@ import (
 	"github.com/pkg/errors"
 
 	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/model/textparse"
 	"github.com/prometheus/prometheus/storage"
 	"github.com/prometheus/prometheus/tsdb/chunks"
 	"github.com/prometheus/prometheus/tsdb/encoding"
@@ -42,6 +43,86 @@ const (
 	Tombstones Type = 3
 	// Exemplars is used to match WAL records of type Exemplars.
 	Exemplars Type = 4
+	// Metadata is used to match WAL records of type Metadata.
+	Metadata Type = 6
+)
+
+func (rt Type) String() string {
+	switch rt {
+	case Series:
+		return "series"
+	case Samples:
+		return "samples"
+	case Exemplars:
+		return "exemplars"
+	case Tombstones:
+		return "tombstones"
+	case Metadata:
+		return "metadata"
+	default:
+		return "unknown"
+	}
+}
+
+// MetricType represents the type of a series.
+type MetricType uint8
+
+const (
+	UnknownMT      MetricType = 0
+	Counter        MetricType = 1
+	Gauge          MetricType = 2
+	Histogram      MetricType = 3
+	GaugeHistogram MetricType = 4
+	Summary        MetricType = 5
+	Info           MetricType = 6
+	Stateset       MetricType = 7
+)
+
+func GetMetricType(t textparse.MetricType) uint8 {
+	switch t {
+	case textparse.MetricTypeCounter:
+		return uint8(Counter)
+	case textparse.MetricTypeGauge:
+		return uint8(Gauge)
+	case textparse.MetricTypeHistogram:
+		return uint8(Histogram)
+	case textparse.MetricTypeGaugeHistogram:
+		return uint8(GaugeHistogram)
+	case textparse.MetricTypeSummary:
+		return uint8(Summary)
+	case textparse.MetricTypeInfo:
+		return uint8(Info)
+	case textparse.MetricTypeStateset:
+		return uint8(Stateset)
+	default:
+		return uint8(UnknownMT)
+	}
+}
+
+func ToTextparseMetricType(m uint8) textparse.MetricType {
+	switch m {
+	case uint8(Counter):
+		return textparse.MetricTypeCounter
+	case uint8(Gauge):
+		return textparse.MetricTypeGauge
+	case uint8(Histogram):
+		return textparse.MetricTypeHistogram
+	case uint8(GaugeHistogram):
+		return textparse.MetricTypeGaugeHistogram
+	case uint8(Summary):
+		return textparse.MetricTypeSummary
+	case uint8(Info):
+		return textparse.MetricTypeInfo
+	case uint8(Stateset):
+		return textparse.MetricTypeStateset
+	default:
+		return textparse.MetricTypeUnknown
+	}
+}
+
+const (
+	unitMetaName = "UNIT"
+	helpMetaName = "HELP"
 )
 
 // ErrNotFound is returned if a looked up resource was not found. Duplicate ErrNotFound from head.go.
@@ -60,6 +141,14 @@ type RefSample struct {
 	V   float64
 }
 
+// RefMetadata is the metadata associated with a series ID.
+type RefMetadata struct {
+	Ref  chunks.HeadSeriesRef
+	Type uint8
+	Unit string
+	Help string
+}
+
 // RefExemplar is an exemplar with it's labels, timestamp, value the exemplar was collected/observed with, and a reference to a series.
 type RefExemplar struct {
 	Ref    chunks.HeadSeriesRef
@@ -68,7 +157,7 @@ type RefExemplar struct {
 	Labels labels.Labels
 }
 
-// Decoder decodes series, sample, and tombstone records.
+// Decoder decodes series, sample, metadata and tombstone records.
 // The zero value is ready to use.
 type Decoder struct{}
 
@@ -79,7 +168,7 @@ func (d *Decoder) Type(rec []byte) Type {
 		return Unknown
 	}
 	switch t := Type(rec[0]); t {
-	case Series, Samples, Tombstones, Exemplars:
+	case Series, Samples, Tombstones, Exemplars, Metadata:
 		return t
 	}
 	return Unknown
@@ -94,14 +183,7 @@ func (d *Decoder) Series(rec []byte, series []RefSeries) ([]RefSeries, error) {
 	}
 	for len(dec.B) > 0 && dec.Err() == nil {
 		ref := storage.SeriesRef(dec.Be64())
-
-		lset := make(labels.Labels, dec.Uvarint())
-
-		for i := range lset {
-			lset[i].Name = dec.UvarintStr()
-			lset[i].Value = dec.UvarintStr()
-		}
-		sort.Sort(lset)
+		lset := d.DecodeLabels(&dec)
 
 		series = append(series, RefSeries{
 			Ref:    chunks.HeadSeriesRef(ref),
@@ -117,6 +199,61 @@ func (d *Decoder) Series(rec []byte, series []RefSeries) ([]RefSeries, error) {
 	return series, nil
 }
 
+// Metadata appends metadata in rec to the given slice.
+func (d *Decoder) Metadata(rec []byte, metadata []RefMetadata) ([]RefMetadata, error) {
+	dec := encoding.Decbuf{B: rec}
+
+	if Type(dec.Byte()) != Metadata {
+		return nil, errors.New("invalid record type")
+	}
+	for len(dec.B) > 0 && dec.Err() == nil {
+		ref := dec.Uvarint64()
+		typ := dec.Byte()
+		numFields := dec.Uvarint()
+
+		// We're currently aware of two more metadata fields other than TYPE; that is UNIT and HELP.
+		// We can skip the rest of the fields (if we encounter any), but we must decode them anyway
+		// so we can correctly align with the start with the next metadata record.
+		var unit, help string
+		for i := 0; i < numFields; i++ {
+			fieldName := dec.UvarintStr()
+			fieldValue := dec.UvarintStr()
+			switch fieldName {
+			case unitMetaName:
+				unit = fieldValue
+			case helpMetaName:
+				help = fieldValue
+			}
+		}
+
+		metadata = append(metadata, RefMetadata{
+			Ref:  chunks.HeadSeriesRef(ref),
+			Type: typ,
+			Unit: unit,
+			Help: help,
+		})
+	}
+	if dec.Err() != nil {
+		return nil, dec.Err()
+	}
+	if len(dec.B) > 0 {
+		return nil, errors.Errorf("unexpected %d bytes left in entry", len(dec.B))
+	}
+	return metadata, nil
+}
+
+// DecodeLabels decodes one set of labels from buf.
+func (d *Decoder) DecodeLabels(dec *encoding.Decbuf) labels.Labels {
+	lset := make(labels.Labels, dec.Uvarint())
+
+	for i := range lset {
+		lset[i].Name = dec.UvarintStr()
+		lset[i].Value = dec.UvarintStr()
+	}
+	sort.Sort(lset)
+	return lset
+}
+
 // Samples appends samples in rec to the given slice.
 func (d *Decoder) Samples(rec []byte, samples []RefSample) ([]RefSample, error) {
 	dec := encoding.Decbuf{B: rec}
@@ -198,13 +335,7 @@ func (d *Decoder) ExemplarsFromBuffer(dec *encoding.Decbuf, exemplars []RefExemp
 		dref := dec.Varint64()
 		dtime := dec.Varint64()
 		val := dec.Be64()
-
-		lset := make(labels.Labels, dec.Uvarint())
-		for i := range lset {
-			lset[i].Name = dec.UvarintStr()
-			lset[i].Value = dec.UvarintStr()
-		}
-		sort.Sort(lset)
+		lset := d.DecodeLabels(dec)
 
 		exemplars = append(exemplars, RefExemplar{
 			Ref:    chunks.HeadSeriesRef(baseRef + uint64(dref)),
@@ -234,16 +365,41 @@ func (e *Encoder) Series(series []RefSeries, b []byte) []byte {
 
 	for _, s := range series {
 		buf.PutBE64(uint64(s.Ref))
-		buf.PutUvarint(len(s.Labels))
+		EncodeLabels(&buf, s.Labels)
+	}
+	return buf.Get()
+}
 
-		for _, l := range s.Labels {
-			buf.PutUvarintStr(l.Name)
-			buf.PutUvarintStr(l.Value)
-		}
+// Metadata appends the encoded metadata to b and returns the resulting slice.
+func (e *Encoder) Metadata(metadata []RefMetadata, b []byte) []byte {
+	buf := encoding.Encbuf{B: b}
+	buf.PutByte(byte(Metadata))
+
+	for _, m := range metadata {
+		buf.PutUvarint64(uint64(m.Ref))
+
+		buf.PutByte(m.Type)
+
+		buf.PutUvarint(2) // num_fields: We currently have two more metadata fields, UNIT and HELP.
+		buf.PutUvarintStr(unitMetaName)
+		buf.PutUvarintStr(m.Unit)
+		buf.PutUvarintStr(helpMetaName)
+		buf.PutUvarintStr(m.Help)
 	}
+
 	return buf.Get()
 }
 
+// EncodeLabels encodes the contents of labels into buf.
+func EncodeLabels(buf *encoding.Encbuf, lbls labels.Labels) {
+	buf.PutUvarint(len(lbls))
+
+	for _, l := range lbls {
+		buf.PutUvarintStr(l.Name)
+		buf.PutUvarintStr(l.Value)
+	}
+}
+
 // Samples appends the encoded samples to b and returns the resulting slice.
 func (e *Encoder) Samples(samples []RefSample, b []byte) []byte {
 	buf := encoding.Encbuf{B: b}
@@ -308,11 +464,6 @@ func (e *Encoder) EncodeExemplarsIntoBuffer(exemplars []RefExemplar, buf *encodi
 		buf.PutVarint64(int64(ex.Ref) - int64(first.Ref))
 		buf.PutVarint64(ex.T - first.T)
 		buf.PutBE64(math.Float64bits(ex.V))
-
-		buf.PutUvarint(len(ex.Labels))
-		for _, l := range ex.Labels {
-			buf.PutUvarintStr(l.Name)
-			buf.PutUvarintStr(l.Value)
-		}
+		EncodeLabels(buf, ex.Labels)
 	}
 }
diff --git a/tsdb/wal.go b/tsdb/wal.go
index 2b9f117aa..615903c63 100644
--- a/tsdb/wal.go
+++ b/tsdb/wal.go
@@ -23,7 +23,6 @@ import (
 	"math"
 	"os"
 	"path/filepath"
-	"sort"
 	"sync"
 	"time"
 
@@ -32,7 +31,6 @@ import (
 	"github.com/pkg/errors"
 	"github.com/prometheus/client_golang/prometheus"
 
-	"github.com/prometheus/prometheus/model/labels"
 	"github.com/prometheus/prometheus/storage"
 	"github.com/prometheus/prometheus/tsdb/chunks"
 	"github.com/prometheus/prometheus/tsdb/encoding"
@@ -790,12 +788,7 @@ const (
 func (w *SegmentWAL) encodeSeries(buf *encoding.Encbuf, series []record.RefSeries) uint8 {
 	for _, s := range series {
 		buf.PutBE64(uint64(s.Ref))
-		buf.PutUvarint(len(s.Labels))
-
-		for _, l := range s.Labels {
-			buf.PutUvarintStr(l.Name)
-			buf.PutUvarintStr(l.Value)
-		}
+		record.EncodeLabels(buf, s.Labels)
 	}
 	return walSeriesSimple
 }
@@ -840,6 +833,7 @@ type walReader struct {
 	cur   int
 	buf   []byte
 	crc32 hash.Hash32
+	dec   record.Decoder
 
 	curType    WALEntryType
 	curFlag    byte
@@ -1123,14 +1117,7 @@ func (r *walReader) decodeSeries(flag byte, b []byte, res *[]record.RefSeries) e
 
 	for len(dec.B) > 0 && dec.Err() == nil {
 		ref := chunks.HeadSeriesRef(dec.Be64())
-
-		lset := make(labels.Labels, dec.Uvarint())
-
-		for i := range lset {
-			lset[i].Name = dec.UvarintStr()
-			lset[i].Value = dec.UvarintStr()
-		}
-		sort.Sort(lset)
+		lset := r.dec.DecodeLabels(&dec)
 
 		*res = append(*res, record.RefSeries{
 			Ref:    ref,
diff --git a/tsdb/wal/checkpoint.go b/tsdb/wal/checkpoint.go
index af4bd940c..f8f9f387d 100644
--- a/tsdb/wal/checkpoint.go
+++ b/tsdb/wal/checkpoint.go
@@ -41,10 +41,12 @@ type CheckpointStats struct {
 	DroppedSamples    int
 	DroppedTombstones int
 	DroppedExemplars  int
+	DroppedMetadata   int
 	TotalSeries       int // Processed series including dropped ones.
 	TotalSamples      int // Processed samples including dropped ones.
 	TotalTombstones   int // Processed tombstones including dropped ones.
 	TotalExemplars    int // Processed exemplars including dropped ones.
+	TotalMetadata     int // Processed metadata including dropped ones.
 }
 
 // LastCheckpoint returns the directory name and index of the most recent checkpoint.
@@ -84,7 +86,8 @@ const checkpointPrefix = "checkpoint."
 
 // Checkpoint creates a compacted checkpoint of segments in range [from, to] in the given WAL.
 // It includes the most recent checkpoint if it exists.
-// All series not satisfying keep and samples/tombstones/exemplars below mint are dropped.
+// All series not satisfying keep, samples/tombstones/exemplars below mint and
+// metadata that are not the latest are dropped.
 //
 // The checkpoint is stored in a directory named checkpoint.N in the same
 // segmented format as the original WAL itself.
@@ -149,13 +152,16 @@ func Checkpoint(logger log.Logger, w *WAL, from, to int, keep func(id chunks.Hea
 		samples   []record.RefSample
 		tstones   []tombstones.Stone
 		exemplars []record.RefExemplar
+		metadata  []record.RefMetadata
 		dec       record.Decoder
 		enc       record.Encoder
 		buf       []byte
 		recs      [][]byte
+
+		latestMetadataMap = make(map[chunks.HeadSeriesRef]record.RefMetadata)
 	)
 	for r.Next() {
-		series, samples, tstones, exemplars = series[:0], samples[:0], tstones[:0], exemplars[:0]
+		series, samples, tstones, exemplars, metadata = series[:0], samples[:0], tstones[:0], exemplars[:0], metadata[:0]
 
 		// We don't reset the buffer since we batch up multiple records
 		// before writing them to the checkpoint.
@@ -238,6 +244,23 @@ func Checkpoint(logger log.Logger, w *WAL, from, to int, keep func(id chunks.Hea
 			}
 			stats.TotalExemplars += len(exemplars)
 			stats.DroppedExemplars += len(exemplars) - len(repl)
+		case record.Metadata:
+			metadata, err := dec.Metadata(rec, metadata)
+			if err != nil {
+				return nil, errors.Wrap(err, "decode metadata")
+			}
+			// Only keep reference to the latest found metadata for each refID.
+			repl := 0
+			for _, m := range metadata {
+				if keep(m.Ref) {
+					if _, ok := latestMetadataMap[m.Ref]; !ok {
+						repl++
+					}
+					latestMetadataMap[m.Ref] = m
+				}
+			}
+			stats.TotalMetadata += len(metadata)
+			stats.DroppedMetadata += len(metadata) - repl
 		default:
 			// Unknown record type, probably from a future Prometheus version.
 			continue
@@ -265,6 +288,18 @@ func Checkpoint(logger log.Logger, w *WAL, from, to int, keep func(id chunks.Hea
 	if err := cp.Log(recs...); err != nil {
 		return nil, errors.Wrap(err, "flush records")
 	}
+
+	// Flush latest metadata records for each series.
+	if len(latestMetadataMap) > 0 {
+		latestMetadata := make([]record.RefMetadata, 0, len(latestMetadataMap))
+		for _, m := range latestMetadataMap {
+			latestMetadata = append(latestMetadata, m)
+		}
+		if err := cp.Log(enc.Metadata(latestMetadata, buf[:0])); err != nil {
+			return nil, errors.Wrap(err, "flush metadata records")
+		}
+	}
+
 	if err := cp.Close(); err != nil {
 		return nil, errors.Wrap(err, "close checkpoint")
 	}
diff --git a/tsdb/wal/watcher.go b/tsdb/wal/watcher.go
index 7b026b4ea..5b949fc00 100644
--- a/tsdb/wal/watcher.go
+++ b/tsdb/wal/watcher.go
@@ -481,7 +481,7 @@ func (w *Watcher) readSegment(r *LiveReader, segmentNum int, tail bool) error {
 	)
 	for r.Next() && !isClosed(w.quit) {
 		rec := r.Record()
-		w.recordsReadMetric.WithLabelValues(recordType(dec.Type(rec))).Inc()
+		w.recordsReadMetric.WithLabelValues(dec.Type(rec).String()).Inc()
 
 		switch dec.Type(rec) {
 		case record.Series:
@@ -554,7 +554,7 @@ func (w *Watcher) readSegmentForGC(r *LiveReader, segmentNum int, _ bool) error
 	)
 	for r.Next() && !isClosed(w.quit) {
 		rec := r.Record()
-		w.recordsReadMetric.WithLabelValues(recordType(dec.Type(rec))).Inc()
+		w.recordsReadMetric.WithLabelValues(dec.Type(rec).String()).Inc()
 
 		switch dec.Type(rec) {
 		case record.Series:
@@ -583,19 +583,6 @@ func (w *Watcher) SetStartTime(t time.Time) {
 	w.startTimestamp = timestamp.FromTime(t)
 }
 
-func recordType(rt record.Type) string {
-	switch rt {
-	case record.Series:
-		return "series"
-	case record.Samples:
-		return "samples"
-	case record.Tombstones:
-		return "tombstones"
-	default:
-		return "unknown"
-	}
-}
-
 type segmentReadFn func(w *Watcher, r *LiveReader, segmentNum int, tail bool) error
 
 // Read all the series records from a Checkpoint directory.
diff --git a/util/runtime/uname_linux.go b/util/runtime/uname_linux.go
index cf81b13ec..ce3bc42a2 100644
--- a/util/runtime/uname_linux.go
+++ b/util/runtime/uname_linux.go
@@ -13,11 +13,7 @@
 
 package runtime
 
-import (
-	"bytes"
-
-	"golang.org/x/sys/unix"
-)
+import "golang.org/x/sys/unix"
 
 // Uname returns the uname of the host machine.
 func Uname() string {
@@ -27,11 +23,11 @@ func Uname() string {
 		panic("unix.Uname failed: " + err.Error())
 	}
 
-	str := "(" + string(buf.Sysname[:bytes.IndexByte(buf.Sysname[:], 0)])
-	str += " " + string(buf.Release[:bytes.IndexByte(buf.Release[:], 0)])
-	str += " " + string(buf.Version[:bytes.IndexByte(buf.Version[:], 0)])
-	str += " " + string(buf.Machine[:bytes.IndexByte(buf.Machine[:], 0)])
-	str += " " + string(buf.Nodename[:bytes.IndexByte(buf.Nodename[:], 0)])
-	str += " " + string(buf.Domainname[:bytes.IndexByte(buf.Domainname[:], 0)]) + ")"
+	str := "(" + unix.ByteSliceToString(buf.Sysname[:])
+	str += " " + unix.ByteSliceToString(buf.Release[:])
+	str += " " + unix.ByteSliceToString(buf.Version[:])
+	str += " " + unix.ByteSliceToString(buf.Machine[:])
+	str += " " + unix.ByteSliceToString(buf.Nodename[:])
+	str += " " + unix.ByteSliceToString(buf.Domainname[:]) + ")"
 	return str
 }
diff --git a/web/api/v1/api.go b/web/api/v1/api.go
index 0ad24abdb..3c4a34291 100644
--- a/web/api/v1/api.go
+++ b/web/api/v1/api.go
@@ -323,6 +323,9 @@ func (api *API) Register(r *route.Router) {
 	r.Get("/query_exemplars", wrapAgent(api.queryExemplars))
 	r.Post("/query_exemplars", wrapAgent(api.queryExemplars))
 
+	r.Get("/format_query", wrapAgent(api.formatQuery))
+	r.Post("/format_query", wrapAgent(api.formatQuery))
+
 	r.Get("/labels", wrapAgent(api.labelNames))
 	r.Post("/labels", wrapAgent(api.labelNames))
 	r.Get("/label/:name/values", wrapAgent(api.labelValues))
@@ -428,6 +431,15 @@ func (api *API) query(r *http.Request) (result apiFuncResult) {
 	}, nil, res.Warnings, qry.Close}
 }
 
+func (api *API) formatQuery(r *http.Request) (result apiFuncResult) {
+	expr, err := parser.ParseExpr(r.FormValue("query"))
+	if err != nil {
+		return invalidParamError(err, "query")
+	}
+
+	return apiFuncResult{expr.Pretty(0), nil, nil, nil}
+}
+
 func extractQueryOpts(r *http.Request) *promql.QueryOpts {
 	return &promql.QueryOpts{
 		EnablePerStepStats: r.FormValue("stats") == "all",
diff --git a/web/ui/README.md b/web/ui/README.md
index a6c1fe711..b95422867 100644
--- a/web/ui/README.md
+++ b/web/ui/README.md
@@ -1,6 +1,6 @@
 ## Overview
 The `ui` directory contains static files and templates used in the web UI. For
-easier distribution they are compressend (c.f. Makefile) and statically compiled 
+easier distribution they are compressed (c.f. Makefile) and statically compiled 
 into the Prometheus binary using the embed package.
 
 During development it is more convenient to always use the files on disk to 
diff --git a/web/ui/react-app/src/pages/graph/CMTheme.tsx b/web/ui/react-app/src/pages/graph/CMTheme.tsx
index 55b319dc2..6436a04ed 100644
--- a/web/ui/react-app/src/pages/graph/CMTheme.tsx
+++ b/web/ui/react-app/src/pages/graph/CMTheme.tsx
@@ -19,8 +19,6 @@ export const baseTheme = EditorView.theme({
   },
 
   '.cm-matchingBracket': {
-    color: '#000',
-    backgroundColor: '#dedede',
     fontWeight: 'bold',
     outline: '1px dashed transparent',
   },
@@ -82,7 +80,6 @@ export const baseTheme = EditorView.theme({
   '.cm-completionMatchedText': {
     textDecoration: 'none',
     fontWeight: 'bold',
-    color: '#0066bf',
   },
 
   '.cm-selectionMatch': {
@@ -105,12 +102,10 @@ export const baseTheme = EditorView.theme({
     fontFamily: 'codicon',
     paddingRight: '0',
     opacity: '1',
-    color: '#007acc',
   },
 
   '.cm-completionIcon-function, .cm-completionIcon-method': {
     '&:after': { content: "'\\ea8c'" },
-    color: '#652d90',
   },
   '.cm-completionIcon-class': {
     '&:after': { content: "'○'" },
@@ -123,7 +118,6 @@ export const baseTheme = EditorView.theme({
   },
   '.cm-completionIcon-constant': {
     '&:after': { content: "'\\eb5f'" },
-    color: '#007acc',
   },
   '.cm-completionIcon-type': {
     '&:after': { content: "'𝑡'" },
@@ -136,7 +130,6 @@ export const baseTheme = EditorView.theme({
   },
   '.cm-completionIcon-keyword': {
     '&:after': { content: "'\\eb62'" },
-    color: '#616161',
   },
   '.cm-completionIcon-namespace': {
     '&:after': { content: "'▢'" },
@@ -187,6 +180,31 @@ export const lightTheme = EditorView.theme(
         backgroundColor: '#add6ff',
       },
     },
+
+    '.cm-matchingBracket': {
+      color: '#000',
+      backgroundColor: '#dedede',
+    },
+
+    '.cm-completionMatchedText': {
+      color: '#0066bf',
+    },
+
+    '.cm-completionIcon': {
+      color: '#007acc',
+    },
+
+    '.cm-completionIcon-constant': {
+      color: '#007acc',
+    },
+
+    '.cm-completionIcon-function, .cm-completionIcon-method': {
+      color: '#652d90',
+    },
+
+    '.cm-completionIcon-keyword': {
+      color: '#616161',
+    },
   },
   { dark: false }
 );
@@ -220,6 +238,26 @@ export const darkTheme = EditorView.theme(
         backgroundColor: '#767676',
       },
     },
+
+    '.cm-matchingBracket, &.cm-focused .cm-matchingBracket': {
+      backgroundColor: '#616161',
+    },
+
+    '.cm-completionMatchedText': {
+      color: '#7dd3fc',
+    },
+
+    '.cm-completionIcon, .cm-completionIcon-constant': {
+      color: '#7dd3fc',
+    },
+
+    '.cm-completionIcon-function, .cm-completionIcon-method': {
+      color: '#d8b4fe',
+    },
+
+    '.cm-completionIcon-keyword': {
+      color: '#cbd5e1 !important',
+    },
   },
   { dark: true }
 );
@@ -239,3 +277,19 @@ export const promqlHighlighter = HighlightStyle.define([
   { tag: tags.invalid, color: 'red' },
   { tag: tags.comment, color: '#888', fontStyle: 'italic' },
 ]);
+
+export const darkPromqlHighlighter = HighlightStyle.define([
+  { tag: tags.name, color: '#000' },
+  { tag: tags.number, color: '#22c55e' },
+  { tag: tags.string, color: '#fca5a5' },
+  { tag: tags.keyword, color: '#14bfad' },
+  { tag: tags.function(tags.variableName), color: '#14bfad' },
+  { tag: tags.labelName, color: '#ff8585' },
+  { tag: tags.operator },
+  { tag: tags.modifier, color: '#14bfad' },
+  { tag: tags.paren },
+  { tag: tags.squareBracket },
+  { tag: tags.brace },
+  { tag: tags.invalid, color: '#ff3d3d' },
+  { tag: tags.comment, color: '#9ca3af', fontStyle: 'italic' },
+]);
diff --git a/web/ui/react-app/src/pages/graph/ExpressionInput.tsx b/web/ui/react-app/src/pages/graph/ExpressionInput.tsx
index d93014ef2..ad6770849 100644
--- a/web/ui/react-app/src/pages/graph/ExpressionInput.tsx
+++ b/web/ui/react-app/src/pages/graph/ExpressionInput.tsx
@@ -1,5 +1,5 @@
 import React, { FC, useState, useEffect, useRef } from 'react';
-import { Button, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
+import { Alert, Button, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
 
 import { EditorView, highlightSpecialChars, keymap, ViewUpdate, placeholder } from '@codemirror/view';
 import { EditorState, Prec, Compartment } from '@codemirror/state';
@@ -15,15 +15,16 @@ import {
   closeBrackets,
   closeBracketsKeymap,
 } from '@codemirror/autocomplete';
-import { baseTheme, lightTheme, darkTheme, promqlHighlighter } from './CMTheme';
+import { baseTheme, lightTheme, darkTheme, promqlHighlighter, darkPromqlHighlighter } from './CMTheme';
 
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faSearch, faSpinner, faGlobeEurope } from '@fortawesome/free-solid-svg-icons';
+import { faSearch, faSpinner, faGlobeEurope, faIndent, faCheck } from '@fortawesome/free-solid-svg-icons';
 import MetricsExplorer from './MetricsExplorer';
 import { usePathPrefix } from '../../contexts/PathPrefixContext';
 import { useTheme } from '../../contexts/ThemeContext';
 import { CompleteStrategy, PromQLExtension } from '@prometheus-io/codemirror-promql';
 import { newCompleteStrategy } from '@prometheus-io/codemirror-promql/dist/esm/complete';
+import { API_PATH } from '../../constants/constants';
 
 const promqlExtension = new PromQLExtension();
 
@@ -98,6 +99,10 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
   const pathPrefix = usePathPrefix();
   const { theme } = useTheme();
 
+  const [formatError, setFormatError] = useState<string | null>(null);
+  const [isFormatting, setIsFormatting] = useState<boolean>(false);
+  const [exprFormatted, setExprFormatted] = useState<boolean>(false);
+
   // (Re)initialize editor based on settings / setting changes.
   useEffect(() => {
     // Build the dynamic part of the config.
@@ -112,8 +117,14 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
           queryHistory
         ),
       });
+
+    let highlighter = syntaxHighlighting(theme === 'dark' ? darkPromqlHighlighter : promqlHighlighter);
+    if (theme === 'dark') {
+      highlighter = syntaxHighlighting(darkPromqlHighlighter);
+    }
+
     const dynamicConfig = [
-      enableHighlighting ? syntaxHighlighting(promqlHighlighter) : [],
+      enableHighlighting ? highlighter : [],
       promqlExtension.asExtension(),
       theme === 'dark' ? darkTheme : lightTheme,
     ];
@@ -169,7 +180,10 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
             ])
           ),
           EditorView.updateListener.of((update: ViewUpdate): void => {
-            onExpressionChange(update.state.doc.toString());
+            if (update.docChanged) {
+              onExpressionChange(update.state.doc.toString());
+              setExprFormatted(false);
+            }
           }),
         ],
       });
@@ -209,6 +223,47 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
     );
   };
 
+  const formatExpression = () => {
+    setFormatError(null);
+    setIsFormatting(true);
+
+    fetch(
+      `${pathPrefix}/${API_PATH}/format_query?${new URLSearchParams({
+        query: value,
+      })}`,
+      {
+        cache: 'no-store',
+        credentials: 'same-origin',
+      }
+    )
+      .then((resp) => {
+        if (!resp.ok && resp.status !== 400) {
+          throw new Error(`format HTTP request failed: ${resp.statusText}`);
+        }
+
+        return resp.json();
+      })
+      .then((json) => {
+        if (json.status !== 'success') {
+          throw new Error(json.error || 'invalid response JSON');
+        }
+
+        const view = viewRef.current;
+        if (view === null) {
+          return;
+        }
+
+        view.dispatch(view.state.update({ changes: { from: 0, to: view.state.doc.length, insert: json.data } }));
+        setExprFormatted(true);
+      })
+      .catch((err) => {
+        setFormatError(err.message);
+      })
+      .finally(() => {
+        setIsFormatting(false);
+      });
+  };
+
   return (
     <>
       <InputGroup className="expression-input">
@@ -220,7 +275,21 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
         <div ref={containerRef} className="cm-expression-input" />
         <InputGroupAddon addonType="append">
           <Button
-            className="metrics-explorer-btn"
+            className="expression-input-action-btn"
+            title={isFormatting ? 'Formatting expression' : exprFormatted ? 'Expression formatted' : 'Format expression'}
+            onClick={formatExpression}
+            disabled={isFormatting || exprFormatted}
+          >
+            {isFormatting ? (
+              <FontAwesomeIcon icon={faSpinner} spin />
+            ) : exprFormatted ? (
+              <FontAwesomeIcon icon={faCheck} />
+            ) : (
+              <FontAwesomeIcon icon={faIndent} />
+            )}
+          </Button>
+          <Button
+            className="expression-input-action-btn"
             title="Open metrics explorer"
             onClick={() => setShowMetricsExplorer(true)}
           >
@@ -232,6 +301,8 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
         </InputGroupAddon>
       </InputGroup>
 
+      {formatError && <Alert color="danger">Error formatting expression: {formatError}</Alert>}
+
       <MetricsExplorer
         show={showMetricsExplorer}
         updateShow={setShowMetricsExplorer}
diff --git a/web/ui/react-app/src/themes/_shared.scss b/web/ui/react-app/src/themes/_shared.scss
index e0af8191c..a03bf1cbd 100644
--- a/web/ui/react-app/src/themes/_shared.scss
+++ b/web/ui/react-app/src/themes/_shared.scss
@@ -7,7 +7,7 @@
       Example:
 
       $foo = '#000';
- 
+
    2. Add a style rule in _shared.scss (this file) that uses new variable.
       Example:
 
@@ -58,7 +58,7 @@
 .metrics-explorer .metric:hover {
   background: $metrics-explorer-bg;
 }
-button.metrics-explorer-btn {
+button.expression-input-action-btn {
   color: $input-group-addon-color;
   background-color: $input-group-addon-bg;
   border: $input-border-width solid $input-group-addon-border-color;
@@ -114,7 +114,7 @@ button.execute-btn {
 }
 
 input[type='checkbox']:checked + label {
-  color: #286090;
+  color: $checked-checkbox-color;
 }
 
 .custom-control-label {
diff --git a/web/ui/react-app/src/themes/dark.scss b/web/ui/react-app/src/themes/dark.scss
index 063077941..0989c833c 100644
--- a/web/ui/react-app/src/themes/dark.scss
+++ b/web/ui/react-app/src/themes/dark.scss
@@ -11,12 +11,14 @@ $config-yaml-color: $black;
 $config-yaml-bg: $gray-500;
 $config-yaml-border: $gray-700;
 
-$query-stats-color: $secondary;
+$query-stats-color: lighten($secondary, 20%);
 
 $metrics-explorer-bg: $dropdown-link-hover-bg;
 
 $clear-time-btn-bg: $secondary;
 
+$checked-checkbox-color: #60a5fa;
+
 .bootstrap-dark {
   @import './shared';
 }
diff --git a/web/ui/react-app/src/themes/light.scss b/web/ui/react-app/src/themes/light.scss
index 4bcf7e67f..8e6b10e80 100644
--- a/web/ui/react-app/src/themes/light.scss
+++ b/web/ui/react-app/src/themes/light.scss
@@ -16,6 +16,8 @@ $metrics-explorer-bg: #efefef;
 
 $clear-time-btn-bg: $white;
 
+$checked-checkbox-color: #286090;
+
 .bootstrap {
   @import './shared';
 }


Test patch:
diff --git a/cmd/promtool/sd_test.go b/cmd/promtool/sd_test.go
index 5fa97bf8a..51fe59dac 100644
--- a/cmd/promtool/sd_test.go
+++ b/cmd/promtool/sd_test.go
@@ -69,5 +69,5 @@ func TestSDCheckResult(t *testing.T) {
 		},
 	}
 
-	require.Equal(t, expectedSDCheckResult, getSDCheckResult(targetGroups, scrapeConfig))
+	require.Equal(t, expectedSDCheckResult, getSDCheckResult(targetGroups, scrapeConfig, true))
 }
diff --git a/discovery/kubernetes/endpoints_test.go b/discovery/kubernetes/endpoints_test.go
index 5fd9460ae..91b1b0c67 100644
--- a/discovery/kubernetes/endpoints_test.go
+++ b/discovery/kubernetes/endpoints_test.go
@@ -128,7 +128,8 @@ func TestEndpointsDiscoveryAdd(t *testing.T) {
 			NodeName: "testnode",
 			Containers: []v1.Container{
 				{
-					Name: "c1",
+					Name:  "c1",
+					Image: "c1:latest",
 					Ports: []v1.ContainerPort{
 						{
 							Name:          "mainport",
@@ -138,7 +139,8 @@ func TestEndpointsDiscoveryAdd(t *testing.T) {
 					},
 				},
 				{
-					Name: "c2",
+					Name:  "c2",
+					Image: "c2:latest",
 					Ports: []v1.ContainerPort{
 						{
 							Name:          "sideport",
@@ -206,6 +208,7 @@ func TestEndpointsDiscoveryAdd(t *testing.T) {
 						"__meta_kubernetes_pod_node_name":                "testnode",
 						"__meta_kubernetes_pod_host_ip":                  "2.3.4.5",
 						"__meta_kubernetes_pod_container_name":           "c1",
+						"__meta_kubernetes_pod_container_image":          "c1:latest",
 						"__meta_kubernetes_pod_container_port_name":      "mainport",
 						"__meta_kubernetes_pod_container_port_number":    "9000",
 						"__meta_kubernetes_pod_container_port_protocol":  "TCP",
@@ -220,6 +223,7 @@ func TestEndpointsDiscoveryAdd(t *testing.T) {
 						"__meta_kubernetes_pod_node_name":               "testnode",
 						"__meta_kubernetes_pod_host_ip":                 "2.3.4.5",
 						"__meta_kubernetes_pod_container_name":          "c2",
+						"__meta_kubernetes_pod_container_image":         "c2:latest",
 						"__meta_kubernetes_pod_container_port_name":     "sideport",
 						"__meta_kubernetes_pod_container_port_number":   "9001",
 						"__meta_kubernetes_pod_container_port_protocol": "TCP",
@@ -649,7 +653,8 @@ func TestEndpointsDiscoveryNamespaces(t *testing.T) {
 				NodeName: "testnode",
 				Containers: []v1.Container{
 					{
-						Name: "c1",
+						Name:  "c1",
+						Image: "c1:latest",
 						Ports: []v1.ContainerPort{
 							{
 								Name:          "mainport",
@@ -720,6 +725,7 @@ func TestEndpointsDiscoveryNamespaces(t *testing.T) {
 						"__meta_kubernetes_pod_node_name":                "testnode",
 						"__meta_kubernetes_pod_host_ip":                  "2.3.4.5",
 						"__meta_kubernetes_pod_container_name":           "c1",
+						"__meta_kubernetes_pod_container_image":          "c1:latest",
 						"__meta_kubernetes_pod_container_port_name":      "mainport",
 						"__meta_kubernetes_pod_container_port_number":    "9000",
 						"__meta_kubernetes_pod_container_port_protocol":  "TCP",
@@ -753,7 +759,8 @@ func TestEndpointsDiscoveryOwnNamespace(t *testing.T) {
 			NodeName: "testnode",
 			Containers: []v1.Container{
 				{
-					Name: "p1",
+					Name:  "p1",
+					Image: "p1:latest",
 					Ports: []v1.ContainerPort{
 						{
 							Name:          "mainport",
diff --git a/discovery/kubernetes/endpointslice_test.go b/discovery/kubernetes/endpointslice_test.go
index 91408c009..a0ae543fc 100644
--- a/discovery/kubernetes/endpointslice_test.go
+++ b/discovery/kubernetes/endpointslice_test.go
@@ -234,7 +234,8 @@ func TestEndpointSliceDiscoveryAdd(t *testing.T) {
 			NodeName: "testnode",
 			Containers: []corev1.Container{
 				{
-					Name: "c1",
+					Name:  "c1",
+					Image: "c1:latest",
 					Ports: []corev1.ContainerPort{
 						{
 							Name:          "mainport",
@@ -244,7 +245,8 @@ func TestEndpointSliceDiscoveryAdd(t *testing.T) {
 					},
 				},
 				{
-					Name: "c2",
+					Name:  "c2",
+					Image: "c2:latest",
 					Ports: []corev1.ContainerPort{
 						{
 							Name:          "sideport",
@@ -307,6 +309,7 @@ func TestEndpointSliceDiscoveryAdd(t *testing.T) {
 						"__meta_kubernetes_endpointslice_port_name":                 "testport",
 						"__meta_kubernetes_endpointslice_port_protocol":             "TCP",
 						"__meta_kubernetes_pod_container_name":                      "c1",
+						"__meta_kubernetes_pod_container_image":                     "c1:latest",
 						"__meta_kubernetes_pod_container_port_name":                 "mainport",
 						"__meta_kubernetes_pod_container_port_number":               "9000",
 						"__meta_kubernetes_pod_container_port_protocol":             "TCP",
@@ -321,6 +324,7 @@ func TestEndpointSliceDiscoveryAdd(t *testing.T) {
 					{
 						"__address__":                                   "1.2.3.4:9001",
 						"__meta_kubernetes_pod_container_name":          "c2",
+						"__meta_kubernetes_pod_container_image":         "c2:latest",
 						"__meta_kubernetes_pod_container_port_name":     "sideport",
 						"__meta_kubernetes_pod_container_port_number":   "9001",
 						"__meta_kubernetes_pod_container_port_protocol": "TCP",
@@ -878,7 +882,8 @@ func TestEndpointSliceDiscoveryNamespaces(t *testing.T) {
 				NodeName: "testnode",
 				Containers: []corev1.Container{
 					{
-						Name: "c1",
+						Name:  "c1",
+						Image: "c1:latest",
 						Ports: []corev1.ContainerPort{
 							{
 								Name:          "mainport",
@@ -953,6 +958,7 @@ func TestEndpointSliceDiscoveryNamespaces(t *testing.T) {
 						"__meta_kubernetes_endpointslice_port_name":           "testport",
 						"__meta_kubernetes_endpointslice_port_protocol":       "TCP",
 						"__meta_kubernetes_pod_container_name":                "c1",
+						"__meta_kubernetes_pod_container_image":               "c1:latest",
 						"__meta_kubernetes_pod_container_port_name":           "mainport",
 						"__meta_kubernetes_pod_container_port_number":         "9000",
 						"__meta_kubernetes_pod_container_port_protocol":       "TCP",
@@ -993,7 +999,8 @@ func TestEndpointSliceDiscoveryOwnNamespace(t *testing.T) {
 			NodeName: "testnode",
 			Containers: []corev1.Container{
 				{
-					Name: "p1",
+					Name:  "p1",
+					Image: "p1:latest",
 					Ports: []corev1.ContainerPort{
 						{
 							Name:          "mainport",
diff --git a/discovery/kubernetes/pod_test.go b/discovery/kubernetes/pod_test.go
index 29655d022..f2b79dbb8 100644
--- a/discovery/kubernetes/pod_test.go
+++ b/discovery/kubernetes/pod_test.go
@@ -50,7 +50,8 @@ func makeMultiPortPods() *v1.Pod {
 			NodeName: "testnode",
 			Containers: []v1.Container{
 				{
-					Name: "testcontainer0",
+					Name:  "testcontainer0",
+					Image: "testcontainer0:latest",
 					Ports: []v1.ContainerPort{
 						{
 							Name:          "testport0",
@@ -65,7 +66,8 @@ func makeMultiPortPods() *v1.Pod {
 					},
 				},
 				{
-					Name: "testcontainer1",
+					Name:  "testcontainer1",
+					Image: "testcontainer1:latest",
 				},
 			},
 		},
@@ -94,7 +96,8 @@ func makePods() *v1.Pod {
 			NodeName: "testnode",
 			Containers: []v1.Container{
 				{
-					Name: "testcontainer",
+					Name:  "testcontainer",
+					Image: "testcontainer:latest",
 					Ports: []v1.ContainerPort{
 						{
 							Name:          "testport",
@@ -130,7 +133,8 @@ func makeInitContainerPods() *v1.Pod {
 			NodeName: "testnode",
 			Containers: []v1.Container{
 				{
-					Name: "testcontainer",
+					Name:  "testcontainer",
+					Image: "testcontainer:latest",
 					Ports: []v1.ContainerPort{
 						{
 							Name:          "testport",
@@ -143,7 +147,8 @@ func makeInitContainerPods() *v1.Pod {
 
 			InitContainers: []v1.Container{
 				{
-					Name: "initcontainer",
+					Name:  "initcontainer",
+					Image: "initcontainer:latest",
 				},
 			},
 		},
@@ -169,6 +174,7 @@ func expectedPodTargetGroups(ns string) map[string]*targetgroup.Group {
 				{
 					"__address__":                                   "1.2.3.4:9000",
 					"__meta_kubernetes_pod_container_name":          "testcontainer",
+					"__meta_kubernetes_pod_container_image":         "testcontainer:latest",
 					"__meta_kubernetes_pod_container_port_name":     "testport",
 					"__meta_kubernetes_pod_container_port_number":   "9000",
 					"__meta_kubernetes_pod_container_port_protocol": "TCP",
@@ -219,6 +225,7 @@ func TestPodDiscoveryBeforeRun(t *testing.T) {
 					{
 						"__address__":                                   "1.2.3.4:9000",
 						"__meta_kubernetes_pod_container_name":          "testcontainer0",
+						"__meta_kubernetes_pod_container_image":         "testcontainer0:latest",
 						"__meta_kubernetes_pod_container_port_name":     "testport0",
 						"__meta_kubernetes_pod_container_port_number":   "9000",
 						"__meta_kubernetes_pod_container_port_protocol": "TCP",
@@ -227,15 +234,17 @@ func TestPodDiscoveryBeforeRun(t *testing.T) {
 					{
 						"__address__":                                   "1.2.3.4:9001",
 						"__meta_kubernetes_pod_container_name":          "testcontainer0",
+						"__meta_kubernetes_pod_container_image":         "testcontainer0:latest",
 						"__meta_kubernetes_pod_container_port_name":     "testport1",
 						"__meta_kubernetes_pod_container_port_number":   "9001",
 						"__meta_kubernetes_pod_container_port_protocol": "UDP",
 						"__meta_kubernetes_pod_container_init":          "false",
 					},
 					{
-						"__address__":                          "1.2.3.4",
-						"__meta_kubernetes_pod_container_name": "testcontainer1",
-						"__meta_kubernetes_pod_container_init": "false",
+						"__address__":                           "1.2.3.4",
+						"__meta_kubernetes_pod_container_name":  "testcontainer1",
+						"__meta_kubernetes_pod_container_image": "testcontainer1:latest",
+						"__meta_kubernetes_pod_container_init":  "false",
 					},
 				},
 				Labels: model.LabelSet{
@@ -267,9 +276,10 @@ func TestPodDiscoveryInitContainer(t *testing.T) {
 	key := fmt.Sprintf("pod/%s/testpod", ns)
 	expected := expectedPodTargetGroups(ns)
 	expected[key].Targets = append(expected[key].Targets, model.LabelSet{
-		"__address__":                          "1.2.3.4",
-		"__meta_kubernetes_pod_container_name": "initcontainer",
-		"__meta_kubernetes_pod_container_init": "true",
+		"__address__":                           "1.2.3.4",
+		"__meta_kubernetes_pod_container_name":  "initcontainer",
+		"__meta_kubernetes_pod_container_image": "initcontainer:latest",
+		"__meta_kubernetes_pod_container_init":  "true",
 	})
 	expected[key].Labels["__meta_kubernetes_pod_phase"] = "Pending"
 	expected[key].Labels["__meta_kubernetes_pod_ready"] = "false"
@@ -329,7 +339,8 @@ func TestPodDiscoveryUpdate(t *testing.T) {
 			NodeName: "testnode",
 			Containers: []v1.Container{
 				{
-					Name: "testcontainer",
+					Name:  "testcontainer",
+					Image: "testcontainer:latest",
 					Ports: []v1.ContainerPort{
 						{
 							Name:          "testport",
diff --git a/discovery/kubernetes/service_test.go b/discovery/kubernetes/service_test.go
index 49cb6cf91..4aeddfad7 100644
--- a/discovery/kubernetes/service_test.go
+++ b/discovery/kubernetes/service_test.go
@@ -113,10 +113,11 @@ func TestServiceDiscoveryAdd(t *testing.T) {
 				Targets: []model.LabelSet{
 					{
 						"__meta_kubernetes_service_port_protocol": "TCP",
-						"__address__":                          "testservice.default.svc:30900",
-						"__meta_kubernetes_service_type":       "ClusterIP",
-						"__meta_kubernetes_service_cluster_ip": "10.0.0.1",
-						"__meta_kubernetes_service_port_name":  "testport",
+						"__address__":                           "testservice.default.svc:30900",
+						"__meta_kubernetes_service_type":        "ClusterIP",
+						"__meta_kubernetes_service_cluster_ip":  "10.0.0.1",
+						"__meta_kubernetes_service_port_name":   "testport",
+						"__meta_kubernetes_service_port_number": "30900",
 					},
 				},
 				Labels: model.LabelSet{
@@ -132,6 +133,7 @@ func TestServiceDiscoveryAdd(t *testing.T) {
 						"__address__":                             "testservice-external.default.svc:31900",
 						"__meta_kubernetes_service_type":          "ExternalName",
 						"__meta_kubernetes_service_port_name":     "testport",
+						"__meta_kubernetes_service_port_number":   "31900",
 						"__meta_kubernetes_service_external_name": "FooExternalName",
 					},
 				},
@@ -178,17 +180,19 @@ func TestServiceDiscoveryUpdate(t *testing.T) {
 				Targets: []model.LabelSet{
 					{
 						"__meta_kubernetes_service_port_protocol": "TCP",
-						"__address__":                          "testservice.default.svc:30900",
-						"__meta_kubernetes_service_type":       "ClusterIP",
-						"__meta_kubernetes_service_cluster_ip": "10.0.0.1",
-						"__meta_kubernetes_service_port_name":  "testport0",
+						"__address__":                           "testservice.default.svc:30900",
+						"__meta_kubernetes_service_type":        "ClusterIP",
+						"__meta_kubernetes_service_cluster_ip":  "10.0.0.1",
+						"__meta_kubernetes_service_port_name":   "testport0",
+						"__meta_kubernetes_service_port_number": "30900",
 					},
 					{
 						"__meta_kubernetes_service_port_protocol": "UDP",
-						"__address__":                          "testservice.default.svc:30901",
-						"__meta_kubernetes_service_type":       "ClusterIP",
-						"__meta_kubernetes_service_cluster_ip": "10.0.0.1",
-						"__meta_kubernetes_service_port_name":  "testport1",
+						"__address__":                           "testservice.default.svc:30901",
+						"__meta_kubernetes_service_type":        "ClusterIP",
+						"__meta_kubernetes_service_cluster_ip":  "10.0.0.1",
+						"__meta_kubernetes_service_port_name":   "testport1",
+						"__meta_kubernetes_service_port_number": "30901",
 					},
 				},
 				Labels: model.LabelSet{
@@ -223,10 +227,11 @@ func TestServiceDiscoveryNamespaces(t *testing.T) {
 				Targets: []model.LabelSet{
 					{
 						"__meta_kubernetes_service_port_protocol": "TCP",
-						"__address__":                          "testservice.ns1.svc:30900",
-						"__meta_kubernetes_service_type":       "ClusterIP",
-						"__meta_kubernetes_service_cluster_ip": "10.0.0.1",
-						"__meta_kubernetes_service_port_name":  "testport",
+						"__address__":                           "testservice.ns1.svc:30900",
+						"__meta_kubernetes_service_type":        "ClusterIP",
+						"__meta_kubernetes_service_cluster_ip":  "10.0.0.1",
+						"__meta_kubernetes_service_port_name":   "testport",
+						"__meta_kubernetes_service_port_number": "30900",
 					},
 				},
 				Labels: model.LabelSet{
@@ -239,10 +244,11 @@ func TestServiceDiscoveryNamespaces(t *testing.T) {
 				Targets: []model.LabelSet{
 					{
 						"__meta_kubernetes_service_port_protocol": "TCP",
-						"__address__":                          "testservice.ns2.svc:30900",
-						"__meta_kubernetes_service_type":       "ClusterIP",
-						"__meta_kubernetes_service_cluster_ip": "10.0.0.1",
-						"__meta_kubernetes_service_port_name":  "testport",
+						"__address__":                           "testservice.ns2.svc:30900",
+						"__meta_kubernetes_service_type":        "ClusterIP",
+						"__meta_kubernetes_service_cluster_ip":  "10.0.0.1",
+						"__meta_kubernetes_service_port_name":   "testport",
+						"__meta_kubernetes_service_port_number": "30900",
 					},
 				},
 				Labels: model.LabelSet{
@@ -273,10 +279,11 @@ func TestServiceDiscoveryOwnNamespace(t *testing.T) {
 				Targets: []model.LabelSet{
 					{
 						"__meta_kubernetes_service_port_protocol": "TCP",
-						"__address__":                          "testservice.own-ns.svc:30900",
-						"__meta_kubernetes_service_type":       "ClusterIP",
-						"__meta_kubernetes_service_cluster_ip": "10.0.0.1",
-						"__meta_kubernetes_service_port_name":  "testport",
+						"__address__":                           "testservice.own-ns.svc:30900",
+						"__meta_kubernetes_service_type":        "ClusterIP",
+						"__meta_kubernetes_service_cluster_ip":  "10.0.0.1",
+						"__meta_kubernetes_service_port_name":   "testport",
+						"__meta_kubernetes_service_port_number": "30900",
 					},
 				},
 				Labels: model.LabelSet{
@@ -307,10 +314,11 @@ func TestServiceDiscoveryAllNamespaces(t *testing.T) {
 				Targets: []model.LabelSet{
 					{
 						"__meta_kubernetes_service_port_protocol": "TCP",
-						"__address__":                          "testservice.own-ns.svc:30900",
-						"__meta_kubernetes_service_type":       "ClusterIP",
-						"__meta_kubernetes_service_cluster_ip": "10.0.0.1",
-						"__meta_kubernetes_service_port_name":  "testport",
+						"__address__":                           "testservice.own-ns.svc:30900",
+						"__meta_kubernetes_service_type":        "ClusterIP",
+						"__meta_kubernetes_service_cluster_ip":  "10.0.0.1",
+						"__meta_kubernetes_service_port_name":   "testport",
+						"__meta_kubernetes_service_port_number": "30900",
 					},
 				},
 				Labels: model.LabelSet{
@@ -323,10 +331,11 @@ func TestServiceDiscoveryAllNamespaces(t *testing.T) {
 				Targets: []model.LabelSet{
 					{
 						"__meta_kubernetes_service_port_protocol": "TCP",
-						"__address__":                          "testservice.non-own-ns.svc:30900",
-						"__meta_kubernetes_service_type":       "ClusterIP",
-						"__meta_kubernetes_service_cluster_ip": "10.0.0.1",
-						"__meta_kubernetes_service_port_name":  "testport",
+						"__address__":                           "testservice.non-own-ns.svc:30900",
+						"__meta_kubernetes_service_type":        "ClusterIP",
+						"__meta_kubernetes_service_cluster_ip":  "10.0.0.1",
+						"__meta_kubernetes_service_port_name":   "testport",
+						"__meta_kubernetes_service_port_number": "30900",
 					},
 				},
 				Labels: model.LabelSet{
diff --git a/discovery/marathon/marathon_test.go b/discovery/marathon/marathon_test.go
index 415456c7a..258e3c8dd 100644
--- a/discovery/marathon/marathon_test.go
+++ b/discovery/marathon/marathon_test.go
@@ -159,10 +159,10 @@ func TestMarathonSDRemoveApp(t *testing.T) {
 	tg2 := tgs[0]
 
 	if tg2.Source != tg1.Source {
-		t.Fatalf("Source is different: %s != %s", tg1.Source, tg2.Source)
 		if len(tg2.Targets) > 0 {
-			t.Fatalf("Got a non-empty target set: %s", tg2.Targets)
+			t.Errorf("Got a non-empty target set: %s", tg2.Targets)
 		}
+		t.Fatalf("Source is different: %s != %s", tg1.Source, tg2.Source)
 	}
 }
 
diff --git a/promql/engine_test.go b/promql/engine_test.go
index e71f7a7a9..9784ac22a 100644
--- a/promql/engine_test.go
+++ b/promql/engine_test.go
@@ -3120,3 +3120,96 @@ func TestRangeQuery(t *testing.T) {
 		})
 	}
 }
+
+func TestQueryLookbackDelta(t *testing.T) {
+	var (
+		load = `load 5m
+metric 0 1 2
+`
+		query           = "metric"
+		lastDatapointTs = time.Unix(600, 0)
+	)
+
+	cases := []struct {
+		name                          string
+		ts                            time.Time
+		engineLookback, queryLookback time.Duration
+		expectSamples                 bool
+	}{
+		{
+			name:          "default lookback delta",
+			ts:            lastDatapointTs.Add(defaultLookbackDelta),
+			expectSamples: true,
+		},
+		{
+			name:          "outside default lookback delta",
+			ts:            lastDatapointTs.Add(defaultLookbackDelta + time.Millisecond),
+			expectSamples: false,
+		},
+		{
+			name:           "custom engine lookback delta",
+			ts:             lastDatapointTs.Add(10 * time.Minute),
+			engineLookback: 10 * time.Minute,
+			expectSamples:  true,
+		},
+		{
+			name:           "outside custom engine lookback delta",
+			ts:             lastDatapointTs.Add(10*time.Minute + time.Millisecond),
+			engineLookback: 10 * time.Minute,
+			expectSamples:  false,
+		},
+		{
+			name:           "custom query lookback delta",
+			ts:             lastDatapointTs.Add(20 * time.Minute),
+			engineLookback: 10 * time.Minute,
+			queryLookback:  20 * time.Minute,
+			expectSamples:  true,
+		},
+		{
+			name:           "outside custom query lookback delta",
+			ts:             lastDatapointTs.Add(20*time.Minute + time.Millisecond),
+			engineLookback: 10 * time.Minute,
+			queryLookback:  20 * time.Minute,
+			expectSamples:  false,
+		},
+		{
+			name:           "negative custom query lookback delta",
+			ts:             lastDatapointTs.Add(20 * time.Minute),
+			engineLookback: -10 * time.Minute,
+			queryLookback:  20 * time.Minute,
+			expectSamples:  true,
+		},
+	}
+
+	for _, c := range cases {
+		c := c
+		t.Run(c.name, func(t *testing.T) {
+			test, err := NewTest(t, load)
+			require.NoError(t, err)
+			defer test.Close()
+
+			err = test.Run()
+			require.NoError(t, err)
+
+			eng := test.QueryEngine()
+			if c.engineLookback != 0 {
+				eng.lookbackDelta = c.engineLookback
+			}
+			opts := &QueryOpts{
+				LookbackDelta: c.queryLookback,
+			}
+			qry, err := eng.NewInstantQuery(test.Queryable(), opts, query, c.ts)
+			require.NoError(t, err)
+
+			res := qry.Exec(test.Context())
+			require.NoError(t, res.Err)
+			vec, ok := res.Value.(Vector)
+			require.True(t, ok)
+			if c.expectSamples {
+				require.NotEmpty(t, vec)
+			} else {
+				require.Empty(t, vec)
+			}
+		})
+	}
+}
diff --git a/promql/parser/prettier_test.go b/promql/parser/prettier_test.go
index 7762e7448..16f4906f6 100644
--- a/promql/parser/prettier_test.go
+++ b/promql/parser/prettier_test.go
@@ -37,25 +37,25 @@ func TestAggregateExprPretty(t *testing.T) {
 		},
 		{
 			in: `sum without(job,foo) (task:errors:rate10s{job="s"})`,
-			out: `sum without(job, foo) (
+			out: `sum without (job, foo) (
   task:errors:rate10s{job="s"}
 )`,
 		},
 		{
 			in: `sum(task:errors:rate10s{job="s"}) without(job,foo)`,
-			out: `sum without(job, foo) (
+			out: `sum without (job, foo) (
   task:errors:rate10s{job="s"}
 )`,
 		},
 		{
 			in: `sum by(job,foo) (task:errors:rate10s{job="s"})`,
-			out: `sum by(job, foo) (
+			out: `sum by (job, foo) (
   task:errors:rate10s{job="s"}
 )`,
 		},
 		{
 			in: `sum (task:errors:rate10s{job="s"}) by(job,foo)`,
-			out: `sum by(job, foo) (
+			out: `sum by (job, foo) (
   task:errors:rate10s{job="s"}
 )`,
 		},
@@ -68,17 +68,17 @@ func TestAggregateExprPretty(t *testing.T) {
 		},
 		{
 			in: `sum by(job,foo) (sum by(job,foo) (task:errors:rate10s{job="s"}))`,
-			out: `sum by(job, foo) (
-  sum by(job, foo) (
+			out: `sum by (job, foo) (
+  sum by (job, foo) (
     task:errors:rate10s{job="s"}
   )
 )`,
 		},
 		{
 			in: `sum by(job,foo) (sum by(job,foo) (sum by(job,foo) (task:errors:rate10s{job="s"})))`,
-			out: `sum by(job, foo) (
-  sum by(job, foo) (
-    sum by(job, foo) (
+			out: `sum by (job, foo) (
+  sum by (job, foo) (
+    sum by (job, foo) (
       task:errors:rate10s{job="s"}
     )
   )
@@ -87,8 +87,8 @@ func TestAggregateExprPretty(t *testing.T) {
 		{
 			in: `sum by(job,foo)
 (sum by(job,foo) (task:errors:rate10s{job="s"}))`,
-			out: `sum by(job, foo) (
-  sum by(job, foo) (
+			out: `sum by (job, foo) (
+  sum by (job, foo) (
     task:errors:rate10s{job="s"}
   )
 )`,
@@ -96,8 +96,8 @@ func TestAggregateExprPretty(t *testing.T) {
 		{
 			in: `sum by(job,foo)
 (sum(task:errors:rate10s{job="s"}) without(job,foo))`,
-			out: `sum by(job, foo) (
-  sum without(job, foo) (
+			out: `sum by (job, foo) (
+  sum without (job, foo) (
     task:errors:rate10s{job="s"}
   )
 )`,
@@ -106,8 +106,8 @@ func TestAggregateExprPretty(t *testing.T) {
 			in: `sum by(job,foo) # Comment 1.
 (sum by(job,foo) ( # Comment 2.
 task:errors:rate10s{job="s"}))`,
-			out: `sum by(job, foo) (
-  sum by(job, foo) (
+			out: `sum by (job, foo) (
+  sum by (job, foo) (
     task:errors:rate10s{job="s"}
   )
 )`,
@@ -139,7 +139,7 @@ func TestBinaryExprPretty(t *testing.T) {
 		{
 			in: `a + ignoring(job) b`,
 			out: `  a
-+ ignoring(job)
++ ignoring (job)
   b`,
 		},
 		{
@@ -175,19 +175,21 @@ func TestBinaryExprPretty(t *testing.T) {
 		{
 			in: `foo_1 + ignoring(foo) foo_2 + ignoring(job) group_left foo_3 + on(instance) group_right foo_4`,
 			out: `      foo_1
-    + ignoring(foo)
+    + ignoring (foo)
       foo_2
-  + ignoring(job) group_left()
+  + ignoring (job) group_left ()
     foo_3
-+ on(instance) group_right()
++ on (instance) group_right ()
   foo_4`,
 		},
 	}
 	for _, test := range inputs {
-		expr, err := ParseExpr(test.in)
-		require.NoError(t, err)
+		t.Run(test.in, func(t *testing.T) {
+			expr, err := ParseExpr(test.in)
+			require.NoError(t, err)
 
-		require.Equal(t, test.out, Prettify(expr))
+			require.Equal(t, test.out, Prettify(expr))
+		})
 	}
 }
 
@@ -560,7 +562,7 @@ or
 		},
 		{
 			in: `min by (job, integration) (rate(alertmanager_notifications_failed_total{job="alertmanager", integration=~".*"}[5m]) / rate(alertmanager_notifications_total{job="alertmanager", integration="~.*"}[5m])) > 0.01`,
-			out: `  min by(job, integration) (
+			out: `  min by (job, integration) (
       rate(
         alertmanager_notifications_failed_total{integration=~".*",job="alertmanager"}[5m]
       )
@@ -575,7 +577,7 @@ or
 		{
 			in: `(count by (job) (changes(process_start_time_seconds{job="alertmanager"}[10m]) > 4) / count by (job) (up{job="alertmanager"})) >= 0.5`,
 			out: `  (
-      count by(job) (
+      count by (job) (
           changes(
             process_start_time_seconds{job="alertmanager"}[10m]
           )
@@ -583,7 +585,7 @@ or
           4
       )
     /
-      count by(job) (
+      count by (job) (
         up{job="alertmanager"}
       )
   )
@@ -630,7 +632,7 @@ func TestUnaryPretty(t *testing.T) {
 			in: `-histogram_quantile(0.99, sum by (le) (rate(foo[1m])))`,
 			out: `-histogram_quantile(
   0.99,
-  sum by(le) (
+  sum by (le) (
     rate(
       foo[1m]
     )
@@ -659,8 +661,10 @@ func TestUnaryPretty(t *testing.T) {
 		},
 	}
 	for _, test := range inputs {
-		expr, err := ParseExpr(test.in)
-		require.NoError(t, err)
-		require.Equal(t, test.out, Prettify(expr))
+		t.Run(test.in, func(t *testing.T) {
+			expr, err := ParseExpr(test.in)
+			require.NoError(t, err)
+			require.Equal(t, test.out, Prettify(expr))
+		})
 	}
 }
diff --git a/promql/parser/printer_test.go b/promql/parser/printer_test.go
index caaff7e46..a044b6969 100644
--- a/promql/parser/printer_test.go
+++ b/promql/parser/printer_test.go
@@ -33,13 +33,16 @@ func TestExprString(t *testing.T) {
 			out: `sum(task:errors:rate10s{job="s"})`,
 		},
 		{
-			in: `sum by(code) (task:errors:rate10s{job="s"})`,
+			in:  `sum by(code) (task:errors:rate10s{job="s"})`,
+			out: `sum by (code) (task:errors:rate10s{job="s"})`,
 		},
 		{
-			in: `sum without() (task:errors:rate10s{job="s"})`,
+			in:  `sum without() (task:errors:rate10s{job="s"})`,
+			out: `sum without () (task:errors:rate10s{job="s"})`,
 		},
 		{
-			in: `sum without(instance) (task:errors:rate10s{job="s"})`,
+			in:  `sum without(instance) (task:errors:rate10s{job="s"})`,
+			out: `sum without (instance) (task:errors:rate10s{job="s"})`,
 		},
 		{
 			in: `topk(5, task:errors:rate10s{job="s"})`,
@@ -48,26 +51,32 @@ func TestExprString(t *testing.T) {
 			in: `count_values("value", task:errors:rate10s{job="s"})`,
 		},
 		{
-			in: `a - on() c`,
+			in:  `a - on() c`,
+			out: `a - on () c`,
 		},
 		{
-			in: `a - on(b) c`,
+			in:  `a - on(b) c`,
+			out: `a - on (b) c`,
 		},
 		{
-			in: `a - on(b) group_left(x) c`,
+			in:  `a - on(b) group_left(x) c`,
+			out: `a - on (b) group_left (x) c`,
 		},
 		{
-			in: `a - on(b) group_left(x, y) c`,
+			in:  `a - on(b) group_left(x, y) c`,
+			out: `a - on (b) group_left (x, y) c`,
 		},
 		{
 			in:  `a - on(b) group_left c`,
-			out: `a - on(b) group_left() c`,
+			out: `a - on (b) group_left () c`,
 		},
 		{
-			in: `a - on(b) group_left() (c)`,
+			in:  `a - on(b) group_left() (c)`,
+			out: `a - on (b) group_left () (c)`,
 		},
 		{
-			in: `a - ignoring(b) c`,
+			in:  `a - ignoring(b) c`,
+			out: `a - ignoring (b) c`,
 		},
 		{
 			in:  `a - ignoring() c`,
diff --git a/rules/alerting_test.go b/rules/alerting_test.go
index efe24b027..3f3eda12a 100644
--- a/rules/alerting_test.go
+++ b/rules/alerting_test.go
@@ -416,6 +416,83 @@ func TestAlertingRuleEmptyLabelFromTemplate(t *testing.T) {
 	require.Equal(t, result, filteredRes)
 }
 
+func TestAlertingRuleQueryInTemplate(t *testing.T) {
+	suite, err := promql.NewTest(t, `
+		load 1m
+			http_requests{job="app-server", instance="0"}	70 85 70 70
+	`)
+	require.NoError(t, err)
+	defer suite.Close()
+
+	require.NoError(t, suite.Run())
+
+	expr, err := parser.ParseExpr(`sum(http_requests) < 100`)
+	require.NoError(t, err)
+
+	ruleWithQueryInTemplate := NewAlertingRule(
+		"ruleWithQueryInTemplate",
+		expr,
+		time.Minute,
+		labels.FromStrings("label", "value"),
+		labels.FromStrings("templated_label", `{{- with "sort(sum(http_requests) by (instance))" | query -}}
+{{- range $i,$v := . -}}
+instance: {{ $v.Labels.instance }}, value: {{ printf "%.0f" $v.Value }};
+{{- end -}}
+{{- end -}}
+`),
+		nil,
+		"",
+		true, log.NewNopLogger(),
+	)
+	evalTime := time.Unix(0, 0)
+
+	startQueryCh := make(chan struct{})
+	getDoneCh := make(chan struct{})
+	slowQueryFunc := func(ctx context.Context, q string, ts time.Time) (promql.Vector, error) {
+		if q == "sort(sum(http_requests) by (instance))" {
+			// This is a minimum reproduction of issue 10703, expand template with query.
+			close(startQueryCh)
+			select {
+			case <-getDoneCh:
+			case <-time.After(time.Millisecond * 10):
+				// Assert no blocking when template expanding.
+				require.Fail(t, "unexpected blocking when template expanding.")
+			}
+		}
+		return EngineQueryFunc(suite.QueryEngine(), suite.Storage())(ctx, q, ts)
+	}
+	go func() {
+		<-startQueryCh
+		_ = ruleWithQueryInTemplate.Health()
+		_ = ruleWithQueryInTemplate.LastError()
+		_ = ruleWithQueryInTemplate.GetEvaluationDuration()
+		_ = ruleWithQueryInTemplate.GetEvaluationTimestamp()
+		close(getDoneCh)
+	}()
+	_, err = ruleWithQueryInTemplate.Eval(
+		suite.Context(), evalTime, slowQueryFunc, nil, 0,
+	)
+	require.NoError(t, err)
+}
+
+func BenchmarkAlertingRuleAtomicField(b *testing.B) {
+	b.ReportAllocs()
+	rule := NewAlertingRule("bench", nil, 0, nil, nil, nil, "", true, nil)
+	done := make(chan struct{})
+	go func() {
+		for i := 0; i < b.N; i++ {
+			rule.GetEvaluationTimestamp()
+		}
+		close(done)
+	}()
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			rule.SetEvaluationTimestamp(time.Now())
+		}
+	})
+	<-done
+}
+
 func TestAlertingRuleDuplicate(t *testing.T) {
 	storage := teststorage.New(t)
 	defer storage.Close()
diff --git a/rules/manager_test.go b/rules/manager_test.go
index f69bf109a..2af757ff2 100644
--- a/rules/manager_test.go
+++ b/rules/manager_test.go
@@ -26,6 +26,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/common/model"
 	"github.com/stretchr/testify/require"
+	"go.uber.org/atomic"
 	"go.uber.org/goleak"
 	yaml "gopkg.in/yaml.v2"
 
@@ -1290,17 +1291,20 @@ func TestUpdateMissedEvalMetrics(t *testing.T) {
 		m[1] = activeAlert
 
 		rule := &AlertingRule{
-			name:           "HTTPRequestRateLow",
-			vector:         expr,
-			holdDuration:   5 * time.Minute,
-			labels:         labels.FromStrings("severity", "critical"),
-			annotations:    nil,
-			externalLabels: nil,
-			externalURL:    "",
-			health:         HealthUnknown,
-			active:         m,
-			logger:         nil,
-			restored:       true,
+			name:                "HTTPRequestRateLow",
+			vector:              expr,
+			holdDuration:        5 * time.Minute,
+			labels:              labels.FromStrings("severity", "critical"),
+			annotations:         nil,
+			externalLabels:      nil,
+			externalURL:         "",
+			active:              m,
+			logger:              nil,
+			restored:            atomic.NewBool(true),
+			health:              atomic.NewString(string(HealthUnknown)),
+			evaluationTimestamp: atomic.NewTime(time.Time{}),
+			evaluationDuration:  atomic.NewDuration(0),
+			lastError:           atomic.NewError(nil),
 		}
 
 		group := NewGroup(GroupOptions{
diff --git a/scrape/helpers_test.go b/scrape/helpers_test.go
index bead84574..8ebe3bda1 100644
--- a/scrape/helpers_test.go
+++ b/scrape/helpers_test.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/prometheus/prometheus/model/exemplar"
 	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/model/metadata"
 	"github.com/prometheus/prometheus/storage"
 )
 
@@ -39,6 +40,11 @@ func (a nopAppender) Append(storage.SeriesRef, labels.Labels, int64, float64) (s
 func (a nopAppender) AppendExemplar(storage.SeriesRef, labels.Labels, exemplar.Exemplar) (storage.SeriesRef, error) {
 	return 0, nil
 }
+
+func (a nopAppender) UpdateMetadata(storage.SeriesRef, labels.Labels, metadata.Metadata) (storage.SeriesRef, error) {
+	return 0, nil
+}
+
 func (a nopAppender) Commit() error   { return nil }
 func (a nopAppender) Rollback() error { return nil }
 
@@ -57,6 +63,8 @@ type collectResultAppender struct {
 	rolledbackResult []sample
 	pendingExemplars []exemplar.Exemplar
 	resultExemplars  []exemplar.Exemplar
+	pendingMetadata  []metadata.Metadata
+	resultMetadata   []metadata.Metadata
 }
 
 func (a *collectResultAppender) Append(ref storage.SeriesRef, lset labels.Labels, t int64, v float64) (storage.SeriesRef, error) {
@@ -89,11 +97,25 @@ func (a *collectResultAppender) AppendExemplar(ref storage.SeriesRef, l labels.L
 	return a.next.AppendExemplar(ref, l, e)
 }
 
+func (a *collectResultAppender) UpdateMetadata(ref storage.SeriesRef, l labels.Labels, m metadata.Metadata) (storage.SeriesRef, error) {
+	a.pendingMetadata = append(a.pendingMetadata, m)
+	if ref == 0 {
+		ref = storage.SeriesRef(rand.Uint64())
+	}
+	if a.next == nil {
+		return ref, nil
+	}
+
+	return a.next.UpdateMetadata(ref, l, m)
+}
+
 func (a *collectResultAppender) Commit() error {
 	a.result = append(a.result, a.pendingResult...)
 	a.resultExemplars = append(a.resultExemplars, a.pendingExemplars...)
+	a.resultMetadata = append(a.resultMetadata, a.pendingMetadata...)
 	a.pendingResult = nil
 	a.pendingExemplars = nil
+	a.pendingMetadata = nil
 	if a.next == nil {
 		return nil
 	}
diff --git a/scrape/manager_test.go b/scrape/manager_test.go
index 946091881..2994e9f97 100644
--- a/scrape/manager_test.go
+++ b/scrape/manager_test.go
@@ -31,11 +31,12 @@ import (
 
 func TestPopulateLabels(t *testing.T) {
 	cases := []struct {
-		in      labels.Labels
-		cfg     *config.ScrapeConfig
-		res     labels.Labels
-		resOrig labels.Labels
-		err     string
+		in            labels.Labels
+		cfg           *config.ScrapeConfig
+		noDefaultPort bool
+		res           labels.Labels
+		resOrig       labels.Labels
+		err           string
 	}{
 		// Regular population of scrape config options.
 		{
@@ -331,11 +332,104 @@ func TestPopulateLabels(t *testing.T) {
 			resOrig: nil,
 			err:     "scrape timeout cannot be greater than scrape interval (\"2s\" > \"1s\")",
 		},
+		// Don't attach default port.
+		{
+			in: labels.FromMap(map[string]string{
+				model.AddressLabel: "1.2.3.4",
+			}),
+			cfg: &config.ScrapeConfig{
+				Scheme:         "https",
+				MetricsPath:    "/metrics",
+				JobName:        "job",
+				ScrapeInterval: model.Duration(time.Second),
+				ScrapeTimeout:  model.Duration(time.Second),
+			},
+			noDefaultPort: true,
+			res: labels.FromMap(map[string]string{
+				model.AddressLabel:        "1.2.3.4",
+				model.InstanceLabel:       "1.2.3.4",
+				model.SchemeLabel:         "https",
+				model.MetricsPathLabel:    "/metrics",
+				model.JobLabel:            "job",
+				model.ScrapeIntervalLabel: "1s",
+				model.ScrapeTimeoutLabel:  "1s",
+			}),
+			resOrig: labels.FromMap(map[string]string{
+				model.AddressLabel:        "1.2.3.4",
+				model.SchemeLabel:         "https",
+				model.MetricsPathLabel:    "/metrics",
+				model.JobLabel:            "job",
+				model.ScrapeIntervalLabel: "1s",
+				model.ScrapeTimeoutLabel:  "1s",
+			}),
+		},
+		// Remove default port (http).
+		{
+			in: labels.FromMap(map[string]string{
+				model.AddressLabel: "1.2.3.4:80",
+			}),
+			cfg: &config.ScrapeConfig{
+				Scheme:         "http",
+				MetricsPath:    "/metrics",
+				JobName:        "job",
+				ScrapeInterval: model.Duration(time.Second),
+				ScrapeTimeout:  model.Duration(time.Second),
+			},
+			noDefaultPort: true,
+			res: labels.FromMap(map[string]string{
+				model.AddressLabel:        "1.2.3.4",
+				model.InstanceLabel:       "1.2.3.4:80",
+				model.SchemeLabel:         "http",
+				model.MetricsPathLabel:    "/metrics",
+				model.JobLabel:            "job",
+				model.ScrapeIntervalLabel: "1s",
+				model.ScrapeTimeoutLabel:  "1s",
+			}),
+			resOrig: labels.FromMap(map[string]string{
+				model.AddressLabel:        "1.2.3.4:80",
+				model.SchemeLabel:         "http",
+				model.MetricsPathLabel:    "/metrics",
+				model.JobLabel:            "job",
+				model.ScrapeIntervalLabel: "1s",
+				model.ScrapeTimeoutLabel:  "1s",
+			}),
+		},
+		// Remove default port (https).
+		{
+			in: labels.FromMap(map[string]string{
+				model.AddressLabel: "1.2.3.4:443",
+			}),
+			cfg: &config.ScrapeConfig{
+				Scheme:         "https",
+				MetricsPath:    "/metrics",
+				JobName:        "job",
+				ScrapeInterval: model.Duration(time.Second),
+				ScrapeTimeout:  model.Duration(time.Second),
+			},
+			noDefaultPort: true,
+			res: labels.FromMap(map[string]string{
+				model.AddressLabel:        "1.2.3.4",
+				model.InstanceLabel:       "1.2.3.4:443",
+				model.SchemeLabel:         "https",
+				model.MetricsPathLabel:    "/metrics",
+				model.JobLabel:            "job",
+				model.ScrapeIntervalLabel: "1s",
+				model.ScrapeTimeoutLabel:  "1s",
+			}),
+			resOrig: labels.FromMap(map[string]string{
+				model.AddressLabel:        "1.2.3.4:443",
+				model.SchemeLabel:         "https",
+				model.MetricsPathLabel:    "/metrics",
+				model.JobLabel:            "job",
+				model.ScrapeIntervalLabel: "1s",
+				model.ScrapeTimeoutLabel:  "1s",
+			}),
+		},
 	}
 	for _, c := range cases {
 		in := c.in.Copy()
 
-		res, orig, err := PopulateLabels(c.in, c.cfg)
+		res, orig, err := PopulateLabels(c.in, c.cfg, c.noDefaultPort)
 		if c.err != "" {
 			require.EqualError(t, err, c.err)
 		} else {
diff --git a/scrape/scrape_test.go b/scrape/scrape_test.go
index 9d0e16df3..2b48949c8 100644
--- a/scrape/scrape_test.go
+++ b/scrape/scrape_test.go
@@ -56,7 +56,7 @@ func TestNewScrapePool(t *testing.T) {
 	var (
 		app   = &nopAppendable{}
 		cfg   = &config.ScrapeConfig{}
-		sp, _ = newScrapePool(cfg, app, 0, nil, false, false, nil)
+		sp, _ = newScrapePool(cfg, app, 0, nil, &Options{})
 	)
 
 	if a, ok := sp.appendable.(*nopAppendable); !ok || a != app {
@@ -91,7 +91,7 @@ func TestDroppedTargetsList(t *testing.T) {
 				},
 			},
 		}
-		sp, _                  = newScrapePool(cfg, app, 0, nil, false, false, nil)
+		sp, _                  = newScrapePool(cfg, app, 0, nil, &Options{})
 		expectedLabelSetString = "{__address__=\"127.0.0.1:9090\", __scrape_interval__=\"0s\", __scrape_timeout__=\"0s\", job=\"dropMe\"}"
 		expectedLength         = 1
 	)
@@ -488,7 +488,7 @@ func TestScrapePoolTargetLimit(t *testing.T) {
 func TestScrapePoolAppender(t *testing.T) {
 	cfg := &config.ScrapeConfig{}
 	app := &nopAppendable{}
-	sp, _ := newScrapePool(cfg, app, 0, nil, false, false, nil)
+	sp, _ := newScrapePool(cfg, app, 0, nil, &Options{})
 
 	loop := sp.newLoop(scrapeLoopOptions{
 		target: &Target{},
@@ -530,7 +530,7 @@ func TestScrapePoolRaces(t *testing.T) {
 	newConfig := func() *config.ScrapeConfig {
 		return &config.ScrapeConfig{ScrapeInterval: interval, ScrapeTimeout: timeout}
 	}
-	sp, _ := newScrapePool(newConfig(), &nopAppendable{}, 0, nil, false, false, nil)
+	sp, _ := newScrapePool(newConfig(), &nopAppendable{}, 0, nil, &Options{})
 	tgts := []*targetgroup.Group{
 		{
 			Targets: []model.LabelSet{
@@ -2626,7 +2626,7 @@ func TestReuseScrapeCache(t *testing.T) {
 			ScrapeInterval: model.Duration(5 * time.Second),
 			MetricsPath:    "/metrics",
 		}
-		sp, _ = newScrapePool(cfg, app, 0, nil, false, false, nil)
+		sp, _ = newScrapePool(cfg, app, 0, nil, &Options{})
 		t1    = &Target{
 			discoveredLabels: labels.Labels{
 				labels.Label{
@@ -2839,7 +2839,7 @@ func TestReuseCacheRace(t *testing.T) {
 			ScrapeInterval: model.Duration(5 * time.Second),
 			MetricsPath:    "/metrics",
 		}
-		sp, _ = newScrapePool(cfg, app, 0, nil, false, false, nil)
+		sp, _ = newScrapePool(cfg, app, 0, nil, &Options{})
 		t1    = &Target{
 			discoveredLabels: labels.Labels{
 				labels.Label{
@@ -2971,7 +2971,7 @@ func TestScrapeReportLimit(t *testing.T) {
 	}))
 	defer ts.Close()
 
-	sp, err := newScrapePool(cfg, s, 0, nil, false, false, nil)
+	sp, err := newScrapePool(cfg, s, 0, nil, &Options{})
 	require.NoError(t, err)
 	defer sp.stop()
 
@@ -3141,7 +3141,7 @@ func TestTargetScrapeIntervalAndTimeoutRelabel(t *testing.T) {
 			},
 		},
 	}
-	sp, _ := newScrapePool(config, &nopAppendable{}, 0, nil, false, false, nil)
+	sp, _ := newScrapePool(config, &nopAppendable{}, 0, nil, &Options{})
 	tgts := []*targetgroup.Group{
 		{
 			Targets: []model.LabelSet{{model.AddressLabel: "127.0.0.1:9090"}},
diff --git a/scrape/target_test.go b/scrape/target_test.go
index f7733a598..4b9589d66 100644
--- a/scrape/target_test.go
+++ b/scrape/target_test.go
@@ -375,7 +375,7 @@ func TestTargetsFromGroup(t *testing.T) {
 		ScrapeTimeout:  model.Duration(10 * time.Second),
 		ScrapeInterval: model.Duration(1 * time.Minute),
 	}
-	targets, failures := TargetsFromGroup(&targetgroup.Group{Targets: []model.LabelSet{{}, {model.AddressLabel: "localhost:9090"}}}, &cfg)
+	targets, failures := TargetsFromGroup(&targetgroup.Group{Targets: []model.LabelSet{{}, {model.AddressLabel: "localhost:9090"}}}, &cfg, false)
 	if len(targets) != 1 {
 		t.Fatalf("Expected 1 target, got %v", len(targets))
 	}
diff --git a/storage/remote/write_handler_test.go b/storage/remote/write_handler_test.go
index 459910992..0393ee287 100644
--- a/storage/remote/write_handler_test.go
+++ b/storage/remote/write_handler_test.go
@@ -27,6 +27,7 @@ import (
 
 	"github.com/prometheus/prometheus/model/exemplar"
 	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/model/metadata"
 	"github.com/prometheus/prometheus/prompb"
 	"github.com/prometheus/prometheus/storage"
 )
@@ -186,3 +187,9 @@ func (m *mockAppendable) AppendExemplar(_ storage.SeriesRef, l labels.Labels, e
 	m.exemplars = append(m.exemplars, mockExemplar{l, e.Labels, e.Ts, e.Value})
 	return 0, nil
 }
+
+func (m *mockAppendable) UpdateMetadata(_ storage.SeriesRef, _ labels.Labels, _ metadata.Metadata) (storage.SeriesRef, error) {
+	// TODO: Wire metadata in a mockAppendable field when we get around to handling metadata in remote_write.
+	// UpdateMetadata is no-op for remote write (where mockAppendable is being used to test) for now.
+	return 0, nil
+}
diff --git a/template/template_test.go b/template/template_test.go
index 45d1a66e7..4d7524d6f 100644
--- a/template/template_test.go
+++ b/template/template_test.go
@@ -17,6 +17,7 @@ import (
 	"context"
 	"math"
 	"net/url"
+	"reflect"
 	"testing"
 	"time"
 
@@ -429,6 +430,16 @@ func TestTemplateExpansion(t *testing.T) {
 			text:   `{{ "1435065584.128" | humanizeTimestamp }}`,
 			output: "2015-06-23 13:19:44.128 +0000 UTC",
 		},
+		{
+			// ToTime - model.SampleValue input - float64.
+			text:   `{{ (1435065584.128 | toTime).Format "2006" }}`,
+			output: "2015",
+		},
+		{
+			// ToTime - model.SampleValue input - string.
+			text:   `{{ ("1435065584.128" | toTime).Format "2006" }}`,
+			output: "2015",
+		},
 		{
 			// Title.
 			text:   "{{ \"aa bb CC\" | title }}",
@@ -560,3 +571,55 @@ func testTemplateExpansion(t *testing.T, scenarios []scenario) {
 		}
 	}
 }
+
+func Test_floatToTime(t *testing.T) {
+	type args struct {
+		v float64
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *time.Time
+		wantErr bool
+	}{
+		{
+			"happy path",
+			args{
+				v: 1657155181,
+			},
+			func() *time.Time {
+				tm := time.Date(2022, 7, 7, 0, 53, 1, 0, time.UTC)
+				return &tm
+			}(),
+			false,
+		},
+		{
+			"more than math.MaxInt64",
+			args{
+				v: 1.79769313486231570814527423731704356798070e+300,
+			},
+			nil,
+			true,
+		},
+		{
+			"less than math.MinInt64",
+			args{
+				v: -1.79769313486231570814527423731704356798070e+300,
+			},
+			nil,
+			true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := floatToTime(tt.args.v)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("floatToTime() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("floatToTime() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/tsdb/db_test.go b/tsdb/db_test.go
index ce772acdd..55f0b4109 100644
--- a/tsdb/db_test.go
+++ b/tsdb/db_test.go
@@ -41,6 +41,7 @@ import (
 	"go.uber.org/goleak"
 
 	"github.com/prometheus/prometheus/model/labels"
+	"github.com/prometheus/prometheus/model/metadata"
 	"github.com/prometheus/prometheus/storage"
 	"github.com/prometheus/prometheus/tsdb/chunkenc"
 	"github.com/prometheus/prometheus/tsdb/chunks"
@@ -3489,3 +3490,254 @@ func TestDBPanicOnMmappingHeadChunk(t *testing.T) {
 
 	require.NoError(t, db.Close())
 }
+
+func TestMetadataInWAL(t *testing.T) {
+	updateMetadata := func(t *testing.T, app storage.Appender, s labels.Labels, m metadata.Metadata) {
+		_, err := app.UpdateMetadata(0, s, m)
+		require.NoError(t, err)
+	}
+
+	db := newTestDB(t)
+	ctx := context.Background()
+
+	// Add some series so we can append metadata to them.
+	app := db.Appender(ctx)
+	s1 := labels.FromStrings("a", "b")
+	s2 := labels.FromStrings("c", "d")
+	s3 := labels.FromStrings("e", "f")
+	s4 := labels.FromStrings("g", "h")
+
+	for _, s := range []labels.Labels{s1, s2, s3, s4} {
+		_, err := app.Append(0, s, 0, 0)
+		require.NoError(t, err)
+	}
+	require.NoError(t, app.Commit())
+
+	// Add a first round of metadata to the first three series.
+	// Re-take the Appender, as the previous Commit will have it closed.
+	m1 := metadata.Metadata{Type: "gauge", Unit: "unit_1", Help: "help_1"}
+	m2 := metadata.Metadata{Type: "gauge", Unit: "unit_2", Help: "help_2"}
+	m3 := metadata.Metadata{Type: "gauge", Unit: "unit_3", Help: "help_3"}
+	app = db.Appender(ctx)
+	updateMetadata(t, app, s1, m1)
+	updateMetadata(t, app, s2, m2)
+	updateMetadata(t, app, s3, m3)
+	require.NoError(t, app.Commit())
+
+	// Add a replicated metadata entry to the first series,
+	// a completely new metadata entry for the fourth series,
+	// and a changed metadata entry to the second series.
+	m4 := metadata.Metadata{Type: "counter", Unit: "unit_4", Help: "help_4"}
+	m5 := metadata.Metadata{Type: "counter", Unit: "unit_5", Help: "help_5"}
+	app = db.Appender(ctx)
+	updateMetadata(t, app, s1, m1)
+	updateMetadata(t, app, s4, m4)
+	updateMetadata(t, app, s2, m5)
+	require.NoError(t, app.Commit())
+
+	// Read the WAL to see if the disk storage format is correct.
+	recs := readTestWAL(t, path.Join(db.Dir(), "wal"))
+	var gotMetadataBlocks [][]record.RefMetadata
+	for _, rec := range recs {
+		if mr, ok := rec.([]record.RefMetadata); ok {
+			gotMetadataBlocks = append(gotMetadataBlocks, mr)
+		}
+	}
+
+	expectedMetadata := []record.RefMetadata{
+		{Ref: 1, Type: record.GetMetricType(m1.Type), Unit: m1.Unit, Help: m1.Help},
+		{Ref: 2, Type: record.GetMetricType(m2.Type), Unit: m2.Unit, Help: m2.Help},
+		{Ref: 3, Type: record.GetMetricType(m3.Type), Unit: m3.Unit, Help: m3.Help},
+		{Ref: 4, Type: record.GetMetricType(m4.Type), Unit: m4.Unit, Help: m4.Help},
+		{Ref: 2, Type: record.GetMetricType(m5.Type), Unit: m5.Unit, Help: m5.Help},
+	}
+	require.Len(t, gotMetadataBlocks, 2)
+	require.Equal(t, expectedMetadata[:3], gotMetadataBlocks[0])
+	require.Equal(t, expectedMetadata[3:], gotMetadataBlocks[1])
+}
+
+func TestMetadataCheckpointingOnlyKeepsLatestEntry(t *testing.T) {
+	updateMetadata := func(t *testing.T, app storage.Appender, s labels.Labels, m metadata.Metadata) {
+		_, err := app.UpdateMetadata(0, s, m)
+		require.NoError(t, err)
+	}
+
+	ctx := context.Background()
+	numSamples := 10000
+	hb, w := newTestHead(t, int64(numSamples)*10, false)
+
+	// Add some series so we can append metadata to them.
+	app := hb.Appender(ctx)
+	s1 := labels.FromStrings("a", "b")
+	s2 := labels.FromStrings("c", "d")
+	s3 := labels.FromStrings("e", "f")
+	s4 := labels.FromStrings("g", "h")
+
+	for _, s := range []labels.Labels{s1, s2, s3, s4} {
+		_, err := app.Append(0, s, 0, 0)
+		require.NoError(t, err)
+	}
+	require.NoError(t, app.Commit())
+
+	// Add a first round of metadata to the first three series.
+	// Re-take the Appender, as the previous Commit will have it closed.
+	m1 := metadata.Metadata{Type: "gauge", Unit: "unit_1", Help: "help_1"}
+	m2 := metadata.Metadata{Type: "gauge", Unit: "unit_2", Help: "help_2"}
+	m3 := metadata.Metadata{Type: "gauge", Unit: "unit_3", Help: "help_3"}
+	m4 := metadata.Metadata{Type: "gauge", Unit: "unit_4", Help: "help_4"}
+	app = hb.Appender(ctx)
+	updateMetadata(t, app, s1, m1)
+	updateMetadata(t, app, s2, m2)
+	updateMetadata(t, app, s3, m3)
+	updateMetadata(t, app, s4, m4)
+	require.NoError(t, app.Commit())
+
+	// Update metadata for first series.
+	m5 := metadata.Metadata{Type: "counter", Unit: "unit_5", Help: "help_5"}
+	app = hb.Appender(ctx)
+	updateMetadata(t, app, s1, m5)
+	require.NoError(t, app.Commit())
+
+	// Switch back-and-forth metadata for second series.
+	// Since it ended on a new metadata record, we expect a single new entry.
+	m6 := metadata.Metadata{Type: "counter", Unit: "unit_6", Help: "help_6"}
+
+	app = hb.Appender(ctx)
+	updateMetadata(t, app, s2, m6)
+	require.NoError(t, app.Commit())
+
+	app = hb.Appender(ctx)
+	updateMetadata(t, app, s2, m2)
+	require.NoError(t, app.Commit())
+
+	app = hb.Appender(ctx)
+	updateMetadata(t, app, s2, m6)
+	require.NoError(t, app.Commit())
+
+	app = hb.Appender(ctx)
+	updateMetadata(t, app, s2, m2)
+	require.NoError(t, app.Commit())
+
+	app = hb.Appender(ctx)
+	updateMetadata(t, app, s2, m6)
+	require.NoError(t, app.Commit())
+
+	// Let's create a checkpoint.
+	first, last, err := wal.Segments(w.Dir())
+	require.NoError(t, err)
+	keep := func(id chunks.HeadSeriesRef) bool {
+		return id != 3
+	}
+	_, err = wal.Checkpoint(log.NewNopLogger(), w, first, last-1, keep, 0)
+	require.NoError(t, err)
+
+	// Confirm there's been a checkpoint.
+	cdir, _, err := wal.LastCheckpoint(w.Dir())
+	require.NoError(t, err)
+
+	// Read in checkpoint and WAL.
+	recs := readTestWAL(t, cdir)
+	var gotMetadataBlocks [][]record.RefMetadata
+	for _, rec := range recs {
+		if mr, ok := rec.([]record.RefMetadata); ok {
+			gotMetadataBlocks = append(gotMetadataBlocks, mr)
+		}
+	}
+
+	// There should only be 1 metadata block present, with only the latest
+	// metadata kept around.
+	wantMetadata := []record.RefMetadata{
+		{Ref: 1, Type: record.GetMetricType(m5.Type), Unit: m5.Unit, Help: m5.Help},
+		{Ref: 2, Type: record.GetMetricType(m6.Type), Unit: m6.Unit, Help: m6.Help},
+		{Ref: 4, Type: record.GetMetricType(m4.Type), Unit: m4.Unit, Help: m4.Help},
+	}
+	require.Len(t, gotMetadataBlocks, 1)
+	require.Len(t, gotMetadataBlocks[0], 3)
+	gotMetadataBlock := gotMetadataBlocks[0]
+
+	sort.Slice(gotMetadataBlock, func(i, j int) bool { return gotMetadataBlock[i].Ref < gotMetadataBlock[j].Ref })
+	require.Equal(t, wantMetadata, gotMetadataBlock)
+	require.NoError(t, hb.Close())
+}
+
+func TestMetadataAssertInMemoryData(t *testing.T) {
+	updateMetadata := func(t *testing.T, app storage.Appender, s labels.Labels, m metadata.Metadata) {
+		_, err := app.UpdateMetadata(0, s, m)
+		require.NoError(t, err)
+	}
+
+	db := openTestDB(t, nil, nil)
+	ctx := context.Background()
+
+	// Add some series so we can append metadata to them.
+	app := db.Appender(ctx)
+	s1 := labels.FromStrings("a", "b")
+	s2 := labels.FromStrings("c", "d")
+	s3 := labels.FromStrings("e", "f")
+	s4 := labels.FromStrings("g", "h")
+
+	for _, s := range []labels.Labels{s1, s2, s3, s4} {
+		_, err := app.Append(0, s, 0, 0)
+		require.NoError(t, err)
+	}
+	require.NoError(t, app.Commit())
+
+	// Add a first round of metadata to the first three series.
+	// The in-memory data held in the db Head should hold the metadata.
+	m1 := metadata.Metadata{Type: "gauge", Unit: "unit_1", Help: "help_1"}
+	m2 := metadata.Metadata{Type: "gauge", Unit: "unit_2", Help: "help_2"}
+	m3 := metadata.Metadata{Type: "gauge", Unit: "unit_3", Help: "help_3"}
+	app = db.Appender(ctx)
+	updateMetadata(t, app, s1, m1)
+	updateMetadata(t, app, s2, m2)
+	updateMetadata(t, app, s3, m3)
+	require.NoError(t, app.Commit())
+
+	series1 := db.head.series.getByHash(s1.Hash(), s1)
+	series2 := db.head.series.getByHash(s2.Hash(), s2)
+	series3 := db.head.series.getByHash(s3.Hash(), s3)
+	series4 := db.head.series.getByHash(s4.Hash(), s4)
+	require.Equal(t, series1.meta, m1)
+	require.Equal(t, series2.meta, m2)
+	require.Equal(t, series3.meta, m3)
+	require.Equal(t, series4.meta, metadata.Metadata{})
+
+	// Add a replicated metadata entry to the first series,
+	// a changed metadata entry to the second series,
+	// and a completely new metadata entry for the fourth series.
+	// The in-memory data held in the db Head should be correctly updated.
+	m4 := metadata.Metadata{Type: "counter", Unit: "unit_4", Help: "help_4"}
+	m5 := metadata.Metadata{Type: "counter", Unit: "unit_5", Help: "help_5"}
+	app = db.Appender(ctx)
+	updateMetadata(t, app, s1, m1)
+	updateMetadata(t, app, s4, m4)
+	updateMetadata(t, app, s2, m5)
+	require.NoError(t, app.Commit())
+
+	series1 = db.head.series.getByHash(s1.Hash(), s1)
+	series2 = db.head.series.getByHash(s2.Hash(), s2)
+	series3 = db.head.series.getByHash(s3.Hash(), s3)
+	series4 = db.head.series.getByHash(s4.Hash(), s4)
+	require.Equal(t, series1.meta, m1)
+	require.Equal(t, series2.meta, m5)
+	require.Equal(t, series3.meta, m3)
+	require.Equal(t, series4.meta, m4)
+
+	require.NoError(t, db.Close())
+
+	// Reopen the DB, replaying the WAL. The Head must have been replayed
+	// correctly in memory.
+	reopenDB, err := Open(db.Dir(), nil, nil, nil, nil)
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		require.NoError(t, reopenDB.Close())
+	})
+
+	_, err = reopenDB.head.wal.Size()
+	require.NoError(t, err)
+
+	require.Equal(t, reopenDB.head.series.getByHash(s1.Hash(), s1).meta, m1)
+	require.Equal(t, reopenDB.head.series.getByHash(s2.Hash(), s2).meta, m5)
+	require.Equal(t, reopenDB.head.series.getByHash(s3.Hash(), s3).meta, m3)
+	require.Equal(t, reopenDB.head.series.getByHash(s4.Hash(), s4).meta, m4)
+}
diff --git a/tsdb/head_test.go b/tsdb/head_test.go
index deda89939..46bd0e260 100644
--- a/tsdb/head_test.go
+++ b/tsdb/head_test.go
@@ -123,6 +123,10 @@ func readTestWAL(t testing.TB, dir string) (recs []interface{}) {
 			tstones, err := dec.Tombstones(rec, nil)
 			require.NoError(t, err)
 			recs = append(recs, tstones)
+		case record.Metadata:
+			meta, err := dec.Metadata(rec, nil)
+			require.NoError(t, err)
+			recs = append(recs, meta)
 		default:
 			t.Fatalf("unknown record type")
 		}
@@ -1004,7 +1008,7 @@ func TestDeletedSamplesAndSeriesStillInWALAfterCheckpoint(t *testing.T) {
 	recs := readTestWAL(t, cdir)
 	recs = append(recs, readTestWAL(t, w.Dir())...)
 
-	var series, samples, stones int
+	var series, samples, stones, metadata int
 	for _, rec := range recs {
 		switch rec.(type) {
 		case []record.RefSeries:
@@ -1013,6 +1017,8 @@ func TestDeletedSamplesAndSeriesStillInWALAfterCheckpoint(t *testing.T) {
 			samples++
 		case []tombstones.Stone:
 			stones++
+		case []record.RefMetadata:
+			metadata++
 		default:
 			t.Fatalf("unknown record type")
 		}
@@ -1020,6 +1026,7 @@ func TestDeletedSamplesAndSeriesStillInWALAfterCheckpoint(t *testing.T) {
 	require.Equal(t, 1, series)
 	require.Equal(t, 9999, samples)
 	require.Equal(t, 1, stones)
+	require.Equal(t, 0, metadata)
 }
 
 func TestDelete_e2e(t *testing.T) {
diff --git a/tsdb/record/record_test.go b/tsdb/record/record_test.go
index 0e241fd2f..7a02eff3f 100644
--- a/tsdb/record/record_test.go
+++ b/tsdb/record/record_test.go
@@ -45,6 +45,30 @@ func TestRecord_EncodeDecode(t *testing.T) {
 	require.NoError(t, err)
 	require.Equal(t, series, decSeries)
 
+	metadata := []RefMetadata{
+		{
+			Ref:  100,
+			Type: uint8(Counter),
+			Unit: "",
+			Help: "some magic counter",
+		},
+		{
+			Ref:  1,
+			Type: uint8(Counter),
+			Unit: "seconds",
+			Help: "CPU time counter",
+		},
+		{
+			Ref:  147741,
+			Type: uint8(Gauge),
+			Unit: "percentage",
+			Help: "current memory usage",
+		},
+	}
+	decMetadata, err := dec.Metadata(enc.Metadata(metadata, nil), nil)
+	require.NoError(t, err)
+	require.Equal(t, metadata, decMetadata)
+
 	samples := []RefSample{
 		{Ref: 0, T: 12423423, V: 1.2345},
 		{Ref: 123, T: -1231, V: -123},
@@ -136,6 +160,16 @@ func TestRecord_Corrupted(t *testing.T) {
 		_, err := dec.Exemplars(corrupted, nil)
 		require.Equal(t, errors.Cause(err), encoding.ErrInvalidSize)
 	})
+
+	t.Run("Test corrupted metadata record", func(t *testing.T) {
+		meta := []RefMetadata{
+			{Ref: 147, Type: uint8(Counter), Unit: "unit", Help: "help"},
+		}
+
+		corrupted := enc.Metadata(meta, nil)[:8]
+		_, err := dec.Metadata(corrupted, nil)
+		require.Equal(t, errors.Cause(err), encoding.ErrInvalidSize)
+	})
 }
 
 func TestRecord_Type(t *testing.T) {
@@ -154,9 +188,80 @@ func TestRecord_Type(t *testing.T) {
 	recordType = dec.Type(enc.Tombstones(tstones, nil))
 	require.Equal(t, Tombstones, recordType)
 
+	metadata := []RefMetadata{{Ref: 147, Type: uint8(Counter), Unit: "unit", Help: "help"}}
+	recordType = dec.Type(enc.Metadata(metadata, nil))
+	require.Equal(t, Metadata, recordType)
+
 	recordType = dec.Type(nil)
 	require.Equal(t, Unknown, recordType)
 
 	recordType = dec.Type([]byte{0})
 	require.Equal(t, Unknown, recordType)
 }
+
+func TestRecord_MetadataDecodeUnknownExtraFields(t *testing.T) {
+	var enc encoding.Encbuf
+	var dec Decoder
+
+	// Write record type.
+	enc.PutByte(byte(Metadata))
+
+	// Write first metadata entry, all known fields.
+	enc.PutUvarint64(101)
+	enc.PutByte(byte(Counter))
+	enc.PutUvarint(2)
+	enc.PutUvarintStr(unitMetaName)
+	enc.PutUvarintStr("")
+	enc.PutUvarintStr(helpMetaName)
+	enc.PutUvarintStr("some magic counter")
+
+	// Write second metadata entry, known fields + unknown fields.
+	enc.PutUvarint64(99)
+	enc.PutByte(byte(Counter))
+	enc.PutUvarint(3)
+	// Known fields.
+	enc.PutUvarintStr(unitMetaName)
+	enc.PutUvarintStr("seconds")
+	enc.PutUvarintStr(helpMetaName)
+	enc.PutUvarintStr("CPU time counter")
+	// Unknown fields.
+	enc.PutUvarintStr("an extra field name to be skipped")
+	enc.PutUvarintStr("with its value")
+
+	// Write third metadata entry, with unknown fields and different order.
+	enc.PutUvarint64(47250)
+	enc.PutByte(byte(Gauge))
+	enc.PutUvarint(4)
+	enc.PutUvarintStr("extra name one")
+	enc.PutUvarintStr("extra value one")
+	enc.PutUvarintStr(helpMetaName)
+	enc.PutUvarintStr("current memory usage")
+	enc.PutUvarintStr("extra name two")
+	enc.PutUvarintStr("extra value two")
+	enc.PutUvarintStr(unitMetaName)
+	enc.PutUvarintStr("percentage")
+
+	// Should yield known fields for all entries and skip over unknown fields.
+	expectedMetadata := []RefMetadata{
+		{
+			Ref:  101,
+			Type: uint8(Counter),
+			Unit: "",
+			Help: "some magic counter",
+		}, {
+			Ref:  99,
+			Type: uint8(Counter),
+			Unit: "seconds",
+			Help: "CPU time counter",
+		}, {
+			Ref:  47250,
+			Type: uint8(Gauge),
+			Unit: "percentage",
+			Help: "current memory usage",
+		},
+	}
+
+	decMetadata, err := dec.Metadata(enc.Get(), nil)
+	require.NoError(t, err)
+	require.Equal(t, expectedMetadata, decMetadata)
+}
diff --git a/tsdb/wal/checkpoint_test.go b/tsdb/wal/checkpoint_test.go
index 63f3558af..8222f8ad3 100644
--- a/tsdb/wal/checkpoint_test.go
+++ b/tsdb/wal/checkpoint_test.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"sort"
 	"strings"
 	"testing"
 
@@ -153,6 +154,14 @@ func TestCheckpoint(t *testing.T) {
 						{Ref: 5, Labels: labels.FromStrings("a", "b", "c", "5")},
 					}, nil)
 					require.NoError(t, w.Log(b))
+
+					b = enc.Metadata([]record.RefMetadata{
+						{Ref: 2, Unit: "unit", Help: "help"},
+						{Ref: 3, Unit: "unit", Help: "help"},
+						{Ref: 4, Unit: "unit", Help: "help"},
+						{Ref: 5, Unit: "unit", Help: "help"},
+					}, nil)
+					require.NoError(t, w.Log(b))
 				}
 				// Write samples until the WAL has enough segments.
 				// Make them have drifting timestamps within a record to see that they
@@ -170,6 +179,16 @@ func TestCheckpoint(t *testing.T) {
 				}, nil)
 				require.NoError(t, w.Log(b))
 
+				// Write changing metadata for each series. In the end, only the latest
+				// version should end up in the checkpoint.
+				b = enc.Metadata([]record.RefMetadata{
+					{Ref: 0, Unit: fmt.Sprintf("%d", last), Help: fmt.Sprintf("%d", last)},
+					{Ref: 1, Unit: fmt.Sprintf("%d", last), Help: fmt.Sprintf("%d", last)},
+					{Ref: 2, Unit: fmt.Sprintf("%d", last), Help: fmt.Sprintf("%d", last)},
+					{Ref: 3, Unit: fmt.Sprintf("%d", last), Help: fmt.Sprintf("%d", last)},
+				}, nil)
+				require.NoError(t, w.Log(b))
+
 				last += 100
 			}
 			require.NoError(t, w.Close())
@@ -193,6 +212,7 @@ func TestCheckpoint(t *testing.T) {
 
 			var dec record.Decoder
 			var series []record.RefSeries
+			var metadata []record.RefMetadata
 			r := NewReader(sr)
 
 			for r.Next() {
@@ -214,14 +234,27 @@ func TestCheckpoint(t *testing.T) {
 					for _, e := range exemplars {
 						require.GreaterOrEqual(t, e.T, last/2, "exemplar with wrong timestamp")
 					}
+				case record.Metadata:
+					metadata, err = dec.Metadata(rec, metadata)
+					require.NoError(t, err)
 				}
 			}
 			require.NoError(t, r.Err())
-			require.Equal(t, []record.RefSeries{
+
+			expectedRefSeries := []record.RefSeries{
 				{Ref: 0, Labels: labels.FromStrings("a", "b", "c", "0")},
 				{Ref: 2, Labels: labels.FromStrings("a", "b", "c", "2")},
 				{Ref: 4, Labels: labels.FromStrings("a", "b", "c", "4")},
-			}, series)
+			}
+			require.Equal(t, expectedRefSeries, series)
+
+			expectedRefMetadata := []record.RefMetadata{
+				{Ref: 0, Unit: fmt.Sprintf("%d", last-100), Help: fmt.Sprintf("%d", last-100)},
+				{Ref: 2, Unit: fmt.Sprintf("%d", last-100), Help: fmt.Sprintf("%d", last-100)},
+				{Ref: 4, Unit: "unit", Help: "help"},
+			}
+			sort.Slice(metadata, func(i, j int) bool { return metadata[i].Ref < metadata[j].Ref })
+			require.Equal(t, expectedRefMetadata, metadata)
 		})
 	}
 }
diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go
index 8df755a1f..a904e47db 100644
--- a/web/api/v1/api_test.go
+++ b/web/api/v1/api_test.go
@@ -930,6 +930,20 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
 			},
 			errType: errorBadData,
 		},
+		{
+			endpoint: api.formatQuery,
+			query: url.Values{
+				"query": []string{"foo+bar"},
+			},
+			response: "foo + bar",
+		},
+		{
+			endpoint: api.formatQuery,
+			query: url.Values{
+				"query": []string{"invalid_expression/"},
+			},
+			errType: errorBadData,
+		},
 		{
 			endpoint: api.series,
 			query: url.Values{